type
status
date
slug
summary
tags
category
icon
password

1. Handler 的作用

为了保障线程安全,Android规定只能由主线程来更新UI信息。在实际开发中,经常会遇到多个子线程同时操作UI信息的情况,这会导致UI线程不安全。为了解决这个问题,我们可以使用Handler作为媒介,让Handler通知主线程按顺序一个个去更新UI,以避免UI线程不安全。
当子线程需要更新UI信息时,我们需要将要更新的消息传递到UI主线程中,然后由主线程完成更新。这样就实现了工作线程对UI的更新处理,并最终完成了异步消息的处理(如图1所示)。
图1 子线程通过Handler在UI线程更新UI
图1 子线程通过Handler在UI线程更新UI

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 工作流程

notion image
 
notion image

4.Handler 使用

4.1子线程向主线程发消息

我们一般使用handler发送消息,只需要两步,首先是创建一个Handler对象,并重写handleMessage方法,就是上图中的3(Message.target.handleMeesage),然后需要消息通信的地方,通过Handler的sendMessage方法发送消息(这里我们创建了一个子线程,模拟子线程向主线程发送消息)。代码如下:

4.2主线程向子线程发消息

handler需要与looper绑定,在主线程开始的时候会自动创建一个looper,而在子线程中需要我们自己去创建looper。所以使用Handler通信之前需要有以下三步:
  1. 调用Looper.prepare()
  1. 创建Handler对象
  1. 调用Looper.loop()
代码如下:

5. Handler机制原理

5.1 Looper.prepare()

从上面的代码可以看出,一个线程最多只有一个Looper对象。当没有Looper对象时,去创建一个Looper,并存放到sThreadLocal中,sThreadLocal是一个static的ThreadLocal对象,它存储了Looper对象的副本,并且可以通过它取得当前线程在之前存储的Looper的副本。如下图:
notion image
Looper的构造方法:
这里主要就是创建了消息队列MessageQueue,并让它供Looper持有,因为一个线程最大只有一个Looper对象,所以一个线程最多也只有一个消息队列。然后再把当前线程赋值给mThread。
MessageQueue的构造方法没有什么可讲的,它就是一个消息队列,用于存放Message。
所以Looper.prepare()的作用主要有以下三点
  1. 创建Looper对象
  1. 创建MessageQueue对象,并让Looper对象持有
  1. 让Looper对象持有当前线程

5.2 new Handler()

Handler的创建过程主要有以下几点
  1. 创建Handler对象
  1. 得到当前线程的Looper对象,并判断是否为空
  1. 让创建的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中取出消息以后的分发,如下面的流程图所示:
notion image

5.4发送消息

使用Handler发送消息主要有两种,一种是sendMessage方式,还有一个post方式,不过两种方式最后都会调用到sendMessageDelayed方法。
notion image
notion image
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,如下图所示
notion image
当退出这个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都不能被回收。
至于为什么使用弱引用而没有使用软引用,其实很简单,对比下两者回收前提条件就清楚了
  1. 弱引用(WeakReference): gc运行时,无论内存是否充足,只有弱引用的对象就会被回收
  1. 软引用(SoftReference): gc运行时,只有内存不足时,只有软引用的对象就会被回收
很明显,当我们Activity退出时,我们希望不管内存是否足够,都应该回收Activity对象,所以使用弱引用合适。

7 同步屏障

7.Handler面试常见问题

1、线程、Looper、Handler之间的关系如下:

  • 一个线程只能绑定一个Looper,一个MessageQueue;但一个Thread可以有多个Handler。
  • 一个Looper可绑定多个Handler,一个MessageQueue。
  • 一个Handler只能绑定一个Looper。
notion image
 

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#sendMessageHandler#post 等一系列发送消息的方法调用。

8、Looper.loop() 方法是一个死循环为什么不会阻塞APP

线程是一段可执行的代码,当可执行代码执行完成后,线程生命周期便该终止了,线程退出。而对于主线程肯定不能运行一段时间后就自动结束了,那么如何保证一直存活呢??简单的做法就是可执行代码能一直执行下去,死循环便能保证不会被退出。把所有要做的任务放到循环中去做就不会觉得卡了。

9、子线程更新UI的方式

  1. Handler的sendMessage方式
  1. Handler的post方式
  1. Activity的runOnUiThread方法
  1. 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,线程间通信等。
 
Android动态高斯模糊蒙层Android基础-ANR
LuluNotion
LuluNotion
一个普通的干饭人🍚
公告
type
status
date
slug
summary
tags
category
icon
password
🎉NotionNext 4.0即将到来🎉
-- 感谢您的支持 ---
👏欢迎更新体验👏