在之前的博文中,讲过一些和Handler有关的知识,例如:

Android 多线程----AsyncTask异步任务详解

Android多线程----异步消息处理机制之Handler详解

今天再把Handler的知识回顾总结一下。

本文包括与Handler有关的以下问题:

  (1)Handler的作用

  (2)为什么Android中要设计为只能在UI线程中去更新UI呢?

  (3)Handler的两个异常

  (4)Handler、Looper MessageQueue之间的关系(源码角度)

  (5)跟线程相关的Handler,即HandlerThread(源码角度分析)

  (6)主线程往子线程发消息

一、Handler的作用:

(1)在非UI线程中完成耗时操作,在UI线程中去更新UI。

(2)可以在主线程中发送延时消息。

二、为什么Android中要设计为只能在UI线程中去更新UI呢?

(1)解决多线程并发问题(根本原因)

(2)提高界面更新的性能问题

(3)架构设计的简单

你可能会说,既然是担心多线程并发问题,那我在子线程中加锁进行更新UI行不行呢?你这样想的话,会容易造成UI卡顿的,而且性能也不好。

注1:大部分面试者很难去说出一个令面试官满意的答案。

注2:关于多线程,这里举一个例子,比如说银行取款的问题。正常情况下,银行卡余额不能少于取款金额,如果多线程进行取款的话,就会造成线程不安全。

注3:Android中之所以说架构简单,是因为帮我们封装了很多更新UI的操作。

三、Handler的两个异常:

在使用Handler时,经常会出现以下两个异常:

(1)CalledFromWrongThreadException:这种异常是因为尝试在子线程中去更新UI,进而产生异常。

(2)Can't create handle inside thread that ha not called Looper.prepared:是因为我们在子线程中去创建Handler,而产生的异常。

我们接下来通过代码来把这两个异常演示一下。

1、子线程中更新UI的异常:

(1)activity_main.xml:

  1. <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
  2. xmlns:tools="http://schemas.android.com/tools"
  3. android:layout_width="match_parent"
  4. android:layout_height="match_parent"
  5. tools:context=".MainActivity">
  6.  
  7. <TextView
  8. android:id="@+id/tv"
  9. android:layout_width="wrap_content"
  10. android:layout_height="wrap_content"
  11. android:text="@string/hello_world"/>
  12.  
  13. <Button
  14. android:id="@+id/btn"
  15. android:layout_width="match_parent"
  16. android:layout_height="wrap_content"
  17. android:text="按钮"/>
  18.  
  19. </RelativeLayout>

上方代码中,一个文本,一个按钮,代码比较简单。

(2)MainActivity.java:

  1. import android.app.Activity;
  2. import android.os.Bundle;
  3. import android.view.View;
  4. import android.widget.Button;
  5. import android.widget.TextView;
  6.  
  7. public class MainActivity extends Activity {
  8.  
  9. private TextView tv;
  10. private Button btn;
  11.  
  12. @Override
  13. protected void onCreate(Bundle savedInstanceState) {
  14. super.onCreate(savedInstanceState);
  15. setContentView(R.layout.activity_main);
  16. tv = (TextView) findViewById(R.id.tv);
  17. btn = (Button) findViewById(R.id.btn);
  18.  
  19. //点击按钮后,尝试在子线程中更新UI
  20. btn.setOnClickListener(new View.OnClickListener() {
  21. @Override
  22. public void onClick(View v) {
  23.  
  24. new Thread(new Runnable() {
  25. @Override
  26. public void run() {
  27. tv.setText("smyhvae"); //子线程中更新UI
  28. }
  29. }).start();
  30.  
  31. }
  32. });
  33. }
  34. }

上方代码中,核心代码是第27行:点击按钮后,在子线程中更新UI。

运行程序后,点击按钮,效果如下:

此时,我们来看一下后台的log日志:

上图中报的错误日志就是因为我们在子线程中去更新UI。

解决方案:

  在子线程中创建Message消息,通过Handler发给主线程,之后在Handler的handleMessage方法中获得Message消息,进而处理更新UI界面。代码如下:

  1. import android.app.Activity;
  2. import android.os.Bundle;
  3. import android.os.Handler;
  4. import android.os.Message;
  5. import android.view.View;
  6. import android.view.View.OnClickListener;
  7. import android.widget.Button;
  8. import android.widget.TextView;
  9.  
  10. public class MainActivity extends Activity implements OnClickListener {
  11. public static final int UPDATE_TEXT = 1;
  12. private TextView text;
  13. private Button changeText;
  14.  
  15. //程序一加载,直接在主线程中创建Handler
  16. private Handler handler = new Handler() {
  17. public void handleMessage(Message msg) {
  18. switch (msg.what) {
  19. case UPDATE_TEXT:
  20. text.setText("Nice to meet you");
  21. break;
  22. default:
  23. break;
  24. }
  25. }
  26. };
  27.  
  28. @Override
  29. protected void onCreate(Bundle savedInstanceState) {
  30. super.onCreate(savedInstanceState);
  31. setContentView(R.layout.activity_main);
  32. text = (TextView) findViewById(R.id.text);
  33. changeText = (Button) findViewById(R.id.change_text);
  34. changeText.setOnClickListener(this);
  35. }
  36.  
  37. @Override
  38. public void onClick(View v) {
  39. switch (v.getId()) {
  40. case R.id.change_text:
  41. new Thread(new Runnable() {
  42. @Override
  43. public void run() {
  44. Message message = new Message();
  45. message.what = UPDATE_TEXT;
  46. handler.sendMessage(message);
  47. }
  48. }).start();
  49. break;
  50. default:
  51. break;
  52. }
  53. }
  54. }

上方第44行代码也可以换成:

  1. Message message = handler.obtainMessage();

2、在子线程中创建Handler的异常:

MainActivity.java:

  1. import android.app.Activity;
  2. import android.os.Bundle;
  3. import android.os.Handler;
  4. import android.widget.TextView;
  5.  
  6. public class MainActivity extends Activity {
  7.  
  8. private TextView tv;
  9.  
  10. @Override
  11. protected void onCreate(Bundle savedInstanceState) {
  12. super.onCreate(savedInstanceState);
  13. setContentView(R.layout.activity_main);
  14. tv = (TextView) findViewById(R.id.tv);
  15.  
  16. //尝试在子线程中去创建Handler
  17. new Thread(new Runnable() {
  18. @Override
  19. public void run() {
  20. new Handler();
  21. }
  22. }).start();
  23. }
  24. }

运行程序后, 报错如下:

四、Handler、Looper MessageQueue之间的关系:(源码角度)

如果要问到Handler,这个问题基本是面试必问的。

原理分析:

Handler是Android类库提供的用于发送、处理消息或Runnable对象的处理类,它结合Message、MessageQueue和Looper类以及当前线程实现了一个消息循环机制,用于实现任务的异步加载和处理。整个异步消息处理流程的示意图如下图所示:

根据上面的图片,我们现在来解析一下异步消息处理机制

  • Message:消息体,用于装载需要发送的对象。
  • Handler:它直接继承自Object。作用是:在子线程中发送Message或者Runnable对象到MessageQueue中;在UI线程中接收、处理从MessageQueue分发出来的Message或者Runnable对象。发送消息一般使用Handler的sendMessage()方法,而发出去的消息经过处理后最终会传递到Handler的handlerMessage()方法中。
  • MessageQueue:用于存放Message或Runnable对象的消息队列。它由对应的Looper对象创建,并由Looper对象管理。每个线程中都只会有一个MessageQueue对象。
  • Looper:是每个线程中的MessageQueue的管家,负责接收和分发Message或Runnable的工作。调用Looper.loop()方法,就是一个死循环,不断地从MessageQueue中取消息:如果有消息,就取出,并调用Handler的handlerMessage()方法;如果没有消息阻塞。

现在可以做出如下总结:

(1)Handler负责发送消息,Looper负责接收Handler发送的消息放到MessageQueue,Looper又将消息回传给Handler自己。

(2)一个Handler对应一个Looper对象,一个Looper对应一个MessageQueue对象(Looper内部包含一个MessageQueue),一个Handler可以生成多个Message。

(3)Handler就是公开给外部线程的接口,用于线程间的通信。Looper是由系统支持的用于创建和管理MessageQueue的依附于一个线程的循环处理对象,而Handler是用于操作线程内部的消息队列的,所以            Handler也必须依附一个线程,而且只能是一个线程。

(4)由于Handler是在主线程中创建的,所以此时handleMessage()方法中的代码也会在主线程中运行,于是我们在这里就可以安心地进行UI操作了

生活中的例子:

上图中,可以这么理解:开会时,我(Handler)想中途离开去做别的事情,通过sendMessage发消息给领导,领导思考了一会儿,同意之后,通过Looper.looep()方法将消息回传给我,说我可以离开,然后我就调用handleMessage方法去做别的事情去了。

注:面试的时候,如果只是从字面的角度来解释Handler、Looper MessageQueue之间的关系,并不能够真正打动面试官,倒不如再举一个生动的例子,让面试官觉得你是懂面向对象的思维的。

五、跟线程相关的Handler,即HandlerThread(源码角度分析)

这个问题可以看一下这篇博客:

http://blog.csdn.net/lmj623565791/article/details/47079737

六、主线程往子线程发消息:(考察你是否真的理解Handler机制)

  我们在平时开发的过程中,经常是子线程往主线程中发消息,让主线程更新UI。但是根据具体的项目需求,也可能会要求让你在主线程中往子线程中发消息。

  1. import android.app.Activity;
  2. import android.os.Bundle;
  3. import android.os.Handler;
  4. import android.os.Looper;
  5. import android.os.Message;
  6. import android.util.Log;
  7. import android.view.View;
  8. import android.view.View.OnClickListener;
  9. import android.widget.Button;
  10. import android.widget.TextView;
  11.  
  12. public class MainActivity extends Activity implements OnClickListener {
  13. public static final int UPDATE_TEXT = 1;
  14. private TextView tv;
  15. private Button btn;
  16. private Handler handler;
  17.  
  18. @Override
  19. protected void onCreate(Bundle savedInstanceState) {
  20. super.onCreate(savedInstanceState);
  21. setContentView(R.layout.activity_main);
  22. tv = (TextView) findViewById(R.id.tv);
  23. btn = (Button) findViewById(R.id.btn);
  24. btn.setOnClickListener(this);
  25. //疑问:为什么这段代码如果写在onClick方法里面会报空指针?
  26. new Thread(new Runnable() {
  27. @Override
  28. public void run() {
  29. //1、准备Looper对象
  30. Looper.prepare();
  31. //2、在子线程中创建Handler
  32. handler = new Handler() {
  33. @Override
  34. public void handleMessage(Message msg) {
  35. super.handleMessage(msg);
  36. Log.i("handleMessage:", Thread.currentThread().getName());
  37. Log.i("后台输出", "收到了消息对象");
  38. }
  39. };
  40. //3、调用Looper的loop()方法,取出消息对象
  41. Looper.loop();
  42. }
  43. }).start();
  44.  
  45. }
  46. @Override
  47. public void onClick(View v) {
  48. Log.i("onClick:", Thread.currentThread().getName());
  49. switch (v.getId()) {
  50. case R.id.btn:
  51. Message msg = handler.obtainMessage();
  52. handler.sendMessage(msg);
  53. break;
  54.  
  55. default:
  56. break;
  57. }
  58. }
  59. }

上方的第29行至41行代码:这是MainThread中发送消息,在子线程中接收消息的固定写法。上面的三个步骤再重复一下:

  • 准备Looper对象
  • 在WorkerThread当中生成一个Handler对象
  • 调用Looper的loop()方法之后,Looper对象将不断地从消息队列当中取出对象,然后调用handler的handleMessage()方法,处理该消息对象;如果消息队列中没有对象,则该线程阻塞

注意,此时handleMessage()方法是在子线程中运行的。

后台运行效果:

小小地总结一下:

  首先执行Looper的prepare()方法,这个方法有两个作用:一是生成Looper对象,而是把Looper对象和当前线程对象形成键值对(线程为键),存放在ThreadLocal当中,然后生成handler对象,调用Looper的myLooper()方法,得到与Handler所对应的Looper对象,这样的话,handler、looper 、消息队列就形成了一一对应的关系,然后执行上面的第三个步骤,即Looper在消息队列当中循环的取数据。

另外,在本文最开头的第一段中,我们在主线程中创建Handler也没有调用Looper.prepare()方法,为什么就没有崩溃呢?,这是由于在程序启动的时候,系统已经帮我们自动调用了Looper.prepare()方法。查看ActivityThread中的main()方法,代码如下所示:

  1. public static void main(String[] args) {
  2. SamplingProfilerIntegration.start();
  3. CloseGuard.setEnabled(false);
  4. Environment.initForCurrentUser();
  5. EventLogger.setReporter(new EventLoggingReporter());
  6. Process.setArgV0("<pre-initialized>");
  7. Looper.prepareMainLooper();
  8. ActivityThread thread = new ActivityThread();
  9. thread.attach(false);
  10. if (sMainThreadHandler == null) {
  11. sMainThreadHandler = thread.getHandler();
  12. }
  13. AsyncTask.init();
  14. if (false) {
  15. Looper.myLooper().setMessageLogging(new LogPrinter(Log.DEBUG, "ActivityThread"));
  16. }
  17. Looper.loop();
  18. throw new RuntimeException("Main thread loop unexpectedly exited");
  19. }

上方代码中,可以看到,在第7行调用了Looper.prepareMainLooper()方法,而这个方法又会再去调用Looper.prepare()方法,代码如下所示:

  1. public static final void prepareMainLooper() {
  2. prepare();
  3. setMainLooper(myLooper());
  4. if (Process.supportsProcesses()) {
  5. myLooper().mQueue.mQuitAllowed = false;
  6. }
  7. }

总结:这样基本就将Handler的创建过程完全搞明白了,总结一下就是在主线程中可以直接创建Handler对象,而在子线程中需要先调用Looper.prepare()才能创建Handler对象。

六、为什么在有些时候子线程中是可以直接更新UI的:

这道面试题应该是本文中最难的一个面试题了,需要好好理解。为了回答这个问题,我们需要先通过看源码去了解下面这三个问题:

(1)Android是如何检测非UI线程去更新UI的

(2)ViewRootImp是什么?

(3)ViewRootImp是在哪里创建的?

源码我就不贴出来了,这里我只是总结一下。

答案:

  非UI线程真的不能更新UI吗? 是可以的

解释:

  在线程中更新UI时会调用ViewParent.invalidateChild()方法检查当前的thread是否是Mainthread

具体源码如下:

  1. final ViewParent p = mParent;
  2. if (p != null && ai != null && l < r && t < b) {
  3. final Rect damage = ai.mTmpInvalRect;
  4. damage.set(l, t, r, b);
  5. p.invalidateChild(this, damage);
  6. }

而ViewParent是一个接口类,其实现类是ViewRootImpl,通过查看invalidateChild()方法里面的代码就可以看到会他调用checkThread()方法。checkThread()方法如下:

  1. void checkThread() {
  2. if (mThread != Thread.currentThread()) { //检查更新UI的线程是否是MainThread
  3. throw new CalledFromWrongThreadException(
  4. "Only the original thread that created a view hierarchy can touch its views.");
  5. }
  6. }

上面的第02行就是检查:在线程中更新UI时当前线程是否是MainThread。

但是,ViewRootImpl这个类是在activity的onResume()方法中创建的。就算在子线程中更新UI,只要在ViewRootImpl创建之前更新UI(比如,程序在执行onCreate方法时,我就去执行setText方法区更新UI),就可以逃避掉checkThread()的检查。

关于本题,给出以下链接大家去细读一下源码吧:

Android更新Ui进阶精解(一):

http://www.jianshu.com/p/6de0a42a44d6

为什么我们可以在非UI线程中更新UI:

http://blog.csdn.net/aigestudio/article/details/43449123

我的公众号

下图是我的微信公众号(生命团队id:vitateam),欢迎有心人关注。博客园分享技术,公众号分享心智

我会很感激第一批关注我的人。此时,年轻的我和你,一无所有;而后,富裕的你和我,满载而归。

Android中关于Handler的若干思考的更多相关文章

  1. Android中使用Handler造成内存泄露的分析和解决

    什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...

  2. Android中使用Handler造成内存泄露

    1.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...

  3. Android中的Handler的机制与用法详解

    概述: 很多android初学者对android 中的handler不是很明白,其实Google参考了Windows的消息处理机制, 在Android系统中实现了一套类似的消息处理机制.在下面介绍ha ...

  4. android中的Handler和Runnable

    最近在做一个项目,在网络请求时考虑用Handler进行处理,然后就研究了一下Handler和Runnable 首先在看一下java中的Runnable The Runnable interface s ...

  5. Android中利用Handler实现消息的分发机制(三)

    在第二篇文章<Android中利用Handler实现消息的分发机制(一)>中,我们讲到主线程的Looper是Android系统在启动App的时候,已经帮我们创建好了,而假设在子线程中须要去 ...

  6. 转:Android中的Handler的机制与用法详解

    注:Message类的用法: message的几个参数都可以携带数据,其中arg1与arg2可以携带int类型,what是用户自定义的int型,这样接受者可以了解这个消息的信息. 说明:使用Messa ...

  7. 深入源代码解析Android中的Handler,Message,MessageQueue,Looper

    本文主要是对Handler和消息循环的实现原理进行源代码分析.假设不熟悉Handler能够參见博文< Android中Handler的使用>,里面对Android为何以引入Handler机 ...

  8. Android中的Handler及它所引出的Looper、MessageQueue、Message

    0.引入 0.1.线程间通信的目的 首先,线程间通信要交流些什么呢? 解答这个问题要从为什么要有多线程开始,需要多线程的原因大概有这些 最早也最基本:有的任务需要大量的时间,但其实并不占用计算资源,比 ...

  9. Android中由Handler和内部类引起的内存泄漏

    原文地址:http://johnnyshieh.github.io/android/2015/09/03/android-handler-memory-leak/ 在Android中我们经常用Hand ...

随机推荐

  1. vs2010 未能将脚本调试器附加到计算机上的进程。已附加了一个调试器

    图片: 上周不小心升级了IE10,今天,VS2010报:未能将脚本调试器附加到计算机XXX上的进程iexplore.exe . 已附加了一个调试器”.启动调试失败. 到网上查找解决办法,最后用这个解决 ...

  2. JavaScript获取浏览器高度和宽度值

    IE中:  document.body.clientWidth ==> *DY对象宽度 document.body.clientHeight ==> *DY对象高度 document.do ...

  3. mongodb 数据库操作--备份 还原 导出 导入

    mongodb数据备份和还原主要分为二种,一种是针对于库的mongodump和mongorestore,一种是针对库中表的mongoexport和mongoimport 一,mongodump备份数据 ...

  4. CSS常用样式(三)

    一.2D变换 1.transform   设置或检索对象的转换 取值: none::以一个含六值的(a,b,c,d,e,f)变换矩阵的形式指定一个2D变换,相当于直接应用一个[a,b,c,d,e,f] ...

  5. android Activity runOnUiThread() 方法使用

    在android 中我们一般用 Handler 做主线程 和 子线程 之间的通信 . 现在有了一种更为简洁的写法,就是 Activity 里面的 runOnUiThread( Runnable )方法 ...

  6. NSDate 格式化 NSDate to NSString

    NSLog(@"%@",[NSDate stringFromDate:[NSDate date] withFormat:@"yyyyMMdd__HH_mm_ss_zzz& ...

  7. DB2导入导出数据库数据

    导出数据库中数据 在db2cmd命令下生成建库脚本(-z指定模式名) db2look -d BBS -z db2admin -u db2admin -e -o bbs.sql 在db2cmd命令下导出 ...

  8. 使用sencha cmd 一键生成你的应用程序代码

    一键生成你的应用程序代码: ------------------------------------------------------------ 我们的出发点就是使用命令来产生一个应用程序,执行以 ...

  9. 未能正确加载 ”Microsoft.VisualStudio.Editor.Implementation.EditorPackate“包错误解决方法

    今天新来一个同事,帮他搭建开发环境.发现他的vs2012一打开就报错. 错误提示: 未能正确加载 "Microsoft.VisualStudio.Editor.Implementation. ...

  10. U-BLOX GPS 模块及GPRMC指令解析

    受朋友所托,调试一款GPS模块,该模块是UBLOX的NEO-6M GPS模组.想到用这款GPS的人较多,自己日后也有可能在用到这个模块,就写下这份笔记. 1. 介绍 基本信息如下: 1, 模块采用U- ...