本文不涉及底层原理,仅把知识串一下
2020.5.17 更新
- 是什么(线程消息通信机制(共享内存模型和消息传递模型);主要场景是子线程切换到主线程)
- 为什么存在(主线程不能进行耗时操作;子线程不能更新 UI(UI 线程不安全;加锁耗时+逻辑复杂);)
- 核心 API(Message、MessageQueue、Looper、Handler、ThreadLocal)
- 原理
- Looper.loop() 和 MessageQueue.next() 内部都是 for(;;) 无限循环,
- Looper.loop() 通过 MessageQueue.next() 获取 msg,然后调用 msg.target.dispatchMessage(msg) 即交给 Handler 处理消息
- MessageQueue.next() 1. 有消息时返回msg;2.没消息时通过 nativePollOnce 阻塞;3. 队列退出返回 null
- 根据 msg.when 系统相对时间进行 MessageQueue 的插入或排队
- dispatchMessage 处理优先级 msg.callback > mCallback > handleMessage
- 使用方式
- 主线程
- 子线程
- HandlerThread,IntentService
- 第三方库 RxJava、EventBus
- 注意事项
- 切换线程的本质 ThreadLocal
- MessageQueue 是一个链表
- Handler 依赖 Looper 处理消息,而 Looper 和线程绑定
- Handler.post/postAtTime/postDelayed/postAtFrontOfQueue 系列方法通过 getPostMessage(Runnable r){msg.callback = r} 最终调用 Handler.sendMessage/sendMessageDelayed/sendMessageAtTime/sendEmptyMessage/sendEmptyMessageDelayed/sendEmptyMessageAtTime/sendMessageAtFrontOfQueue 系列方法
- 内存泄漏(静态内部类+弱引用;移除消息;)
- 主线程死循环问题(程序运行本质保证不会退出;主线程的消息循环机制;Linux的循环异步pipe/epoll方式不占用资源)
- 主线程卡顿问题(运行在子线程交互;onCreate/onStart/onResume的超时 ANR)
- 其他
- 子线程更新 UI 的方式。1. 实现 Handler;2. runOnUiThread;3.view.post(Runnable)
- 子线程不能更新 UI 的原因,ViewRootImpl 的 checkThread()在 Activity 维护的 View 树的行为
- 子线程 Toast 、showDialog,是在 Window.addView,并非在 ViewRootImpl,所以可以在子线程更新 UI,需要初始化子线程的 Looper.prepare()/.loop
- 切记调用 Looper.myLooper().quit()
- interface IdleHandler { boolean queueIdle()},空闲 Handler,方法返回 false表示只回调一次
Handler 是 Android 消息机制的上层接口。
作用:将一个任务切换到某个指定的线程中去执行
使用场景:通常是在子线程进行耗时I/O等操作,然后回到主线程更新UI;
1 背景
- Android 规定访问UI只能在主线程中进行,ViewRootImpl#checkThread。
- Android 建议不要在主线程进行耗时操作,否则会导致 ANR
鉴于以上两者存在矛盾,Android提供了Handler这个功能。
补充:
- 为什么不允许在子线程访问UI?这是Android的UI控件不是线程安全的,如果在多线程并发访问可能导致UI控件处于不可预期的状态。
- 为什么不对UI控件的访问加上锁机制?缺点有两个:1. 会让UI访问逻辑变得复杂;2. 降低UI访问的效率;
扩展:
- 区分 Eventbus 消息机制的跨线程发送消息,观察者模式
2 关键字
Handler、Looper、MessageQueue、ThreadLocal
3 原理描述
3.1 ThreadLocal
作用:某些数据是以线程为作用域,不同的线程具有不同的数据副本
使用场景:Looper、ActivityThread、AMS
其他实现方式:提供全局哈希表管理
3.2 MessageQueue
名字是队列,其实采用单链表的数据结构,在插入和删除上有优势。
包含两个操作:插入enqueueMessage 和读取next(while读取,读取操作伴随着删除操作)
next方法是一个无限循环的方法,如果消息队列中没有消息,那么next方法会一直阻塞在这里,当有新消息到来时,next方法会返回这条消息并将其从单链表中移除。
3.3 Looper
扮演消息循环的角色,不停的从MessageQueue中查看是否有新消息,如果有就会立刻处理,否则就一直阻塞在那里
子线程使用
new Thread(){
@Override
public void run(){
Looper.prepare(); // 为当前线程创建一个Looper
Handler handler = new Handler();
Looper.loop();//开启消息循环***
}
}.start();
Looper.quit(); 直接退出
Looper.quitSafely();把消息队列中的已有消息处理完毕后才安全退出
Looper 最重要的方法是 loop方法,只有调用了loop后,消息系统才会真正地起作用。
- loop是一个死循环,会调用MessageQueue的next方法获取新消息
- 没消息时,next是一个阻塞操作,也导致loop会阻塞。
- 有消息时,会处理消息,执行 msg.target.dispatchMessage(msg); 其中 msg.target就是发送这条消息的Handler对象,在enqueueMessage添加时赋值。
3.4 Handler
主要工作:消息的发送和接收过程。
发送:通过post或send一系列方法实现(post最终调用send),这个过程只是向消息队列插入了一条消息。
接收:Looper不断循环从队列中取消息,然后调用Handler的dispatchMessage处理消息。共有以下三个互斥方式处理,三个优先级高低依次为1、2、3
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg); // 1、 post发送消息所传递的Runnable参数
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) { // 2、初始化Handler时设置的Callback回调
return;
}
}
handleMessage(msg); // 3、派生子类重写handleMessage方法
}
}
4 主线程 Handler 使用示例
主线程的消息循环
Android的主线程就是ActivityThread,在入口方法main中创建Looper并开启循环
public static void main(String[] args) {
...
Looper.prepareMainLooper(); // 创建Looper 和 MessageQueue
...
Looper.loop(); // 开启循环
...
}
主线程的Handler是内部类 H
ActivityThread和AMS的进程间通信通过ApplicationThread(是Binder,不是线程)进行。
ActivityThread - AMS 通信内存泄漏问题
在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用
解决方案1:静态内部类+弱引用
解决方案2:当外部类结束生命周期时,清空Handler内消息队列
5 参考资料
语雀 Handler面试知识点
《Android开发艺术探索》第10章 Android的消息机制
Android中为什么主线程不会因为Looper.loop()里的死循环卡死?
为什么 Android 要采用 Binder 作为 IPC 机制?
网友评论