zoukankan      html  css  js  c++  java
  • 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的消息机制都会非常轻松的面对!!!

  • 相关阅读:
    网页加速的14条优化法则 网站开发与优化
    .NET在后置代码中输入JS提示语句(背景不会变白)
    C语言变量声明内存分配
    SQL Server Hosting Toolkit
    An established connection was aborted by the software in your host machine
    C语言程序设计 2009春季考试时间和地点
    C语言程序设计 函数递归调用示例
    让.Net 程序脱离.net framework框架运行
    C语言程序设计 答疑安排(2009春季 110周) 有变动
    软件测试技术,软件项目管理 实验时间安排 2009春季
  • 原文地址:https://www.cnblogs.com/webor2006/p/11630538.html
Copyright © 2011-2022 走看看