分析Handler的流程--《源码系列》

·  阅读 551

Handler的源码分析总结

1 Handler 简介

UI 线程里的操作要更新 UI 时,可以传递消息到主线程,主线程根据需求来更新 ui,可以避免线程操作不安全。

2 主要步骤

  • 异步通信准备
  • 消息发送
  • 消息循环
  • 消息处理

2.1 流程图

Handler工作流程图
Handler工作流程图

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 流程和说明

  1. 在app启动的时候,ActivityThread中main方法:Looper.prepareMainLooper()
  2. prepareMainLooper中的prepare()
    //创建全局唯一的主线程Looper对象
    sThreadLocal.set(new Looper(quitAllowed));
    复制代码
  3. 同时在创建Looper的时候,创建了全局唯一的线程消息队列
      mQueue = new MessageQueue(quitAllowed);
      mThread = Thread.currentThread();
    复制代码
  4. 创建Handler实例时,Handler()
      mLooper = Looper.myLooper();
      mQueue = mLooper.mQueue;
    复制代码

Looper.myLooper()中sThreadLocal.get()取出主线程Looper对象,绑定MessageQueue (消息队列)。

  1. 消息发送:workHandler.sendMessage(msg),通过enqueueMessage方法,调用了队列中的queue.enqueueMessage(msg, uptimeMillis);将消息放入全局的消息队列,并且赋值了MessageQueue中的全局消息。

  2. 消息处理:在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);
            }
        }
    }
    复制代码
  3. 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()方法。

  4. 以上都是我最近学习看源码,看了各种文章,写了个大概的过程,还没有去深入,有不对的地方还望指出。


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 泄露。

解决方案:

  1. 尽可能避免使用Handler做延迟操作。
  2. 使用静态内部类继承Hanlder(静态内部类不会持有外部对象的引用),如果我们需要在Handler中 使用外部的Activity时,可以定义一个Activity弱引用(WeakReference)对象,弱引用在第二次GC回收时,可以被回收。
  3. 在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。

分类:
Android
标签:
收藏成功!
已添加到「」, 点击更改