type
status
date
slug
summary
tags
category
icon
password
1. Handler 的作用
为了保障线程安全,Android规定只能由主线程来更新UI信息。在实际开发中,经常会遇到多个子线程同时操作UI信息的情况,这会导致UI线程不安全。为了解决这个问题,我们可以使用Handler作为媒介,让Handler通知主线程按顺序一个个去更新UI,以避免UI线程不安全。
当子线程需要更新UI信息时,我们需要将要更新的消息传递到UI主线程中,然后由主线程完成更新。这样就实现了工作线程对UI的更新处理,并最终完成了异步消息的处理(如图1所示)。
2. Handler 相关概念解释
主要涉及的有:处理器(Handler)、消息(Message)、消息队列(Message Queue)、循环器(Looper)。
概念 | 定义 | 作用 |
Message | 线程间通讯的数据单元(即Handler接受/处理的对象) | 存储需要操作的信息 |
Message Queue | 一种数据结构(先进先出) | 存储Handler发来的消息 |
Handler | 主线程与子线程的通讯媒介线程消息的处理者 | 添加消息(Message)到消息队列(Message Queue)处理由循环器(Looper)分配过来的消息(Message)。 |
Looper | Message Queue 与 Handler的通讯媒介 | 消息获取:循环取出essage Queue中的Message <br />消息分发:将取出的Message发送给对应的Handler |
3.Handler 工作流程
4.Handler 使用
4.1子线程向主线程发消息
我们一般使用handler发送消息,只需要两步,首先是创建一个Handler对象,并重写handleMessage方法,就是上图中的3(Message.target.handleMeesage),然后需要消息通信的地方,通过Handler的sendMessage方法发送消息(这里我们创建了一个子线程,模拟子线程向主线程发送消息)。代码如下:
4.2主线程向子线程发消息
handler需要与looper绑定,在主线程开始的时候会自动创建一个looper,而在子线程中需要我们自己去创建looper。所以使用Handler通信之前需要有以下三步:
- 调用Looper.prepare()
- 创建Handler对象
- 调用Looper.loop()
代码如下:
5. Handler机制原理
5.1 Looper.prepare()
从上面的代码可以看出,一个线程最多只有一个Looper对象。当没有Looper对象时,去创建一个Looper,并存放到sThreadLocal中,sThreadLocal是一个static的ThreadLocal对象,它存储了Looper对象的副本,并且可以通过它取得当前线程在之前存储的Looper的副本。如下图:
Looper的构造方法:
这里主要就是创建了消息队列MessageQueue,并让它供Looper持有,因为一个线程最大只有一个Looper对象,所以一个线程最多也只有一个消息队列。然后再把当前线程赋值给mThread。
MessageQueue的构造方法没有什么可讲的,它就是一个消息队列,用于存放Message。
所以Looper.prepare()的作用主要有以下三点
- 创建Looper对象
- 创建MessageQueue对象,并让Looper对象持有
- 让Looper对象持有当前线程
5.2 new Handler()
Handler的创建过程主要有以下几点
- 创建Handler对象
- 得到当前线程的Looper对象,并判断是否为空
- 让创建的Handler对象持有Looper、MessageQueu、Callback的引用
5.3 Looper.loop()
首先还是判断了当前线程是否有Looper,然后得到当前线程的MessageQueue。接下来,就是最关键的代码了,写了一个死循环,不断调用MessageQueue的next方法取出MessageQueue中的Message,注意,当MessageQueue中没有消息时,next方法会阻塞,导致当前线程挂起,后面会讲到。
拿到Message以后,会调用它的target的dispatchMessage方法,这个target其实就是发送消息时用到的Handler。所以就是调用Handler的dispatchMessage方法,代码如下:
可以看出,这个方法就是从MessageQueue中取出Message以后,进行分发处理。
首先,判断msg.callback是不是空,其实msg.callback是一个Runnable对象,是Handler.post方式传递进来的参数,后面会讲到。而hanldeCallback就是调用的Runnable的run方法。
然后,判断mCallback是否为空,这是一个Handler.Callback的接口类型,之前说了Handler有多个构造方法,可以提供设置Callback,如果这里不为空,则调用它的hanldeMessage方法,注意,这个方法有返回值,如果返回了true,表示已经处理 ,不再调用Handler的handleMessage方法;如果mCallback为空,或者不为空但是它的handleMessage返回了false,则会继续调用Handler的handleMessage方法,该方法就是我们经常重写的那个方法。
关于从MessageQueue中取出消息以后的分发,如下面的流程图所示:
5.4发送消息
使用Handler发送消息主要有两种,一种是sendMessage方式,还有一个post方式,不过两种方式最后都会调用到sendMessageDelayed方法。
sendMessage方法传入的是Message,将Message传入Message Queue。
post方法代码:
可以看到,post方法只是先调用了getPostMessage方法,用Runnable去封装一个Message,然后就调用了sendMessageDelayed,把封装的Message加入到MessageQueue中。
所以使用handler发送消息的本质都是:把Message加入到Handler中的MessageQueue中去。
6.Handler的内存泄漏
Handler的常用方式:
但是会有一个问题,我们进入这个页面然后点击按钮,发送一个延时100s的消息,再退出这个Activity,这时候可能导致内存泄漏。
根本原因是因为我们
创建的匿名内部类Handler对象持有了外部类Activity的对象
,我们知道,当使用handler发送消息时,会把handler作为Message的target保存到MessageQueue,由于延时了100s,所以这个Message暂时没有得到处理,这时候它们的引用关系为MessageQueue持有了Message,Message持有了Handler,Handler持有了Activity,如下图所示当退出这个Activity时,因为Handler还持有Activity,所以gc时不能回收该Activity,导致了内存泄漏。
解决方案:
静态内部类+弱引用
静态内部类是不会引用外部类的对象的,但是既然静态内部类对象没有持有外部类的对象,那么我们怎么去调用外部类Activity的方法呢?答案是使用弱引用。代码如下:
首先,我们自定义了一个静态内部类MyHandler,然后创建MyHandler对象时传入当前Activity的对象,供Hander以弱应用的方式持有,这个时候Activity就被强引用和弱引用两种方式引用了,我们继续发起一个延时100s的消息,然后退出当前Activity,这个时候Activity的强引用就不存在了,只存在弱引用,gc运行时会回收掉只有弱引用的Activity,这样就不会造成内存泄漏了。
但这个延时消息还是存在于MessageQueue中,得到这个Message被取出时,还是会进行分发处理,只是这时候Activity被回收掉了,activity为null,不能再继续调用Activity的方法了。所以,其实这是Activity可以被回收了,而Handler、Message都不能被回收。
至于为什么使用弱引用而没有使用软引用,其实很简单,对比下两者回收前提条件就清楚了
- 弱引用(WeakReference): gc运行时,无论内存是否充足,只有弱引用的对象就会被回收
- 软引用(SoftReference): gc运行时,只有内存不足时,只有软引用的对象就会被回收
很明显,当我们Activity退出时,我们希望不管内存是否足够,都应该回收Activity对象,所以使用弱引用合适。
7 同步屏障
7.Handler面试常见问题
1、线程、Looper、Handler之间的关系如下:
- 一个线程只能绑定一个Looper,一个MessageQueue;但一个Thread可以有多个Handler。
- 一个Looper可绑定多个Handler,一个MessageQueue。
- 一个Handler只能绑定一个Looper。
2、子线程中创建 Handler 对象
不可以在子线程中直接调用 Handler 的无参构造方法,因为
Handler
在创建时必须要绑定一个 Looper
对象3、Handler 是如何与 Looper 关联的?
(1)通过构造方法传参
(2)直接调用无参构造方法自动绑定
4、Looper 是如何与 Thread 关联的
Looper 与 Thread 之间是通过 ThreadLocal 关联的,这个可以看
Looper.prepare()
方法Looper
中有一个 ThreadLocal
类型的 sThreadLocal
静态字段,Looper
通过它的 get
和 set
方法来赋值和取值。由于
ThreadLocal
是与线程绑定的,所以我们只要把 Looper
与 ThreadLocal
绑定了,那 Looper
和 Thread
也就关联上了5、在子线程中如何获取当前线程的 Looper
6、Looper.loop() 会退出吗?
不会自动退出,但是我们可以通过
Looper.quit()
或者 Looper.quitSafely()
让它退出。两个方法都是调用了
MessageQueue.quit(boolean)
方法,当 MessageQueue.next()
方法发现已经调用过 MessageQueue.quit(boolean)
时会 return null
结束当前调用,否则的话即使 MessageQueue
已经是空的了也会阻塞等待7、MessageQueue#next 在没有消息的时候会阻塞,如何恢复?
当其他线程调用
MessageQueue#enqueueMessage
时会唤醒 MessageQueue
,这个方法会被 Handler#sendMessage
、Handler#post
等一系列发送消息的方法调用。8、Looper.loop() 方法是一个死循环为什么不会阻塞APP
线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程肯定不能运行一段时间后就自动结束了,那么如何保证一直存活呢??简单的做法就是可执行代码能一直执行下去,死循环便能保证不会被退出。把所有要做的任务放到循环中去做就不会觉得卡了。
9、子线程更新UI的方式
- Handler的sendMessage方式
- Handler的post方式
- Activity的runOnUiThread方法
- View的post方式
10、总结
Android中,有哪些是基于Handler来实现通信的?答:App的运行、更新UI、AsyncTask、Glide、RxJava等处理Handler消息,是在哪个线程?一定是创建Handler的线程么?答:创建Handler所使用的Looper所在的线程消息是如何插入到MessageQueue中的?答: 是根据when在MessageQueue中升序排序的,when=开机到现在的毫秒数+延时毫秒数当MessageQueue没有消息时,它的next方法是阻塞的,会导致App ANR么?答:不会导致App的ANR,是Linux的pipe机制保证的,阻塞时,线程挂起;需要时,唤醒线程子线程中可以使用Toast么?答:可以使用,但是Toast的显示是基于Handler实现的,所以需要先创建Looper,然后调用Looper.loop。Looper.loop()是死循环,可以停止么?答:可以停止,Looper提供了quit和quitSafely方法Handler内存泄露怎么解决?答: 静态内部类+弱引用 、Handler的removeCallbacksAndMessages等方法移除MessageQueue中的消息为什么不能在子线程中更新UI?答:Android UI操作并不是线程安全的,如果在多个线程中同时操作,可能会导致UI状态不一致。因此,Android规定只有主线程可以操作UI。如果在子线程中更新UI,系统会抛出异常。Handler有哪些常见的使用场景?答:Handler常见的使用场景有:定时执行任务,延时执行任务,执行耗时操作然后更新UI,线程间通信等。
- 作者:LuluNotion
- 链接:https://tangly1024.com/article/android-basic-handler
- 声明:本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。
相关文章