「Handler」的源码分析总结
1 Handler 简介
UI 线程里的操作要更新 UI 时,可以传递消息到主线程,主线程根据需求来更新 ui,可以避免线程操作不安全。
2 主要步骤
- 异步通信准备
- 消息发送
- 消息循环
- 消息处理
2.1 流程图
2.2 基本使用
//继承Handler类 & 复写handleMessage()
class workHandler extends Handler {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
// 执行的UI操作
if(msg.what==1){
mText.setText("我是张三")
}
}
}
//在主线程中创建Handler实例
private Handler mhandler = new mHandler();
//创建所需的消息对象
Message msg = Message.obtain(); // 实例化消息对象
msg.what = 1; // 消息标识
msg.obj = "更新的文案"; // 消息内容存放
//在工作线程中可通过Handler的sendMessage() / post()发送消息到消息队列中
workHandler.sendMessage(msg);
//创建Handler类对象也可以通过匿名内部类的方式
private Handler mhandler = new Handler(){
@Override
public void handleMessage(Message msg) {
}
};
复制代码
3 源码分析
3.1 相关的类和方法
3.2 流程和说明
- 在app启动的时候,ActivityThread中main方法:
Looper.prepareMainLooper()
prepareMainLooper
中的prepare()
//创建全局唯一的主线程Looper对象 sThreadLocal.set(new Looper(quitAllowed)); 复制代码
- 同时在创建Looper的时候,创建了全局唯一的线程消息队列
mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); 复制代码
- 创建Handler实例时,Handler()
mLooper = Looper.myLooper(); mQueue = mLooper.mQueue; 复制代码
Looper.myLooper()中sThreadLocal.get()取出主线程Looper对象,绑定MessageQueue (消息队列)。
消息发送:
workHandler.sendMessage(msg)
,通过enqueueMessage方法,调用了队列中的queue.enqueueMessage(msg, uptimeMillis);
将消息放入全局的消息队列,并且赋值了MessageQueue中的全局消息。消息处理:在ActivityThread中main方法中通过
Looper.loop()
开启消息循环public static void loop() { //获取全局唯一的Looper对象 final Looper me = myLooper(); //获取消息队列 final MessageQueue queue = me.mQueue; //省略... for (;;) { Message msg = queue.next(); // might block //省略... try { //这里根据出队的msg,通过dispatchMessage分发给对应的handler msg.target.dispatchMessage(msg); } } } 复制代码
Handler中消费消息(调用接口或者重写方法)
public void dispatchMessage(Message msg) { if (msg.callback != null) {//这里其实是handler.post()时走的方法 handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } 复制代码
如果msg.callback不为空,说明使用了post()方法来发送消息,调用了Runnable的run方法;否则说明使用的是sendMessage()方法来发送消息,里面还有一层判断
mCallback
是否为空,不为空的话,说明创建Handler对象时传入了Callback接口,最后都是调用了复写的handleMessage()
方法。以上都是我最近学习看源码,看了各种文章,写了个大概的过程,还没有去深入,有不对的地方还望指出。
3.3 几种状态说明
「消息阻塞和延迟」:Looper 的阻塞主要是靠 MessageQueue 来实现的,在next()中进行阻塞。next()中nativePollOnce(ptr, nextPollTimeoutMillis)调用naive方法操作管道,由nextPollTimeoutMillis决定是否需要阻塞nextPollTimeoutMillis为0的时候表示不阻塞,为-1的时候表示一直阻塞直到被唤醒,其他时间表示延时。
「唤醒」:enqueueMessage()中进行唤醒:
if (needWake) { nativeWake(mPtr); } 复制代码
「简单理解阻塞和唤醒:阻塞时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制,可以同时监控多个描述符,当某个描述符就绪(读或写就绪),则立刻通知相应程序进行读或写操作,本质同步I/O,即读写是阻塞的。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。」
4 常见问题
「一个线程能否创建多个Handler?」
一个Thread只能有一个Looper,一个MessageQueen,可以有多个Handler,因为 Handler 最终是被 Message 持用的(post 里面的 Runnable 最终也会被包装成一个 Message),以便 Looper 在拿到 Message 后调用 Handler 的 dispatchMessage 完成回调,而且项目中仔细去看也确实如此,我们可以每个 Activity 中都创建一个 Handler 来处理回调到主线程的任务。
「Handler 引起的内存泄露原因?解决方案是什么?」
泄露原因:Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 会泄露。这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。
解决方案:
- 尽可能避免使用Handler做延迟操作。
- 使用静态内部类继承Hanlder(静态内部类不会持有外部对象的引用),如果我们需要在Handler中 使用外部的Activity时,可以定义一个Activity弱引用(WeakReference)对象,弱引用在第二次GC回收时,可以被回收。
- 在onDestory时,清除Handler消息队列中的消息removeCallbacksAndMessages(null)
「为什么不建议在子线程中更新UI?」
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
复制代码
mThread是UI线程,这里会检查当前线程是不是UI线程。「那么为什么onCreate里面没有进行这个检查呢?」。这个问题原因出现在Activity的生命周期中,在onCreate方法中,UI处于创建过程,对用户来说界面还不可视,直到onStart方法后界面可视了,再到onResume方法后界面可以交互。从某种程度来讲,在onCreate方法中不能算是更新UI,只能说是配置UI,或者是设置UI的属性。这个时候不会调用到ViewRootImpl.checkThread(),因为ViewRootImpl没被创建。而在onResume方法后,ViewRootImpl才被创建。这个时候去交互界面才算是更新UI。 setContentView只是建立了View树,并没有进行渲染工作(其实真正的渲染工作是在 onResume之后)。也正是建立了View树,因此我们可以通过findViewById()来获取到View对象,但是由于并没有进行渲染视图的工作,也就是没有执行ViewRootImpl.performTransversal。同样View中也不会执行onMeasure(),如果在onResume()方法里直接获取View.getHeight()/View.getWidth()得到的结果总是0。
「那么真的只有UI线程才能更新界面UI吗?」
当检查当前线程是否是主线程这个方法checkThread()执行后,才不能在子线程更新UI。那么在此之前是否可以在子线程中更新UI呢
有兴趣的可以去看看玩安卓的每日一问,这是其中的一问
「Looper死循环为什么不会导致应用卡死?」
可参考上面的阻塞状态说明
「主线程 Looper 与子线程 Looper 有什么不同?」
主要的区别还在在于 Looper 的 loop 循环是否能够退出,主线程创建时传入的 quitAllowed 是 false。