04.Android消息机制
目录介绍
- 01.Handler必备知识点
- 1.1 消息机制的背景
- 1.2 Handler消息类型
- 1.3 Handler四要素
- 03.子线程的消息处理
- 3.1 子线程中定义Handler
- 3.2 不建议在子线程访问UI
- 3.3 子线程更新UI方式
- 3.9 解决handler内存泄露
- 04.消息机制源码分析
- 4.1 Handler源码分析
- 4.2 Message源码分析
- 4.3 MessageQueue源码
- 4.4 Looper源码分析
- 05.消息机制设计思想
- 5.1 为何要设计消息机制
- 5.2 消息机制核心思想
- 06.Handler同步屏障
- 6.1 什么是同步屏障
- 07.消息机制问题和思考
- 7.1 发送消息Delay可靠吗
- 7.2 Looper停止App就退出吗
00.问题答疑思考
- 请说一下Android消息机制?子线程中是否可以new一个handler对象?会出现什么问题,为什么?如何提高Message的优先级?
- 阻塞唤醒原理。这里一般是会问为何loop()方法是死循环却不会占用cpu时间片 or 为何next()方法阻塞却不会卡死。更深一点会问到Linux的IO多路复用epoll原理。
- Looper从消息队列中取出消息后,如何传递给handler?消息队列MessageQuee底层怎么实现的?handler里面的nativePollOnce 为什么不会anr?
- Handler、Thread和HandlerThread的差别 ?HandlerThread是怎么实现的?
- Looper.loop()为什么不会阻塞主线程,IdleHandler(闲时机制)是做什么用的?
- Handler机制,IdleHandler执行时机。Handler#postDelay(runnable, 20s) 一个消息,然后把手机时间调整为1分钟后,刚才的runnable会不会执行。
- ThreadLocal是如何做到线程间的不共享数据的,ThreadLocalMap里面的key和value是什么。实现及如何保证Local属性?
- 说下handler的流程,异步消息是什么?Android中哪些场景会发送异步消息?我们在代码中可以手动发异步消息吗?
- 什么是消息同步屏障?Android中是如何实现消息同步屏障的?同步屏障的目的主要是为了什么?
- Choreographer是什么?Choreographer发出异步消息后如何执行任务?
01.Handler必备知识点
1.1 消息机制的背景
- Android应用程序与传统的PC应用程序一样,都是消息驱动的。
- 也就是说,在Android应用程序主线程中,所有函数都是在一个消息循环中执行的。
- Android应用程序其它线程,也可以像主线程一样,拥有消息循环。
- Android应用程序主线程是一个特殊的线程,因为它同时也是UI线程以及触摸屏、键盘等输入事件处理线程。
- 主要搞清楚那些知识点:
- 线程与消息的关系;线程的消息队列创建;线程的消息循环;线程的消息发送;线程的消息处理;消息在异步任务的应用
1.2 Handler消息类型
- Handler的Message种类分为三种:1.普通消息;2.异步消息;3.屏障消息
- 普通消息
- 普通消息又称为同步消息,我们平时发的消息基本都是同步消息。
- 异步消息
- Handler的构造方法有个async参数,默认的构造方法此参数是false,只要我们在构造handler对象的时候,把该参数设置为true就可以。
- 在创建Message对象时,直接调用Message的setAsynchronous()方法。
- 普通消息和异步消息区别
- 在一般情况下,异步消息和同步消息没有什么区别,但是一旦开启了同步屏障以后就有区别了。
1.3 Handler四要素
- 作用:
- 跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
- 四要素:
- Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
- MessageQueue(消息队列):用来存放Handler发送过来的消息,通过 Handler 发送的消息并非是立即执行的,需要存入一个消息队列中来依次执行,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
- Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
- Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。Looper 不断从 MessageQueue 中获取消息并将之传递给消息处理者(即是消息发送者 Handler 本身)进行处理。
- 具体流程
- Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
- 通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
- 调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
image
03.子线程的消息处理
3.1 子线程中定义Handler
- 直接在子线程中创建handler,看看会出现什么情况?
- 运行后可以得出在子线程中定义Handler对象出错,难道Handler对象的定义或者是初始化只能在主线程中?
- 其实不是这样的,错误信息中提示的已经很明显了,在初始化Handler对象之前需要调用Looper.prepare()方法。
- 为何会报错?
- Handler的工作是依赖于Looper的,而Looper(与消息队列)又是属于某一个线程(ThreadLocal是线程内部的数据存储类,通过它可以在指定线程中存储数据,其他线程则无法获取到),其他线程不能访问。
- Handler就是间接跟线程是绑定在一起了。因此要使用Handler必须要保证Handler所创建的线程中有Looper对象并且启动循环。因为子线程中默认是没有Looper的,所以会报错。
- 如何正确运行。在这里问一个问题,在子线程中可以吐司吗?
- 答案是可以的,只不过又条件。需要在子线程中调用Looper.prepare()。
- 在子线程中,如果手动为其创建Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待的状态,而如果退出Looper以后,这个线程就会立刻终止,因此建议不需要的时候终止Looper。(Looper.myLooper().quit()😉
3.2 不建议在子线程访问UI
- 这是因为Android的UI控件不是线程安全的,如果在多线程中并发访问可能会导致UI控件处于不可预期的状态,那么为什么系统不对UI控件的访问加上锁机制呢?缺点有两个:
- ①首先加上锁机制会让UI访问的逻辑变得复杂
- ②锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。
- 所以最简单且高效的方法就是采用单线程模型来处理UI操作。
- 为什么说子线程不能更新UI?
- 子线程是不能直接更新UI的。Android实现View更新有两组方法,分别是invalidate和postInvalidate。
- 前者在UI线程中使用,后者在非UI线程即子线程中使用。换句话说,在子线程调用 invalidate 方法会导致线程不安全。
- 熟悉View工作原理的人都知道,invalidate 方法会通知 view 立即重绘,刷新界面。
- 作一个假设,现在用 invalidate 在子线程中刷新界面,同时UI线程也在用 invalidate 刷新界面,这样会不会导致界面的刷新不能同步?这就是invalidate不能在子线程中使用的原因。
3.3 子线程更新UI方式
- 主要有这些方法
- 第一种:主线程中定义Handler,子线程通过mHandler发送消息,主线程Handler的handleMessage更新UI
- 第二种:用Activity对象的runOnUiThread方法
- 第三种:创建Handler,传入getMainLooper,使用handler.post更新UI
- 第四种:View.post(Runnable r)
- 相似处分析
- 跟进去看源码,发现其实它们的实现原理都还是一样,最终都是通过Handler发送消息来实现的。
3.9 解决handler内存泄露
- 为何会内存泄漏
- 通过内部类的方式创建mHandler对象,此时mHandler会隐式地持有一个外部类对象引用这里就是Activity,当执行post/send方法时,该方法会将你的Handler装入一个Message,并把这条Message推到MessageQueue中,MessageQueue是在一个Looper线程中不断轮询处理消息。
- 那么当这个Activity退出时消息队列中还有未处理的消息或者正在处理消息,而消息队列中的Message持有mHandler实例的引用,mHandler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。
- 解决Handler内存泄露主要2点
- 有延时消息,要在Activity销毁的时候移除Messages。需在onDestroy()函数中调用mHandler.removeCallbacksAndMessages(null)
- 匿名内部类导致的泄露改为匿名静态内部类,并且对上下文或者Activity使用弱引用。注意:每次使用前注意判空。
04.消息机制源码分析
4.1 消息机制流程图
- 整个消息机制的流程图
image
4.1 Handler源码分析
- Handler构造方法源码
- 如果构造函数没有传入
Looper
参数,则会默认使用当前线程关联的Looper
对象,mQueue
需要依赖于从Looper
对象中获取。 - 如果
Looper
对象为 null ,则会直接抛出异常,注意这里是在ActivityThread的main方法中调用Looper.prepare()
。最后可以看到默认是创造同步消息。
- 如果构造函数没有传入
- Handler发送消息源码【send/post】
Handler#sendMessage,sendEmptyMessage,sendEmptyMessageDelayed,post,postAtTime等,各种发送消息方法 Handler#sendMessageAtTime(),发送消息的形式有多种形式,其最终调用的都是
sendMessageAtTime()
方法。 这需要一个已初始化的MessageQueue
类型的全局变量mQueue
,否则程序无法继续走下去。mQueue
变量是在构造函数中进行初始化的,且mQueue
是成员常量,这说明Handler
与MessageQueue
是一一对应的关系,不可更改 Handler#enqueueMessage(),在这个方法中对message附值target是当前handler对象,然后再调用到enqueueMessage分发消息 可以看到在这个方法中默认是同步消息,因为mAsynchronous
默认是false。 需要注意msg.target = this
这句代码,target 对象指向了发送消息的主体,即 Handler 对象本身。 即由 Handler 对象发给 MessageQueue 的消息最后还是要交由 Handler 对象本身来处理。 - Handler处理消息源码【loop轮训中取消息处理】
Handler#dispatchMessage(),分发处理handler消息 如果
msg.callback
不为 null ,则调用 callback 对象的run()
方法,该 callback 实际上就是一个 Runnable 对象,对应的是 Handler 对象的post()
方法 如果msg.callback
为 null,由于mCallback
默认为空,则会调用handleMessage方法处理消息。 Handler#handleMessage(),这里是一个空实现。外部开发自己去实现该方法处理消息 - 使用 postDelayed 后消息队列会发生什么变化?
- post delay的Message并不是先等待一定时间再放入到MessageQueue中,而是直接进入并阻塞当前线程,然后将其delay的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。
- 此时,如果队头的Message正是被delay的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该Message,否则唤醒后直接执行。
4.2 Message源码分析
- Message的作用
- 大概意思是,Message类被用于发送给Handler,其可以存储一些描述信息和任意数据类型的对象。
- 如何处理Message创建和销毁
- 此时我们不禁会抱有一个疑问,在应用运行期间,系统岂不是会不断地创建Message、处理Message、销毁Message?
- Android的消息机制并不会大量重复创建Message对象,而是重复利用在消息池中已存在的Message对象。
- 看一下消息体Message创建流程
Message#obtain(),如果消息链中没有消息时,就会new一个新的Message对象并返回;如果消息链中有可用对象,就会获取到当前链表的mPool,而后将mPool指向下一个Message对象。 Looper#loop()#msg.recycleUnchecked(),这个每一条Message被处理后最终都会调用Message.recycleUnchecked()方法 Message#recycleUnchecked(),这个主要是做消息重制状态的操作。 recycleUnchecked()方法有两个作用:首先,清空消息状态,同时设置flags = FLAG_IN_USE,表明该消息已被使用,这个flags在obtain()方法中会被设置为0; 然后,如果消息池的大小未超过最大容纳数量,就将自身添加到链表的表头。也就是说,Message并不是在创建时而是在回收时被添加到消息池中的。
4.3 MessageQueue源码
- MessageQueue,主要包含2个操作:插入和读取。
- 读取操作会伴随着删除操作,插入和读取对应的方法分别为enqueueMessage和next,其中enqueueMessage的作用是往消息队列中插入一条消息,而next的作用是从消息队列中取出一条消息并将其从消息队列中移除。
- MessageQueue创建源码
Handler#(),在handler构造函数中,拿到Looper对象,然后通过Looper对象去获取mQueue Looper#(),在Looper私有构造函数中,通过new去创建一个mQueue消息队列对象。这说明 Looper 与 MessageQueue 是一一对应的关系
- MessageQueue插入消息【Handler发送消息】
Handler#sendMessageAtTime(),通过handler发送消息,最终会调用queue.enqueueMessage(msg, uptimeMillis),将消息插入到消息队列中。 MessageQueue#enqueueMessage(),handler发送消息最终会执行到这里,传递了一个消息体message,还有一个消息延迟时间 if (msg.target == null),如果msg的target为空(target是指对应的handler),则会直接抛出异常; 如果 mMessages 为空,说明消息队列是空的,或者 mMessages 的触发时间要比新消息晚,则将新消息插入消息队列的头部; 如果 mMessages 不为空,则寻找消息列队中第一条触发时间比新消息晚的非空消息,并将新消息插到该消息前面。按照时间将所有的Message排序; 到此,一个按照处理时间进行排序的消息队列就完成了,后边要做的就是从消息队列中依次取出消息进行处理 因为存在多个线程往同一个 Loop 线程的 MessageQueue 中插入消息的可能,所以
enqueueMessage()
内部需要进行同步。 - MessageQueue取出消息【Looper轮训处理消息】
Looper#loop(),这里可以知道Looper轮训器不断轮训处理消息,这里会调用到mQueue.next()取出即将要处理的message MessageQueue#next(),
next()
方法是一个无限循环的方法,如果消息队列中没有消息,则该方法会一直阻塞,当有新消息来的时候next()
方法会返回这条消息并将其从单链表中删除。 - 如何理解延迟消息
- 这个所谓的延时呢,不是延时发送消息,而是延时去处理消息,看MessageQueue#enqueueMessage()源码,我们在发消息都是马上插入到消息队列当中。
- MessageQueue是按照Message触发时间的先后顺序排列的【也就是说会改变链表的顺序】,队头的消息是将要最早触发的消息。
4.4 Looper源码分析
- 定位到
ActivityThread
类的main()
方法- 看到
main()
函数的方法签名,可以知道该方法就是一个应用的起始点,即当应用启动时,系统就自动在主线程做好了Looper
的初始化操作。
- 看到
- Looper初始化源码分析【ActivityThread类main入口】
Looper#prepareMainLooper(),这个方法主要是初始化当前线程的Looper工作 Looper#prepare(false),
sThreadLocal
对象又是通过prepare(boolean)
来进行赋值的,且该方法只允许调用一次,一个线程只能创建一个 Looper 对象,否则将抛出异常。 sThreadLocal.set(new Looper(quitAllowed)) 当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。 Looper#Looper(false),可以看到在Looper
这个构造方法中,创建了一个MessageQueue
消息队列 Looper 类的构造函数也是私有的,且在构造函数中还初始化了一个线程常量mThread
,这都说明了 Looper 只能关联到一个线程,且关联之后不能改变。mQueue
又是在Looper
类的构造函数中初始化的,且mQueue
是Looper
类的成员常量,这说明 Looper 与 MessageQueue 是一一对应的关系。 - Looper轮训源码分析【loop方法入口】
Looper#loop(),这个方法开启了一些消息循环。不断的判断MessageQueue中的消息是否为空,如果为空则直接return掉,然后执行queue.next()方法。
- Looper死循环为何不阻塞应用卡死
- 这里就涉及到Linux pipe/epoll机制,简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。
- 这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。
- 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
05.消息机制设计思想
5.1 为何要设计消息机制
5.2 消息机制核心思想
- 不管在哪,谈到消息机制,都会有这三个要素:
消息队列
消息循环(分发)
消息处理
消息队列
,是消息对象
的队列,基本规则是FIFO
。消息循环(分发)
,基本是通用的机制,利用死循环
不断的取出消息队列头部的消息,派发执行
消息处理
,这里不得不提到消息
有两种形式:- Enrichment 自身信息完备
- Query-Back 自身信息不完备,需要回查
- 这两者的取舍,主要看系统中
生成消息的开销
和回查信息的开销
两者的博弈。
06.Handler同步屏障
6.1 什么是同步屏障
- 什么场景用到同步屏障
- 在系统源码中就有使用。Android系统中的UI更新相关的消息即为异步消息,需要优先处理。那么handler机制中如何优先消息了,这个就需要了解同步屏障。
- 屏幕刷新机制
- 16ms左右刷新UI,而是60hz的屏幕,即1s刷新60次。屏幕会在每次刷新的时候发出一个 VSYNC 信号,通知CPU进行绘制计算。具体到我们的代码中,可以认为就是执行onMeasure()、onLayout()、onDraw()这些方法。
- 消息机制的同步屏障
- 同步屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。
- 不过异步消息却例外,屏障不会挡住异步消息,因此可以认为,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。
- 一句话概括
- 其实就是阻碍同步消息,只让异步消息通过。
07.消息机制问题和思考
7.1 发送消息Delay可靠吗
- 不靠谱的,引起不靠谱的原因有如下
- 发送的消息太多,Looper负载越高,任务越容易积压,进而导致卡顿
- 消息队列有一些消息处理非常耗时,导致后面的消息延时处理
- 大于Handler Looper的周期时基本可靠(例如主线程>50ms)
- 对于时间精确度要求较高,不要用handler的delay作为即时的依据
- 如何优化保证可靠性
- 消息精简,从数量上处理
- 队列优化,重复消息过滤
- 互斥消息取消
- 复用消息
- 消息空闲IdleHandler
MessageQueue.IdleHandler ideHandler =new MessageQueue.IdleHandler() { @Override public boolean queueIdle() { return false; } }; Looper.myQueue().addIdleHandler(ideHandler);
- 使用独享的Looper(HandlerThread)
HandlerThread handlerThread = new HandlerThread("A-Thread"); handlerThread.start(); Handler handler = new Handler(handlerThread.getLooper());
7.2 Looper停止App就退出吗
- looper如果停止了,那么app会退出吗,先做个实验看一下。代码如下所示
- 可以发现调用这句话,是会让app退出的。会报错崩溃日志是:java.lang.IllegalStateException: Main thread not allowed to quit.
- 第一种:Looper.getMainLooper().quit();
- 第二种:Looper.getMainLooper().quitSafely();
- 崩溃分析:二者最终都调用了MessageQueue中的quit方法,MessageQueue的quit方法源码如下:
- Looper在ActivityThread初始化时,调用prepare()-->new Looper(false)--->new MessageQueue(false)--->mQuitAllowed设置为false。这个时候直接推出执行到崩溃。
- 通过观察
MessageQueue#quit()
源码我们可以发现:- 当我们调用Looper的quit方法时,实际上执行了MessageQueue中的removeAllMessagesLocked方法,该方法的作用是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息(延迟消息是指通过sendMessageDelayed或通过postDelayed等方法发送的需要延迟执行的消息)还是非延迟消息。
- 当我们调用Looper的quitSafely方法时,实际上执行了MessageQueue中的removeAllFutureMessagesLocked方法,通过名字就可以看出,该方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有的非延迟消息派发出去让Handler去处理,quitSafely相比于quit方法安全之处在于清空消息之前会派发所有的非延迟消息。
- 无论是调用了quit方法还是quitSafely方法只会,Looper就不再接收新的消息。即在调用了Looper的quit或quitSafely方法之后,消息循环就终结了,这时候再通过Handler调用sendMessage或post等方法发送消息时均返回false,表示消息没有成功放入消息队列MessageQueue中,因为消息队列已经退出了。