【Android FrameWork】⑤线程通信相关

751 阅读12分钟

1. 线程的消息队列是怎么创建的

看几个问题

  • 可以在子线程创建 handler么
    • 可以,先looper.prepare
  • 主线程的Looper和子线程的Looper有什么区别?
    • 子线程的Looper可以退出,主线程的Looper不能退出
    • 主线程的looper在启动时就创建好了
  • Handler、Looper和MessageQueue有什么关系?
    • java层looper包含MessageQueue,一对一关系
    • 与handler是一对多的关系,可以有多个handler
    • native层的MessageQueue包含一个looper
  • MessageQueue是怎么创建的?
    • java层,Looper构造函数中new出来的,Java层创建的时候调用nativeInit函数
    • native层,会创建一个looper,looper中创建一个eventFd,并且添加一个可读事件到epoll里(见图91)

子线程创建handler

  • 子线程创建handler的前提是要先调用looper.preapare()函数
  • 否则会抛出异常,如图89

handler源码分析

  • looper需要从threadLoacl中get出来
  • 如果looper==null,就会抛出上面的异常
  • 如何得到looper呢,无论子线程还是主线程都是通过prepare函数调用获得的,quitAllowed标记looper是否可以退出
    • 子线程的looper是可以退出的
    • 主线程的looper是不能退出的
    • 图90
  • 主线程的looper会保存在一个静态变量中
  • 下面看looper的构造函数
    • 图91

    • looper构造函数中构造了一个messageQueue

    • messageQueue保存了quitAllowed变量,然后在native层做了一些初始化操作

Handler、Looper和MessageQueue的关系

  • 一个线程包含一个looper,looper中包含了一个messageQueue,一个looper可能对应多个handler
  • handler的消息都发到了线程looper的MessageQueue中
  • MessageQueue分发消息会分别分发到三个hander,分发逻辑是根据message的target值

消息队列的整体架构

  • 分两层,java层和native层,核心实现在native层
  • java层,一个线程线程对应一个looper,对应一个MessageQueue,对应一或多个Handler
  • Looper和MessageQueue在native层都有对应的native对象
    • native层的MessageQueue有一个mLooper,指向native层的Looper

2. 说说android线程间消息传递机制

考察点

  • 消息循环过程是怎样的?
    • 即looper.loop原理
  • 消息是怎么发送的?
    • Handler.sendMessage
  • 消息是怎么处理的?
    • handler.dispatchMessage

消息循环

  • 先拿到当前线程的looper和messageQueue
  • for循环遍历,不断调用next(),拿到下一条要处理的消息
  • 通过msg.target(实质上是一个handler)分发事件到对应的handler,回收消息
  • 看一下消息的分发
    • 如图95
    • 如果消息自带callback,优先用callback处理
    • 否则如果全局callback不为空,用全局callback处理
    • 最后才会走到handleMessage
  • 再看一下如何从消息队列取下一条消息
    • 如图96
    • 通过一个for循环,nativePollOnce是一个native函数,会一直阻塞,阻塞在这是为了监听事件,如果有别的线程往当前的消息队列发消息,就会唤醒这个阻塞并返回,或者到了超时时间也会返回
    • 返回之后就会从消息队列里取一条消息,消息队列是一个单链表,从单链表表头取了一条消息,并且标记为markUse,也就是使用中的意思,然后将消息返回
    • nativePollOnce第一次是立即返回的
    • 如果没有新消息,nativePollOnce超时时间会被设为-1,等到有新消息的时候才会返回

如何向消息队列发消息

  • 先看代码逻辑
    • 如图97
  • 在看图99
    • 图99
    • 图中有两个线程 A和B
    • 线程A中有一个消息队列及一个eventFd,线程A有两种状态,运行状态和阻塞状态,阻塞在eventfd上,监听eventFd事件
    • 当线程B往线程A的消息队列中发送消息的时候,同时也会向eventFd中写一个数,这样线程A就能收到事件了,就会被唤醒来处理这些消息

3. handler的消息延时是怎么实现的?

先看几个问题

  • 消息延时是做了什么特殊处理么?
    • 消息队列messageQueue每次插入消息都会按时间顺序排列
  • 是发送延时了,还是消息处理延时了?
    • 处理消息延时
  • 延时精度怎么样?

延时消息的处理

  • 延时发送消息实质上就是按消息的时间顺序把消息插入到队列中(按时间顺序排序)
  • 来看一下去下一条消息的next函数
    • 如图100
    • 如果没有下一条消息,就会一直阻塞,有消息就会返回
    • 所谓的“有下一条消息”,指的是下一条可用的消息,比如消息队列中有一条消息,但是这个消息的触发时间还没到,那么这就不是一条可用的消息
    • 如果第一条消息的处理时间还没到,就需要等待msg.when-now的时间,并赋值给nextPollTimeoutMillis即超时时间,时间一到,nativePollOnce -> epoll_wait就会自动返回,下一次检查条件满足
    • 如果时间到了,就会返回消息分发

答题要点

  • 消息队列按消息触发时间排序
    • 即越早要触发的消息就越排在前面
  • 设置epoll_wait的超时时间,使其在特定时间唤醒
    • 先计算好要触发的时间距离当前时间多长,把这个时间当做epoll_wait的超时时间,epoll_wait超时的时间就是消息触发的时间,这个时候线程会被唤醒,检查消息队列是否有新的消息要处理
  • 关于延时精度
    • 不是很精确,epoll_wait的超时时间不够精确,另外一点,消息队列有些消息处理非常耗时,导致后来的消息延时处理了

4. 说说IdleHandler的原理

考察点

  • 了解IdleHandler的作用以及调用方式
    • 线程消息队列空闲时执行
  • 了解IdleHandler有哪些使用场景
    • 延迟执行
    • 批量任务
  • 熟悉IdleHandler的实现原理
    • 在MessageQueue中添加到IdleHandler数组中
    • MessageQueue.next()调用,没有新的消息分发时,会调用
    • 返回true会保留IdleHandler,返回false会移除

IdleHandler的作用以及调用方式

/**
     * Callback interface for discovering when a thread is going to block
     * waiting for more messages.
     */
    public static interface IdleHandler {
        /**
         * Called when the message queue has run out of messages and will now
         * wait for more.  Return true to keep your idle handler active, false
         * to have it removed.  This may be called if there are still messages
         * pending in the queue, but they are all scheduled to be dispatched
         * after the current time.
         */
        boolean queueIdle();
    }
  • 注释中明确的指出当消息队列空闲时会执行IdelHandler的queueIdle()方法,该方法返回一个boolean值,如果为false则执行完毕之后移除这条消息, 如果为true则保留表示IdelHandler一直有效,等到下次空闲时会再次执行
  • 看下IdleHandler的使用,如图101
    • 图101
    • 可以看出idelHandler的调用时机是当消息队列没有新消息可以分发的时候,就会处理idelHandler

FrameWork中IdleHandler的使用

  • 先看ActivityThread中的一个函数scheduleGcIdler
    • 图102
    • scheduleGcIdler向主线程添加了一个IdleHandler,这个gcIdler的doGcIfNeeded函数主动触发了一次GC,return false说明主线程在idle的时候只会触发一次gc,也就是主线程第一次idel的时候会触发gc
  • 在看一下同步等待线程idle的技巧
    • 图103
    • 当mIdle为true的时候,退出循环,函数返回

适用场景

  • 延迟执行
    • 图104
    • 当主线程繁忙的时候,有些非紧急的事情可以采取延时加载的方式,但是这个时间是多久其实很难测算,虽然可以采用hander.postDelayed(delayTime)的方式,但是IdleHandler也是一个很好的解决方式
  • 批量任务
    • 批量任务有两个特点
      • ①任务密集
      • ②只关注最终结果
    • 比如打开App,收到了很多条推送,现在要处理这些推送消息并刷新页面,如果刷新界面是一个繁重的任务,那么来一条推送刷一次界面就不太好了. 可以开一个工作线程,来一条推送就把它封装成一个消息丢到工作线程中处理,等到消息全部处理完成工作线程空闲下来之后,再去汇总结果刷新界面,这种场景比较适合IdleHandler

5. 主线程进入loop循环了为什么没有ANR?

考察点

  • 了解ANR触发的原理
  • 了解应用大致启动流程
  • 了解线程的消息循环机制
  • 了解应用和系统服务通信过程

ANR是什么

  • ANR弹窗是在AMS中弹出的,AMS运行于system_server进程,所以ANR相当于是在系统进程中弹了一个Dialog
  • 弹出ANR的实际调用函数是appNotResponding,如图105
    • 图105
  • ANR场景
    • Service Timeout: 服务20s之内没有执行
    • BroadCastQueue TimeOut: 前台广播10s内没有执行完成
    • ContentProvider TimeOut
    • InputDispatching TimeOut: 输入事件超过5s没有执行,包括触屏按键等事件

ANR如何被触发 以Service为例

  • 图106
  • 当Service没有启动起来的时候,就会延迟弹出ANR
  • 当Service启动之后,延迟弹出ANR的消息会被删除

消息循环机制

看下哪些线程可能会往主线程发送消息

  • ①主线程给自己发消息(主线程running状态下)
  • ②子线程给主线程发消息
  • ③binder线程给主线程发消息
    • 如图108
    • 由于系统服务向应用端发起binder调用的时候,先跑在应用端的binder线程池中,应用在处理请求时,并没有在binder线程中处理这些请求,而是先封装了一个消息,把消息丢到主线程中处理,比如启动Activity,创建Service等

应用主线程loop循环,为什么没有ANR?

  • ANR是应用没有在规定的时间内完成AMS指定的任务导致的
  • AMS请求调到应用端binder线程,封装成消息再丢消息去唤醒主线程来处理
    • 应用在一个for循环中不断地从消息队列中取消息、处理消息
    • 所以,进入for循环不代表是死循环,它还是可以处理系统发来的请求
  • ANR不是因为主线程loop循环,而是因为主线程中有耗时任务
    • 主线程中有耗时任务的时候,其他请求会被delay,导致在规定的时间内没有完成
    • 或者这个请求本身也很耗时

6. 听说过消息屏障么?

消息分类 消息分三种

  • 图109
  • normal:普通消息
  • barrier:屏障消息(给其他消息添堵的),不需要分发
  • async:异步消息,与普通消息相比多了一个异步标志位

界面绘制,输入事件这些事件都是比较紧急的,不能被普通消息耽误,所以专门设计了消息屏障,保证异步消息的优先执行

消息屏障的特点

  • 屏障消息没有target,因其不需要分发,所以也就不需要handler
  • 消息屏障也是带时间戳的,在消息队列按照时间排序,屏障消息只会影响到它后面的消息
  • 消息队列可以插入多个屏障消息
  • 发送屏障消息不会唤醒线程
  • 插入屏障消息会返回一个token,这个token是它的序列号
  • postSyncBarrier不是一个开放的api,只能通过反射创建
    • 如果发一个不带target的消息是不被允许的,需要指定handler

看下消息屏障的移除

  • 删除屏障需要带上token,根据token找到消息
  • 删除屏障的时候需要唤醒线程,假设屏障消息恰好在队列头部,撤出屏障就需要唤醒线程
  • 如果屏障消息不在队列头,就说明消息队列没有被屏障block住
  • 删除屏障后就会马上处理被block住的消息,消息的时间到了就会被马上分发,没到时间线程就会继续休眠等待

屏障的处理

  • 屏障会block住后面的普通消息(normal)
  • 当遍历到屏障消息时,会继续遍历看看有没有异步消息

插入消息的时候有屏障会怎样

  • 如果插到队列头了,如果当前线程是休眠的,就要唤醒他
  • 如果没插到队列头,如果当前线程是休眠的,并且队列头是屏障,并且当前消息是最早的一条异步消息,就要唤醒线程。

看几个问题

  • 消息队列是空的时候,插一个屏障,会触发idleHandler么?
    • 不会,线程还在休眠
  • 如果删除了屏障,消息队列空了,会触发idleHandler么?
    • 不会,idleHandler已经调过了,不会重复触发
    • 比如处理完了一条消息,再去取下一条消息,发现没有新的消息了,这时候就会调idleHandler,调完之后检查消息队列,如果检查还是没有新的消息,就会休眠;如果被唤醒了但是消息队列没有可执行的消息了,这时候不会重复调用idleHandler
    • 所以:之前消息队列处理完最后一个消息之后,休眠之前肯定调过一次idleHandler,消息屏障删除时唤醒了线程,消息队列还是没有要处理的消息时,是不会重复调用idleHandler的
  • 如果消息队列只有一个屏障消息,插一个普通消息会idleHandler么?
    • 有可能:idleHandler的触发条件是消息队列为空或者第一条消息的时间还没到,所以关键是屏障的时间到了没有
  • 如果消息队列只有一个屏障消息,插一个异步消息会idleHandler么?
    • 有可能:关键是看屏障的时间到了没有

FrameWork中屏障的使用

  • 界面绘制
    • 图114
    • 这里的逻辑是:绘制的时候插入一条消息屏障,把后面的消息block住,让界面绘制的异步消息优先执行
  • 输入事件分发