• Android Handler机制彻底梳理


    Android的消息机制其实也就是Handler相关的机制,对于它的使用应该熟之又熟了,而对于它的机制的描述在网上也一大堆【比如15年那会在网上抄了一篇https://www.cnblogs.com/webor2006/p/4837623.html对它的关系描述,但仅仅是背一背概念】,在面试时也时不时的会问起它,说实话从事Android这么多年也没自己从头到尾的去将它的工作机制详细的给挼一遍,所以这里写一篇关于它的整个机制的描述来加深对Handler的核心机制的进一步了解。

    Android消息机制:

    先来看一张关于整个消息机制的描述图,这个流程会在之后自己从0开始手写实现的,如下:

    以上模型大致解释一下:

    1、以Handler的sendMessage方法为例,当发送一个消息后,会将此消息加入消息队列MessageQueue中。
    2、Looper负责去遍历消息队列并且将队列中的消息分发给对应的Handler进行处理。
    3、在Handler的handleMessage方法中处理该消息,这就完成了一个消息的发送和处理过程。
    这里从图中可以看到参与消息处理有四个对象,它们分别是 Handler, Message, MessageQueue,Looper。

    其中在图中涉及到这两个状态:

    这个在之后的源码分析中是能看到的。

    ThreadLocal的工作原理:

    ThreadLocal 是一个线程内部的数据存储类,通过它可以在指定的线程中存储数据,数据存储以后,只有再指定线程中可以获取到存储的数据,对于其他线程来说则无法获取到数据。为啥要先说它呢?因为打好这个基础后有助于分析Android的消息机制,下面先举例来对它的工作原理有一个了解:

    先来瞅一下ThreadLocal的源码:

    接受一个泛型,那该泛型是怎么用的呢?

    它里面有一个ThreadLocalMap的静态内部类,然后在我们往ThreadLocal存东西时最终的这个T会赋值给ThreadLocal中的Entry类中的value,如下:

     

     

    下面来定义一下:

    下面来调用一下:

    此时看一下这个get()方法的实现:

    接下来根据当前线程来获取ThreadLocalMap:

     

    其中可以看一下该threadLocals在Thread初始化的情况:

    所以。。回到get()主流程上来:

    然后:

    最后setInitialValue()方法就会返回cexo,然后整个get()方法就返回了:

    这就是为啥我们的结果显示cexo的原因,好,下面将在子线程中再来打印一下:

    其原因就不多分析了,跟在主线程的是一模一样的流程,都是由于我们重写了initialValue()方法。 

    下面再来看:

    此时再来分析一下set的过程:

    然后再拿时:

    下面再来改一下程序,再改之前需要将ThreadLocal中设置的东东给清除掉,避勉内存泄漏,如下:

    好,再来新建一个线程:

    也就是通过ThreadLocal存放的值是跟线程绑定的,关于它的大致使用就了解到这,下面正式进入到核心的消息机制源码分析阶段了。

    Android消息机制源码分析:

    1、启动App创建全局唯一得Looper对象和全局唯一得MessageQueue消息对象:

    先来看一下整体这块的流程图:

    以此为蓝本,接下来分析一下这块流程的源码【以Android9.0为例】:

    点击进入看一下:

    看到了ThreadLocal的身影了,这就是为啥要先了解它的机制的原因之所在,好,此时流程就到了这:

    然后看一下这个Looper创建的细节,就会创建全局唯一的消息队列,如下:

    以上就是在主线程启动时创建Looper的大致过程。

    2、Activity中创建Handler:

    先贴一下整个这块的流程:

    下面先回顾一下它的实际使用,比较简单,主要是根据实际的应用来过行源码底层分析会比较亲切:

     

    先来看一下Handler()的构造:

     

    3、发送消息:

    整体流程:

    咱们依照此流程来分析一下:

    继续往里跟:

    流程就跑到了这:

    接下来来看一个这个enqueueMessage()方法:

    其中可以看一下Message.target变量:

    也就是每个消息都绑定了Handler,下面回到主流程:

    也就是如流程图的这一步:

    具体看一下在全局消息队列中的处理:

     呃,貌似有点颠覆对队列的认知,不应该拿到消息往队列中插么,貌似这里就是简单的给它里面的成员变量赋了个值而已呢,是的,这个消息队列一定得要知道并非是我们认知中的那种,下面看一眼它的javadoc对它的描述:

    以上是消息发送的大致流程。

    4、处理消息:

    先来看一下这块的大致流程图:

    依据它再来看一下代码流程:

    下面具体来看一下该loop()方法,首先也是获取全局唯一的Looper和MessageQueue对象:

    接着则会循环从队列中取消息,将会调用消息队列绑定的Handler的相关的方法来对消息进行处理,如下:

    那咱们再来看一下Handler中的这个消息分发是如何来处理该消息的:

     

    消息阻塞和延时:

    Looper 的阻塞主要是靠 MessageQueue 来实现的,在next()@MessageQuese 进行阻塞,在 enqueueMessage()@MessageQueue 进行唤醒。主要依赖 native 层的 Looper 依靠 epoll 机制 进行的。
    nativePollOnce(ptr, nextPollTimeoutMillis); 这里调用naive方法操作管道,由nextPollTimeoutMillis决定是否需要阻塞
    nextPollTimeoutMillis为0的时候表示不阻塞,为-1的时候表示一直阻塞直到被唤醒。

    消息阻塞流程:

    下面具体来看一下代码,先看在Looper中的loop()的阻塞相关:

    接着则看MessageQueue的next()方法了:

    那假如在这块阻塞了之后,那在主线程中不会引发ANR么?其实是不会的,原因简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制。

    好,下面再详细的来分析其阻塞的一个整体流程:

    假设loop for循环第一次、MessageQueue for循环也第一次:

     

    接着就会往下执行,则会执行到这:

    好继续:

     

    假如它现在等于0,则会执行到这了:

     此时再下一次循环中,则就会进入阻塞状态了:

    而假如mIdleHandlers.size()>0,那么执行顺序就会发生变化了,如下:

    好,接下来再假设mMessages不为null:

    当退出循环时,如果找到了,则走如下条件分支:

    下面具体再看下里面的条件:

    而这个流程往下:

    这就是消息延时的一个机制。 

    消息延时入队流程:

    此时就需要回到MessageQueue中的enqueueMessage()方法了,如下:

    而如果当前消息的when要大于上一条消息的when,则会走另一个条件分支了,如下:

    先来看一下javadoc的说明:

    而在理解这个分支代码之前需要理解一个东东:

    对于Message对像池的大小是有大小的,那是多少呢?下面看下源码的定义:

     

    那如果消息大小超过了这个对象池总个数呢,则是插不进去的,具体这块的代码如下:

     

    其中sPool是一个静态变量:

    了解了它的对象池之后,下面再回过头来理解这个条件:

    比如上一个消息为:

    然后再插入个新消息为:

    此时进入条件时:

    也就是处理完之后就成这样了:

    同样的如果再来第三个msg:

    同样的也会按从小到大的顺序来进行排序:

     

    最后则会执行唤醒的条件,如下:

     

    当执行唤醒时,则在next()中正在阻塞的就会被唤醒:

    手写Handler消息核心机制:

    经过这么大的篇幅来对Handler核心流程的源码进行了分析之后,接下来弄一个比较有“挑战”的事,从0开始手写一上handler发送及消息接收的“核心流程”,不涉及到延时相关的东东,因为那块太复杂了,下面从ActivityThread.main()中一直到Activity创建消息到接收消息手写实现一下,这里抛开Android环境以单元测试的方式来手写,先定义一个main()方法:

    来模拟它:

    然后我们先将Looper、Handler、MessageQueue、Message都创建一下:

    好,在main()中首先得生成全局唯一的Looper对象,如下:

     

    接下来实现这个方法:

    校仿一下:

    然后在Looper的构造方法中需要初始化MessageQueue,如原码中所示:

    所以继续校仿:

    好,接下来再来创建Handler,如这个流程:

    它里面持有Looper和MessageQueue的引用,如系统源码所示:

    所以咱们在我们的构造方法中来实例化一下:

    然后来在MyLooper中实现myLooper()方法,还是校仿源码:

    然后再来实例化MessageQueue:

    然后我们在主线程中来创建一个Handler:

    我们知道在实际使用时需要重写它里面的一个handleMessage()方法,所以咱们还得在MyHandler中来定义一下该方法,如下:

    此时就可以重写方法了:

    好,接下来在子线程中来发送消息,如这个流程:

    其中消息里面得要有一些属性,这里只定义简单的几个,如下:

    然后咱们继续来创建消息:

    此时咱们再来定义发送消息的方法,先看一下源码是如何写的:

    所以咱们也来写一下:

    我们之前分析过MessageQueue的入队方法,它是采用对像池的方式来存储的,咱们这里简单一点,直接用阻塞队列来存放,重在模拟整个过程,如下:

    好,最后就是开始Looper的消息循环了,如源码所示:

    接下来这就是最后一步的实现了,下面来实现一下:

    好,接下来则来看一下这个next()方法的实现:

    这里就木有实现阻塞队列,还是重点看整体流程,好继续,先看一下系统源码,当拿到消息之后接下来是怎么处理的:

    所以咱们校仿一下:

    接下来就要分发消息的处理了,还是先来参照系统的方式:

     

    所以咱们简单处理一下:

    修改一下:

    好,接下来就到最后的消息处理啦,如下:

    Handler中常见问题分析:

    • 为什么不能在子线程中更新UI,根本原因是什么?

      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。

    • 为什么主线程用Looper死循环不会引发ANR异常?

      简单说就是在主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()中的nativePollOnce()方法里,
      此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,
      通过往pipe管道写端写入数据来唤醒主线程工作。这里采用的epoll机制,是一种IO多路复用机制。

    • 为什么Handler构造方法里面的Looper不是直接new?
      如果在Handler构造方法里面new Looper,怕是无法保证保证Looper唯一,只有用Looper.prepare()才能保证唯一性,具体去看prepare方法。
    • MessageQueue为什么要放在Looper私有构造方法初始化?
      因为一个线程只绑定一个Looper,所以在Looper构造方法里面初始化就可以保证mQueue也是唯一的Thread对应一个Looper 对应一个 mQueue。
      谈到这点,发现咱们手写的代码中关于Looper的构造定义不对,当时是定义成了public了,如下:

      而系统中定义确实是私有的:

      所以修改一下:

    • Handler.post的逻辑在哪个线程执行的,是由Looper所在线程还是Handler所在线程决定的?
      由Looper所在线程决定的。逻辑是在Looper.loop()方法中,从MsgQueue中拿出msg,并且执行其逻辑,这是在Looper中执行的,因此由Looper所在线程决定。
    • MessageQueue.next()会因为发现了延迟消息,而进行阻塞。那么为什么后面加入的非延迟消息没有被阻塞呢?
      这是因为新消息在入列时,会存在唤醒的情况,如下:
    • Handler的dispatchMessage()分发消息的处理流程?

      Msg.callback 在mHandler1.post()中使用
      mCallback在new Handler是通过接口回调

      Post()和sendMessage()都是发送消息,加入消息队列得方式也是一样,区别在于处理消息得方式。通过跟踪源码,容易区分。

    终于。。整个Handler相关的东东都梳理完了,真的,还是细节挺多的,不过这么走了一遍真的受益匪浅!!如果把整个全部消化,我想未来不管面试官怎么来问Android的消息机制都会非常轻松的面对!!!

  • 相关阅读:
    JQuery实现表格的全选和反选,以及分页勾选保存(laypage插件分页的使用)
    js之如何获取css样式
    JS正则表达式一些基本使用、验证、匹配、正则匹配时一个变量
    jQuery绑定事件的四种方式
    console命令详解
    实现a标签中的各种点击(onclick)事件的方法
    用JSON.parse和eval出现的问题
    js 将json对象转成字符串
    iOS主流机型更新
    * -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]’
  • 原文地址:https://www.cnblogs.com/webor2006/p/11630538.html
Copyright © 2020-2023  润新知