一、消息机制介绍
首先,要了解消息机制中的四个概念;
A Handler:负责消息的发送与消息的处理
B MessageQueue:用来存放消息
C Looper:轮询消息队列,负责取消息
D ThreadLocal:保证每个线程都有自己的消息轮询器Looper
1.1 Handler的用法:因发送消息到消息队列的方式不同而不同
- Handler.sendMessage()—内部类/匿名Handler子类
- Handler.post()——更新的UI操作可直接在重写的run()中定义
以上两种方式本质没有区别,都是向messageQueue中加入了消息,只是回调方式的不同,post runnable 回调是回到run方法中,send 最终会调用到handlerMessage
二、 Handler源码工作流程——生产者消费者设计模式(参考资料)
如何实现线程间跨越?
2.1 使用流程
handler.sendMessage==>handler.handlerMessage()期间做了什么?
- 不管是sendMessage还是sendEmptyMessage或post都会走到handler.sendMessageAtTime==》handler.enqueueMessage
- MessageQueue.enqueueMessage将消息保存在消息队列中(消息队列/消息队列插入)
- 通过Looper.loop中MessageQueue.next()函数取消息队列中的消息
- 再调用handler.dispatch()最后走handler.handlerMessage()
2.2 Message
- Message通过new或者obtain获取,最好通过obtain,可检查是否有可复用的Message,避免过多创建销毁Message对象达到优化内存和性能的目的(不停创建Message会导致崩溃);
- Message实现内存共享,内存不分线程,
- Message如何实现主线程与子线程内存共享:子线程中发送消息,主线程中处理消息
- Message 享元设计模式:实现内存复用
2.3 MessageQueue:单链表实现的优先级队列(根据时间排序)
消息管理中保证线程安全的两个函数:
enqueueMessage 消息入库
synchronized锁是个内置锁,对所有调用同一个MessageQueue对象的线程来说都是互斥的,一个线程是对应着一个唯一的Looper对象,而Looper中又只有一个唯一的MessageQueue,所以主线程中只有一个MessageQueue对象,也就是说,所有的子线程向主线程发送消息的时候,主线程一次都只会处理一个消息,其他的都需要等待,那么这个时候消息队列就不会出现混乱
next 消息出库
在next里面加锁,因为,这样由于synchronized(this)作用范围是所有 this正在访问的代码块都会有保护作用,也就是它可以保证 next函数和 enqueueMessage函数能够实现互斥。这样才能真正的保证多
线程访问的时候messagequeue的有序进行
MessageQueue属于哪个线程?
==》跟随着Looper创建,是一个容器,不属于任何线程
2.4 Looper源码
构造函数——私有构造函数,通过Looper.prepare来初始化
==》ThreadLocal: 线程上下文存储变量(ThreadLocalMap)
==》prepare初始化时有判空处理,故一个线程只有一个Looper,且不可修改
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
loop方法
ActivityThread的main函数中
Looper.prepareMainLooper();
…
Looper.loop();其中有个for死循环
说明所有代码都是在Handler上运行的,通过消息的机制将信息发出
若要死循环停止,从代码来看需要获取一个空的message,什么情况下会出现呢?
==》应用退出
==》
三 、内存泄漏情况 + 解决方案(参考实例)
在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用
在Handler消息队列 还有未处理的消息 / 正在处理消息时,消息队列中的Message持有Handler实例的引用
由于Handler = 非静态内部类 / 匿名内部类(2种使用方式),故又默认持有外部类的引用
垃圾回收器(GC)无法回收MainActivity,从而造成内存泄漏
解决方案分析:
造成内存泄露的原因有2个关键条件:
存在“未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系
Handler的生命周期 > 外部类的生命周期
只要使之以上条件不满足即可(推荐 静态内部类 + 弱引用的方式):
静态内部类 不默认持有外部类的引用,从而使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 的引用关系 不复存在。
==》将Handler的子类设置成 静态内部类
==》使用WeakReference弱引用持有Activity实例
亦可当外部类结束生命周期时,清空Handler内消息队列
不仅使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 不复存在,同时 使得 Handler的生命周期(即 消息存在的时期) 与 外部类的生命周期 同步
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
// 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期
}
四、 HandlerThread(参考实例)
子线程中创建子线程------Looper
主线程发送子线程的消息------不行
子线程中创建自己的looper——HandlerThread
==》线程安全
==》封装获取looper
所以引入HandlerThread------方便使用/保证线程安全
五、 消息机制同步屏障——架构思维(类比车辆让救护车的优先处理)
MessageQueue中的next() 对应sychronized的第一个判断中msg.target == null 条件是指轮询执行同步消息直到找到一个异步消息为止?
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
同步消息--------MessageQueue.postSyncBarrier(),立刻执行
异步消息------一般的都是异步消息
六、应用场景——多线程Handler的应用
1.6.1 IntentService——子线程按顺序执行
==>onStart生命周期发送消息
==》ServiceHandler-----onHandleIntent
==>stopSelf——处理完Service自动停止,实现内存释放,不会导致内存问题
1.6.2 Fragment生命周期管理
attachFragment
commit
destachFragment
1.6.3 Glide生命周期管理
==》Glide.with(context).from().to()
context不能用fragment.getApplicationContext,因为生命周期会一直执行,而用fragment则会更具fragment的生命周期加载,节省内存
==》多次判空问题
七、面试题
1、一个线程有几个Handler
==》n个,可随时创建
2、一个线程有几个Looper,如何保证
==》1个,prepare中有添加判断
3、Handler什么情况下有内存泄漏?而其他内部类没有此现象
==》内部类持有外部类对象,当内部类持有外部类引用为被释放时------static
==》生命周期问题
4、为何主线程可 new Handler ,若想在子线程 new Handler要如何操作?
==》因为ActivityThread中有prepareMainLooper
==》子线程必须有prepare再looper
5、子线程中维护的Looper,消息队列无消息时的处理方案?有什么作用?
==》Looper.quit() :唤醒+
----- Message处理是 生产者消费者设计模式
------ 没有阻塞设置,可以一直存Message,因系统也用,若设上限会导致系统卡机
---- 消息睡眠和唤醒机制 nativeWake()
==》next中msg == null时返回null
==》nativePollOnce message不到时间自动唤醒 + messageQueue为空无限等待
6、可存在多个Handler向MessageQueue中添加数据,但如何保证线程安全?
==》锁 enqueueMessage中有synchronized 内置锁
next()
7、Looper在主线程中死循环为什么没有导致界面的卡死?(参考资料)
- 导致卡死的是在Ui线程中执行耗时操作导致界面出现掉帧,甚至ANR,Looper.loop()这个操作本身不会导致这个情况。
- 有人可能会说,我在点击事件中设置死循环会导致界面卡死,同样都是死循环,不都一样的吗?Looper会在没有消息的时候阻塞当前线程,释放CPU资源,等到有消息到来的时候,再唤醒主线程。
- App进程中是需要死循环的,如果循环结束的话,App进程就结束了。
八、IdleHandler介绍?
Hanlder空闲时处理空闲任务的一种机制。
应用场景:
- MessageQueue没有消息,队列为空的时候。
- MessageQueue属于延迟消息,当前没有消息执行的时候。
不会发生死循环,MessageQueue使用计数的方法保证一次调用MessageQueue#next方法只会使用一次的IdleHandler集合。