Handler源码解析

330 阅读27分钟

Handler源码解析:

  • Android源码:

    • Android11--->30的源码
  • Handler作用

    • 依托消息管理机制,进行线程切换;

    • 共享内存池,MessageQueue:避免内存抖动

    1. next函数:将消息传出去
    1. Looper.loop():轮询,取出消息

      • 采用for循环不断进行轮询(死循环),取出消息

      • 此时,ActivityThread中的main就会在死循环(for(;;))中不断执行

        • 因为APP启动后,一直可见(loop中的死循环就保证了代码不会停止)
        • 程序不会停下的好处:类似于后台服务器一直保证活(loop是心跳机制)
        • 但是当没有消息了,loop就会被阻塞(block)
      • 由ActivityThread调用

    2. 调用dispatch函数进行分发:

  • APP中的所有事务处理都是Handler

    • 每一个APP事务都是Message;
    • 服务/Fragment中的每一个生命周期都是一个Message
    • 相当于所有代码都是运行在Message之上了
    • 证明了所有的异常的log信息的起点都是Looper.loop
  • 在处理APP卡顿:定位卡顿位置,判断每个生命周期的执行时间

    • 基础:

      • Handler对外开放了接口
      • 所有主线程中的代码都是Message
    • 思路:BlockCanary

      • 搞一个日志管理系统:通过日志打印时间不同得出每段生命周期的执行时间,就行了
      • 找每个生命周期的执行时间--->找对应的消息的时间
    • 执行:BlockCanary的实现思路

      • 在Looper中有一个msg.target.dispatchMessage(msg)代码
      • 在前后分别整一个开始时间和结束时间--->对logging赋值
      • Looper中有一个叫setMessageLogging的接口,设置一个类,就是接口的参数,就可以打印各个生命周期执行的时间
      • 最后再在msg.target.dispatchMessage(msg)这行代码后面的还有一个logging对应就行了
  • 从handler.sendMessage到Handler处理发生了什么?

    • 概述:

      • sendMessage:向消息队列中添加消息,生产者
      • next():从消息队列中取出消息,消费者
      • 消息队列本身可以看作仓库,整体为生产者消费者模型
    • 生产者--消费者是一个带阻塞的系统

      • 当仓库满了--->入队列被阻塞

      • 当仓库空了--->出队列被阻塞

      • 仓库问题:本身没有设置上限,但是当系统没有内存,进程没有内存时就是满了

        • 为什么不设置上限:达到上限APP就挂掉了

          • 因为Message本身就是一个心跳机制并且对于60HZ的手机,一秒刷新60次,平均16.7ms刷新一次UI,一次刷新带来3条Message;直接干没了。直接就挂掉了
      • 仓库什么时候醒来?入队列就醒了

        • 会调用Native方法
    • 什么是阻塞,为什么仓库空了要阻塞,为什么这个阻塞不会导致ANR?

      • 什么是阻塞:非得等,并且不能干其他的,阻塞就是睡觉,将CPU的资源放出去;这个时候线程都挂起来了

      • 这个时候可以睡觉,当可以了,就会通知你

      • 因为安卓中的每一个都可以看做消息,当这个仓库都会空了,那么就让它睡觉,把CPU的资源交出去;

      • 这个时候为什么不会ANR,明明线程都挂起来了,而且挂的时间还比较久

        • Handler的阻塞与Message的阻塞两码事并且ANR也算一个消息

        • ANR可以看作一个定时任务没有在定时范围内完成,就像埋了一个定时炸弹;

          没有完成就爆炸,并且弹个ANR

    • Android阻塞是如何实现的:

      • 消息队列入队列是如何排序的:for循环轮询,按照执行时间排序,时间早在前面

      • 但是当我拿消息(加了锁的Synchronized)的时候,还没有等到它的执行时间,那么就阻塞一秒钟;但这个是不可能的;因为至少系统会不断刷新UI

      • 具体实现

        1. 取出对头消息,拿到时间,计算出需要等的时间

        2. 退出,将这个时间通过nativePollOnce传到native代码中:

        3. java去调用底层代码--->Linux 层;java虚拟机是由C++实现的,

          java调用底层代码(JNI 层),此时将这个java代码打上native标记

      • 什么是静态调用:

        • java层中调用的跟Native层中调用的是一样的
  • Handler中的阻塞:最终是epoll_wait()

    • 调用流程:java--->JNI--->CPP--->Linux中的epoll_wait(),此时的阻塞是在底层发起的

      1. java层:当消息需要等待的时候调用nativePollOnce(this,等待时间)

      2. 调用与之同名的JNI层的函数:android_os_MessageQueue_nativePollOnce

      3. 然后转交到NativeMessageQueue::pollOnce

        • 调用C++层中对应的mLooer->pollOnce(等待时间),让Looper等待,因为都干掉Looper了,因为一个线程就只有一个Looper,此时线程的概念就被弱化了
      4. 再到pollInner:epoll层了

        • 最后到这个epoll_wait里面来(在这里线程才被阻塞,上层调用者也因此被阻塞)
  • 非阻塞忙轮询

    • 一分钟干一个电话,问到没有
  • 场景:设计到I/O:(当有多个网络请求,从数据库中拿数据)

    • 阻塞:读数据的时候用的

      • 因为在读取数据的时候,由于读取的速度不同(缓存,阻塞)

      • 一个线程中如何处理多个I/O?最节约时间的

        • 一个线程里面用while循环,遍历所有的流;有就拿,拿完所有的流
        • 当所有的流都没有数据了,这个时候还是在for循环里面但是没有读取数据的动作,CPU就空转--->CPU资源浪费
        • 所以当流都读完了,就阻塞,避免CPU一直在for循环中挣扎
    • 引入select:处理的是一系列的流,是一把

      • 还有一种解决办法:在for循环之前使用select选择流(Linux 进行实现的),当都没有需要处理的流了,CPU在select里面阻塞;
      • select只知道这个一系列流中有I/O需要处理但是它不知道要处理谁,要处理多少个;这个是硬件层面的了
      • 一般情况下都是,先使用select判断一下;这一系列中有需要处理的,那么就搞个循环去处理所有的流;select就是先对一大把过滤一下,最后细化
    • 细节

      • 即使采用多个线程处理多个I/O :由于CPU底层决定了,每一次I/O,都会涉及到硬盘的读取,每一次读取都会降低效率;
    • 引入epoll:epoll是Linux内核中的一种可扩展IO事件处理机制。大量应用程序请求时能够获得较好的性能。

  • 引入epoll

    • 工作原理:

      • 使用epoll_wait()处理所有的流,当这个流有I/O需要处理的话,就把这个流加到activie_stream[]去--->后面只需要使用一个for循环去遍历activie_stream[]就行了
    • 好处:

      • CPU底层是需要对所有的流都进行一次遍历O(n),但是引入epoll机制后,将时间复杂度降到O(1)了
    • 重要函数:

      • int epoll_create(int size); 创建一个epoll的句柄, size用来告诉内核需要监听的数目一共有多大

        在创建MessageQueue的时候,会调用nativeInit,在这个里面完成对epoll进行初始化

      • int epoll_ctl(int epfd, int op, int fd, structepoll_event *event); epoll的事件注册函数,因为MessageQueue本身就可以看做是一个事件

        • 整个消息队列就是一个事件;
        • 在初始化epoll的时候就直接初始化了这个事件
      • int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)

        • 这个超时时间是上层传过来的
        • 一般不会传-1
      • 线程与epoll之间是没有关系的,是在MessageQueue搞了一个出来就会带上一个epoll

    • Nginx:基于epoll机制

      • 数据在服务器上,通过软件进行数据分发,就是基于epoll机制的;
    • 为什么在创建消息队列(int epoll_create(int size);)的时候会调用nativeInit():

      • 必然会对epoll进行初始化
    • 有了epoll之后,在消息队列的一个构造函数中(MessageQueue(boolean quitAllowed))

      • 在调用nativeInit的时候会传入一个epoll的句柄(mPtr):实际上就是一个整数,也是epoll的标签

         mPtr = nativeInit();
        
      • 在调用nativePollOnce(ptr,nextPollTimeoutMillis);

        • 就是向ptr句柄指向的epoll中,等了那么久的时间:nextPollTimeoutMillis;
      • nativeWake(ptr):向消息队列中放入一个消息时会调用此函数

        • ptr指向具体的epoll对应的事件
        • ptr不是地址值,内部是有封装的,只是说代表函数
        • 具体的唤醒是在Linux层中的字节码
        • epoll是在IO中管理阻塞的机制

同步屏障:保障UI刷新(此事件优先级最高)

  • 概述:一般是按照消息队列的顺序进行执行的,就是交警,先封路再解封

  • 场景:120HZ 的屏幕,3ms发送Message更新UI,但是此时的消息队列是按照顺序执行的,怎么去保障UI正常刷新;

  • postSyncBarrier:就是屏障,就是Message,target为null的就是屏障消息

    • 创建了一个msg,然后直接干到队列中;

    • 一把的消息,在创建消息后,会对target属性进行赋值的

    • 一般的消息(要执行的消息):同步消息(自己调用的),异步消息(需要优先执行的,比如UI刷新(在ViewRootIml中)),(Message共有三种,同步,异步,屏障)

    • UI刷新:ViewRootIml

      • 在scheduleTraversals()中

        1. 先发送一个屏障消息:mTraversalsBarrier = xxx.postSyncBarrier;
        1. 调用postCallBack:调用到一个Internel,这个都调用到了choreographer:UI刷新的源头,60H在,120HZ都是在这里决定的

          • postCallBackDelayedInternal中的else

            1. 发送了一个Message:UI更新的Message

            2. 设置异步属性:msg.setAsynchronous(true)

              • 所以这个消息是异步消息,一般的消息是不会设置这个属性的
        2. 就有个异步消息发送

  • 取出消息:从队头去取出来,卡顿优化;

    • 场景:当队头消息为onResume函数,已知其耗时200ms,

    • 效果:画面卡顿,在log中提示:fragme丢失

      • Skipped 30 frames! The application may be doing too much work on its main thread. 因为在执行这个消息的时候,有30次UI刷新没有正常进行;
      • 怎么去解决这个问题:用Loop里面的log去追,看看是那个耗时了,抓到了就解决它;
      • 抓到他了,实在不能优化,就放到异步线程里面去
      • 产生的原因:因为队列前的消息太耗时了,即使存在同步屏障机制,但是还是需要等到这个耗时消息执行完后,当准备去执行异步消息的时候,队列中都卡了30个异步消息了 因为消息队列为生产者-消费者模式,消费和生产是可以同时进行的,完全存在异步消息堆积的可能
    • 细节:不会发生ANR,一般情况,应用无响应都是5秒

  • 那么就需要保证优先级高的消息进行执行:

    • 例如:scheduleTraversals,先设置屏障,调用postCallBack,在里面为此消息设置异步属性;流程就是这样

    • 具体流程:

      1. 设置屏障:mTraversalsBarrier = xxx.postSyncBarrier;

        • 发送了一个屏障消息
      2. 发送需要更新的事件:异步消息

    • 细节一:设置屏障的时候mTraversalsBarrier = xxx.postSyncBarrier;

      • 发送了一个屏障消息
    • 为什么屏障要在异步消息之前:因为屏障消息需要先执行

    • 同时,在取消息(执行next):会有一个if条件判断,当msg.target==null(说明其为屏障消息)

      • 此时执行do-while循环:轮询队列

        1. 找到异步消息:msg.isAsynchronous()&&msg!=null
        1. 执行异步消息:返回异步消息
        2. 返回Loop里面去处理:Message msg = queue.next()
    • 当处理完整个异步消息之后,有一个unscheduleTraversals函数:

      • removeSyncBarrier:移除屏障
      • 这个removeCallBack里面:remove这个刷新的消息
  • 不移除屏障消息会发生什么

    • 会一直在哪个for循环里面:MessageQueue里面有个for(;;)

      • 因为它发现每次队头消息都是屏障消息:每次都会执行相同的代码
  • 屏障消息只能插队头吗?

    • 所有的消息都是有时间顺序的:

    • 比如插入屏障消息:postSyncBarrier(long when){}when就是时间,当前时刻

      • 内部代码:屏障消息也是基于时间排序的

         if(when != 0){
             xxx
         }
        
    • 屏障消息本质上也是一个消息,也是按照时间顺序进行入队和出队,只是说,它的target属性为null而已

  • 为什么每次UI更新至少要刷三个消息

    • 三个消息:屏障消息,UI刷新(异步消息),解决屏障消息(一般消息)
  • scheduleTraversals:这个是开放的接口

    • 执行doTraversals:

      • 执行performTraversals:

        • 里面就有三个重要的函数:

          • performDraw
          • perforLayout
          • performMeasure:测量的起点,从根布局进行测量

    UI 是一颗树形的,有嵌套关系,UI的刷新,实质上就是刷新根布局

Handler源码解析(一)

  • 源码的问题:理解源码为什么去实现,为什么去用,会造成什么效果

  • Handler存在的意义:

    • 系统的核心
    • 跟web中的AJAX有异曲同工之妙,Spring架构中有
    • 大大降低了并发的问题出现的可能性,几乎看不到多线程死锁的问题;
  • 数据通信带来什么问题

    • 线程如何通讯:

      • 方法:LiveData,EventBus,采用Handler通信(共享内存)
    • 为什么线程之间不会干扰:Handler使用锁机制,内存管理设计优秀

    • 为什么wait/notify用武之地不大:

      • java是基于虚拟机的,虚拟机是用C/C++进行实现的

      • handler已经将这部分功能进行了Linux层的封装

        • Linux的epoll机制
  • 为什么Handler通信是共享内存

  • 课程内容:

    • 源码
    • 设计思路
    • 设计模式
    • 异步消息,同步消息,消息屏障
    • HandlerThread
    • IntentServer
  • Choreographer:编舞者

    • 管理事件分发,处理屏幕点击,将点击事件封装成一个消息
  • Handler:实现线程(子线--->主线;主线--->子线)间通信;

    • 是消息管理机制,是系统的维护者:ActivityThread(AMS中的一个部分,就是围绕Handler玩的)
    • 子线程(携带javaBean)--->主线程(显示)
    • 一般情况下都是子线程发送消息,主线程接收消息
  • 应用的启动:

    1. lancher(桌面程序,是一个app):启动app的,

      • 点击应用图标就相当于点击了lancher的一个按键
      • 进行响应,zygot,fork出一个进程给应用(一对一的),
    2. zygot:

      • fork出一个进程给应用(一对一的),为每一个分配独立的JVM

      • JVM中的java程序,那么java程序就有main函数,JVM就启动这个main函数,程序就开始运行,这个main函数就在ActivityThread.java里面

      • zygot为什么要给每一个程序分配一个虚拟机

        • 主要就是隔离
        • 独立的,挂掉了 ,不会影响其他的应用
        • 在功能机中出现FATAL ERROR,就是一个程序挂掉,那么手机就挂掉了,智能机就不会
      • main函数中干了什么:

        1. 环境(JVM)初始化
         Environment.initForCurrentUser();
        
        1. 打印log

        2. 启动主线程的Loop:主线程就运行起来了(准备了一个主线程的Loop,Looper,prepareMainLooper();,执行这个Loop:Looper.loop())

          • Looper.loop():意味着APP所有的代码都是Handler管理的,APP的所有代码都是在Handler中进行的

            • 有一个死循环:代码不能跳出循环,那么Handler只需要不停的分发消息进行

            • 就是一个永动机,不断在消息队列中进行

            • 除非Loop断开:返回一个为空的msg

            • 什么时候返回一个为空的msg:

              • 应用退出:调用quit,就是MessageQuit
          • Handler:

            • 所有的代码都在Handler之中运行

            • 线程通信只是它的一个附属功能而已

            • 运行起来就是一个传送带,

  • Handler--->sendMeassage--->message.enqueMessage()--->handler.handMessage()处理消息

    • 源码不能打断点,除非自己编译一个模拟器

    • 有一张图:

    image-20220215143121886

    • enqueMessage:

      • 功能:将消息放到消息队列中去,看源码的时候就可以看到明显的队列痕迹(优先队列,单链表构成的,不会冲突)
    • Next:从消息队列中取消息,返回一个Message

      • 由Loop调用,Loop来取:Looper.loop

        • 有一个死循环,去调用next()函数
        • 在调用之前,传送带就在滚动了
        • 再调用dispatch函数
    • Handler的调度流程:

      • handler--->sendMessage--->messageQueue.enqueMessage(插入消息队列)--->looper.loop()--->messageQueue.next()从消息队列中取出消息--->handler.dispatchMessage()--->handler.handleMessage()

      • loop:通上电,就一直跑;在for循环中一直跑

      • message就在滚动:

        • 因为message代表一个内存,就相当于是内存共享了
        • message:new,obtain
        • 因为内存又是不分线程区别的,所以可以共用
      • message是怎么从子线程到主线程的

        • 假设在子线程中进行一个handler.sendEmptyMessage(),在主线程调用handleMessage()进行消息处理;这个就实现了从子线程到主线程的切换
    • 消息队列是一个什么样的数据结构

      • 采用单链表实现的优先级队列

      • 具体实现:为什么是单链表

        1. 在MessageQueue持有一个Message对象mMessage
        2. 在Message对象中有一个Message next,这个next也是一个Message;
        3. 依托于这种嵌套机制,就实现了一个链表,并且还是单链表
      • 具体实现:为什么具有优先级?

        • sendXxxMessageAtTime()

        • 在MessageQueue.java中有一个enqueMessage()函数

        • 插入排序

          image-20220215145638569

      • 消息的when属性是怎么来的?在Handler.java中的sendXXXMessage()里面的

        image-20220215150020326

      • 它都是一个单链表了,那么为什么是队列?

        • MessageQueue.java中的next()函数

          image-20220215150727985

      • 在MessageQueue构造的时候使用了插入排序进行操作的

Loop源码:

  • 核心:构造函数,loop(),ThreadLocal

  • Loop是怎么初始化的?

    • Looper.java中有一个私有的构造函数

           private Looper(boolean quitAllowed) {
               mQueue = new MessageQueue(quitAllowed);
               mThread = Thread.currentThread();
           }
      
    • 既然是私有的构造函数,那么怎么初始化?在Looper.java中有一个prepare函数

      image-20220215151344823

    • 为什么要在prepare里面这样去整个私有的构造去耍?因为在ActivityThread.java里的main函数中:prepareMainLooper

      image-20220215151851601

      • 首先:当这个Looper的构造函数是public,那么谁都可以new,就不好管理

      • 进行判断:

        image-20220215152106167

  • ThreadLocal:多线程中进行线程上下文的存储变量;

    • 源码:这个源码要去补充

    • 就是线程隔离的工具类,本身是不存数据的,在调用set函数,根据所需要存储的数据获取当前的线程,每一个线程里面都有一个ThreadLocalMap(每一个线程的一个成员变量,用于存储相应线程的上下文),这个Map是一个弱应用的东西,key就是ThreadLocal,Value就是所需要保存的数据

    • ThreadLocal.java中的set

      image-20220215152901520

    • Looper.java

      image-20220215153137521

    • 保证了一个线程就只有一个Looper,并且当Looper存在后就不能改了

      • 因为一个线程对应一个ThreadLocalMap,
      • 中间有个ThreadLocal做了限制的
      • ThreadLocalMap<唯一的ThreadLocal,value>,因为this唯一,退出value唯一;因为键值对是一一对应的
    • 怎么去保证ThreadLocal,在value活着的时候,只跟value在一起?

      • 在HashMap的源码中,对一个key不断set,会导致value覆盖的情况;只需要只让他set一次就行了 ;那么就在那个prepare()中做手脚
      • looper.java中的prepare函数: image-20220215153834012

MessageQueue在主线程还是子线程?

  • Handler.java 中的Handler构造函数

    image-20220215154315035

  • Looper由ThreadLocal中来:Looper会维持一个MessageQueue;

    image-20220215154414593

  • MessageQueue伴随Looper创建,两者在一个线程中均是唯一

    image-20220215154913338

  • 多个线程,会有多个Message吗?

    • 不好说,因为一个线程只会创建一个Looper,一个Loop只会创建一个MessageQueue
    • 那么在主线程中,就只能是线程,Looper,MessageQueue一一对应;
    • 关键就是在于看有没有在多个线程中创建多个Looper
  • 为什么要有ThreadLocal?

    • 保证一个线程里面只有一个Looper
  • 面试题:

    • 一个线程中有几个Handler

      • 无数个,Handler机制就只有一个
      • 因为Handler是可以new出来的,想整几个整几个
      • 怎么去new?
    • 一个线程有几个 Looper?怎么保证?

      • 就只有一个,用了一个ThreadLocal来保证

      • ThreadLocal中的prepare中检验

        • 如何说不为空,就证明已经有了,抛出异常
    • Handler内存泄漏的原因?为什么其他内部类就没有?

      • 内部类持有外部类的对象:java编程思想中的有(2-7章)

      • recycleView中的adapter,ViewHolder这些都是内部类为什么没有内存泄漏?

        • 因为生命周期决定了
      • 源码分析:handler中的sendMessageAtTime中调用enqueueMessage,在enqueueMessage中

        image-20220215161213896

      • 所以在Message的传递过程中,可以set一个Message在20分钟后执行,那么这个message会等待20分钟,并且在这20分钟中,这个message一直持有这段内存20分钟;并且message持有了Handler,Handler又持有了Activity,并且在这是GC是不能被回收的,因为message不可达,那么他持有的所有东西都不能被回收;因为这种关系,在内部类持有外部类中,这个内部类在外部类的生命周期的范围被别的对象持有了,外部类也不能释放;这个就是内存泄漏的真正原因

    • 为何主线程可以new Handler,在子线程中怎么new Handler?

      • 主线程本来就可以new,在主线中的ActivityThread()在main函数中就弄了下面的两句
      • 子线程中必须干两句:先Looper.prepare()再Looper.loop()不然运行不了,就算new出来的 Handler没有什么调用;
    • 子线程中维护的Looper,消息队列没有消息的时候处理方案是什么?有什么用?

      • quit函数:Looper.java中的

      • Loop函数是一个死循环,退出的条件是msg==null,但是msg又不能等于null,只有在Looper.java中的quit中,调用了MessageQueue.java中的quit函数,此时对mQuitting赋值为true,将消息队列中的所有消息全部清空,之后,进行nativeWake(mPtr)唤醒

      • MessageQueue的消息睡眠与唤醒

        • 生产者-消费者模型
        • 在子线程中enqueueMessage向消息队列中添加消息
        • 在主线程中调用next从消息队列中取出消息;
        • 所有的消息都在MessageQueue,仓库(这个是可以挂掉的,比如说作一个秒表,后台进行notification,造成整个消息队列中全是Message,Message并不适合定时功能,用System.clock就行了)
        • 当队列满或者空,就会阻塞;
      • MessageQueue的阻塞机制

        • 没有限制的,随便整就行了,最多就阻塞;Handler不做受限,内存耗完就G了

        • 在java多线程中有个阻塞队列,多了,后面的就阻塞

        • 为什么Handler不设置上限?

          • Handler是大家都在用的,系统还是在用这个,系统消息进不来就G了
        • 两个方面阻塞:

          • 取出的消息还没有当它的执行时刻阻塞,自动唤醒,时间够了就将这个消息return出去

            image-20220215163503066

          • 因为没有调用quit函数,还是在这个for(;;)里面,调用nativePollOnce

            image-20220215163650360

          • nativePollOnce:注意第2个参数(-1,无限等待,直到有消息来将其唤醒,0,不等待,值就是等待的时间)

            image-20220215163730152

          • 第二层等待:消息队列为空的时候,还是会调用nativePollOnce,无限等待的状态;只有向消息队列中添加一个消息,进行唤醒就是调用nativeWake(ptr)

            • 因为ptr的默认值是-1,相当于待机

              image-20220215164158469

          • Handler没有做队列满的操作,内存耗尽就满了,就G了
        • nativeWake怎么玩的?

          • java的wake--->JNI 同名函数--->native的wake(mLooper->wake())--->C++的wake--->Linux中的wake
          • 这个是NDK的了
        • 阻塞是否提升了性能?

          • 对,空出了CPU;
      • 当消息队列没有消息,是怎么处理的?

        • 主线程:Looper.java中的loop函数

          image-20220215165952602

        • 子线程中:

          1. 调用quit函数,Looper.quit--->MessageQueue.quit

          2. 对那个东西进行赋值,调用nativeWake,唤醒后,往下执行

            image-20220215170354663

          3. 直接返回null:此时的msg就是null:MessageQueue里的next

            image-20220215170610601

          4. 然后再次检验就退出了Looper.loop

            image-20220215170913440

    • 既然可以存在多个Handler向MessageQueue中添加数据(发消息时各个Handler可能处于不同线程),那么如何保证线程安全?

      • 依托锁机制,在MessageQueue中的enqueueMessage基本被synchronized包裹了

      • 这个东西叫内置锁:依托JVM实现上锁,解锁;可以锁代码块

      • 为什么里面用this,锁的就是所有的东西,锁了对象,对象里面所有的函数、代码块等全部都会被受限;那么当我在执行synchronized(this)代码块中的内容时,同一对象其他地方上this锁的地方都不能走,全部都要等;但是,锁只是锁的对象;对象都不同了,随便整;但是对象相同了,就不行,就像调用了enqueueMessage就不能调用next

      • 一个线程只有一个可以操作MessageQueue的地方

        • 因为一个线程只有一个MessageQueue对象,对象里面又有锁
      • 当多个Handler处于不同线程同时向消息队列中放消息

        • 排序,谁早,先插谁,因为上锁了的
        • 为什么取消息的时候要加锁,因为你取的时候也有人在插入
        • 退出的时候也要加锁,退出了就不要插,取
      • 主线程不能调用quit,这个调用就崩溃了

Handler面试解析

  • Handler工作流程

    • 示意图:

      image-20220215185844839

    • 具体流程:

      • 整体为一条传送带,调用postXxx函数或者sendXXX函数将消息放到传送带(MessageQueue优先级队列),Looper.loop函数为其提供动力(其中for(;;)中不断轮询消息队列,调用next函数取出消息);拿出队头消息(执行时间最早),当消息满足执行条件(当前时间大于消息的执行时间)调用dispatch进行消息分发;一般来说是从子线程将消息放入队列,主线程接收消息队列中的消息,实现了线程之间通信;每一条Message对应了一段内存区域,整体实现了共享内存
    • 疑问:线程之间的跨越是如何实现的?

      • 内存是没有线程概念的:

        • 比如在Activity中可以new一个HashMap,此时开启线程,在线程中也可以使用这个HashMap
    • 为什么发送的是子线程,接收消息是主线程?

      • 子线程中执行的函数,那么这个函数就属于子线程
      • 一般是在异步线程中发送消息:handler.sendXXXMessage(msg)--->MessageQueue.enqueueMessage(msg)
      • 只是说将子线程中需要执行的事件,转成了一段内存,
      • 一个线程有一个独立的ThreadLocal,一个ThreadLocal有唯一对应的Looper,对应MessageQueue(唯一)
      • 主线程中的Looper.loop()一直在执行,轮询主线程中的MessageQueue,那么loop函数在主线程中调度,那么其中的for(;;)就在主线程中调度,for(;;)其中的,msg.target.dispatchMessage(msg)就在主线程中了;
  • ThreadLocal的唯一性:

    • static final修饰了ThreadLocal,这个就是唯一的,

      • 意味着所有的线程都是只有这一个,全局唯一的
    • Looper:线程单例;

    • 全局只有一个Looper?:线程唯一的

    • ThreadLocal的全局唯一性怎么保障Looper的线程单例

      • 每一个线程都有各自的ThreadLocalMap
      • 并且ThreadLocalMap的KEY都是唯一的:ThreadLocal
      • 那么他的Value就肯定只有一个了
  • handler细节:

    • 享元模式:

      • 当消息成功执行后:调用 msg.target.dispatchMessage(msg);

      • 会调用msg.recycleUnchecked();将这个消息存起来;

        1. 将Message所有的内容全部置空

        2. 将这个置空的消息放到sPool(也是一个消息)里面去

        3. 构成了一个新的回路,相当于将其查到队头去

          • 避免了抖动的问题:防止OOM
          • 因为new 的时候会造成内存碎片,因为new的时候是开辟的是连续的内存空间
          • 内存碎片就会导致OOM
          • Full GC会回收内存,但是不能保证一定回收了;
          • BitMap也是这样玩的,不用了,就把数据擦掉;后面就直接用
      • 设计:

        • Activity管理,intent,BitMap等都是这种享元设计模式;达到内存复用的目的
        • 还有在RecycleView里面就有;
    • Looper的死循环为什么不会导致ANR

      • ANR:点击事件5S没有响应(没有处理完),一般10S广播没有响应,就出现ANR,Service20S

      • 点击事件最终还是一个Message,封装在Choreographer中的doFrame里面就会将点击事件等封装成消息,调用CallBack()

        • 发送消息之后,就记录一个时间

          image-20220215194105049

        • 消息执行后,执行doCallBacks():这里检测到执行时间大于5S(对于点击事件哈),那么让Handler发送一个ANR的提醒

        • Handler发送ANR跟Looper里面的block没有关系

        • Handler会什么会弹出弹窗,因为这个Handler可以自己唤醒自己,并且优先级高

    • 为什么Looper.block不会导致ANR?

      • Looper.block只是交出CPU而已,此时线程没有事情干,根本就没有消息
  • HandlerThread存在的意义:在子线程中创建一个属于自己的Looper

    • 优点:

      • 方便使用:方便初始化,方便获取线程looper
      • 保证了线程安全
    • 如果自己去封装:不行,在多线程背景下,可能子线程中的东西都没有去执行,异步问题

       Looper looper;
       public void run(){
           开一个子线程
       looper.prepare()
       //初始化handler
           threadHandler = new Handler(loop)
       looper.loop()
       }
       thread.start()
       Handler handler = new Handler(thread.getLooper())
      
    • 多线程的锁机制:Handler解决了锁机制问题

    • 不用HandlerThread东西:

       开一个子线程
       looper.prepare()
       //初始化handler
           threadHandler = new Handler(loop)
       looper.loop()
      

      千万不能将这个Looper搞成全局的,引起内存的问题

    • 在主线程中是不能创建handler的,不行的

    • HandlerThread怎么玩的?

      • 继承了Thread,本身就是一个子线程,在哪里都是可以用的

      • 对Looper进行了封装

        image-20220215195548780

      • getLooper执行细节:notifyAll与wait处理的是同一个锁

        image-20220215201027725

  • IntentService:

    • Service:

      • 处理耗时的后台应用,在其中启动一个线程,去处理耗时任务;
      • 生命周期由后台处理,那么IntentService生命周期后台调度
    • IntentService.onCreate

      • 示意图:

        image-20220215201618473

    • IntentService.onHandleIntent

      • 示意图:

        image-20220215201704176

    • IntentService.onStart()

      • 示意图:

        image-20220215201858287

    • IntentService.SerViceHandler

      • 示意图:

        image-20220215202120478

      • 所以只需要在new出IntentService的子类,实现onHandleIntent,将耗时工作放到这个里面就行了

  • Handler源码有什么用:Handler处理完异步耗时操作之后,自己就把自己干掉了

    • 从handleMessage开始:这个里面是在子线程执行消息,子线程中维护了一个消息队列

      image-20220215210354113

    • 消息处理完后执行:stopSelf(msg.arg1);是Service的stopSelf

      处理完Service自动停止,不用担心内存还要泄漏,自己就干掉自己了

      image-20220215211054453

    • 为什么只有一个Message,发了很多个消息?

      • 场景:一项任务分成几个子任务,子任务全部执行完后,这个任务才成功;这样子就可以用IntentSetvice来解决,最佳方案;

      • 解决:IntentService

        用多个线程来处理,一个线程处理完--->下一个线程--->下一个线程

      • IntentService优势:维持一个线程独有的队列,可以保证每一个任务都是在同一个线程中去处理,并且都是依次执行。

      • 怎么保证这些都是在同一个线程中?

        • 因为获取到的都是同一个线程对应的Looper,

          • 因为在IntentService里面的onCreate中创建的是HandlerThread

          image-20220215214749896

          • HandlerThread里面的Looper就是这个线程独有的,

            image-20220215214847371

          • 一个线程只有一个Looper,这样就保证了就是一个线程

            一个线程对应一个ThreadLocalMap---><唯一的ThreadLocal,value> looper对应唯一的MQ

      • 怎样保证它是线程中的任务时顺次执行的

        • 任务就是消息,放到消息队列中的,这个是有先后顺序的,一定是执行前面的,再来后面的,保证线程任务的依次执行
      • 多个任务用同一个IntentService就行了

      • IntentService怎么用的?

      • Service还要在清单文件中注册;

  • 原理的其他应用场景:

    • Fragment的生命周期管理:FragmentTabPagerAdapter.java

      image-20220215220326798

    • Glide的生命周期中的应用:在Handler源码面试总结的这个地方没有听
  • Glide的生命周期使用:后面去补上这个问题;

    • Glide.with