Android主线程运作机制,消息队列与VSYNC

前言

对于Android开发者来说,“主线程”再熟悉不过了,如果我们不做特殊的处理(如开线程、AsyncTask、HandlerThread、IntentService、coroutine等),那么我们的代码将执行在主线程,而卡顿、掉帧等较差的交互体验也多是因为主线程承受了其本不该承受的重任务,进而导致渲染任务不能如期完毕所致。

所以,较好的理解主线程的运作机制对于开发具有优良性能的APP至关重要。本文将主要围绕主线程最常见的应用场景来阐述其运作机制。

为了方便阐述与理解,本文将忽略MQ的屏障与异步消息机制

缘起

正常我们如果编写程序,比如一个HelloWorld:

void main() {
	println("Hello World");
}
复制代码

在经过编译后,其实它的样子变成了:

void main() {
  println("Hello World!");
  exit(0);
}
复制代码

即,编译器会主动在入口方法的最后插入结束指令,以让进程(0号线程)主动结束。在开发者看来,就变成了我们的入口方法中代码执行完毕了,程序就自然结束了。

在Android中,当一个APP启动,其实是由zygote进程fork出来的一个子进程,这个进程(其中自然携带了一个0号线程)以 ActivityThread.main 为程序入口开始执行,而在main方法的最后,其实是没有机会到达exit的终点的,因为其调用了Looper.loop() 让0号线程“永远”不会到达终点(除了进程被杀掉):

即,APP进程在经过入口方法后,就将主线程全权交给了Looper机制去操控。我们称这个Looper为Main Looper,即主线程Looper,其背后会构造一个MessageQueue作为主线程的消息队列(以下简称主MQ)。

关于Looper的运行机制本文并不打算详细介绍,相信即使是初级的开发者也是理解的。简而言之:MQ是一个消息队列,而Looper利用无限循环从中不停的取出消息进行解析执行,Handler正是这个“解释执行”的载体。而为了避免跑死CPU,这里的无限循环是采用epoll机制进行的,无消息或等消息时其实是进入休眠,等某消息的预期时间到达或新的即时消息插入,则MQ会被唤醒。综上,只要有人向MQ丢消息,就一定会在被执行(或如期、或延期),如果MQ中没有消息,就会挂起等待(闲置)。

我们常见的主线程MQ的消息来源有三个:

  1. Activity生命周期,触发onCreate、onPause、onResume等
  2. 开发者通过主线程Handler进行post,自定义的处理过程,RxJava、Retrofit内部也有这种操作
  3. Choreograhper监听VSYNC信号,ViewRootImpl借由其进行View的onMeasure、onLayout、onDraw(以下不严谨的简称View渲染)

当然,具体的消息来源还有更多,如application的attach、service、broadcast等等,这里只列举最常见的便于理解

可用一张图表示以上内容(心疼老板一秒钟):

执行权

即一段代码在某一线程上获得了执行的权利,一个线程只有一份执行权。可以肯定的说,对于一个Android App,执行在主线程中的代码(无论开发者代码还是Framework代码)的执行权都是MQ赋予的(当然,排除掉亿点点ActivityThread.main入口函数代码)。换言之,每一段主线程代码的执行权都是因为其对应的Message被Looper调度处理时得来的。

如上图所示,时序上MsgA、B、C是串行关系,而其对应的A、B、C三个代码段也必然会串行执行。举个接地气的例子,比如CodeA是Activity的onCreate生命周期:

//Code A
fun void onCreate(savedInstance: Bundle?) {
	super.onCreate(savedInstance)
  ...
  MyApiService.requestSomeData(callback: object Callback() {
  	@Override
    fun onSuccess(data: DataModel) {
      //回调时间点
    	UIHandler.get().post {
        doCodeB(data) //codeB 执行时间点
      }
    }
  })
  ...
  //准备执行耗时操作时间点
  doSomeHeavyWork()
  //onCreate即将完成时间点
}
复制代码

上述代码中:

  1. 我们在onCreate中首先发起了一个异步请求,假设这个callback会携带数据在后台线程回来
  2. 我们需要数据的处理过程CodeB在主线程中进行,所以使用UIHandler.post了出去,此时向主MQ插入了一个MsgB
  3. 发起请求后,做了一件坏事,即doSomeHeavyWork()在主线程做了长耗时操作

虽然我们尽快的发起了网络请求(一定是后台线程进行的),但即便网络速度再快——使得回调时间点发生在准备执行耗时操作时间点前,CodeB也无法得到执行。因为此时主线程的执行权还在CodeA手中,它尚未完成。如图解释:

(绿色正在执行,蓝色等待执行)

至此,解释了常见的Activity生命周期和开发者代码(含框架代码)的调度关系。所谓“执行权”也可以理解为单一线程维度的唯一资源,而这个资源只能通过公平等待得来(不存在非公平锁的逻辑)。

VSYNC

即垂直同步,用来协调渲染和屏幕呈现的刷新率。本文不打算深入介绍,可以简单的理解为VSYNC是一个由硬件发出(有时也会软件模拟)的周期性中断信号,对于60Hz刷新率来说,这个周期性即为16.67ms。

自Android黄油计划(3.0)后就引入了VSYNC机制,对于APP来说,每一个View的渲染的最短触发间隔也是跟随VSYNC来的(onMeasure、onLayout有可能在一次VSYNC中触发两次,但onDraw只可能有一次),其背后的始作俑者(Java层)是Choreographer(编舞者)。

VSYNC信号在APP层面被调度的简单原理是:

  • Choreographer作为Java层对接底层VSYNC信号监听的桥连,当有人需要监听VSYNC信号时(调用postCallback相关接口),其会发起监听请求。即VSYNC信号是订阅制,谁关心谁收到(下一个)
  • Choreographer收到VSYNC信号时(onVsync)并不是处于主线程,其会向主MQ发送一条Message,由这条Message来进行此次VSYNC信号的处理(doFrame)
  • 常见的,当页面上任何一个View调用requestLayout时都会最终触发ViewRootImpl.scheduleTraversal(),而这个方法的实际内容就是让告知Choreographer我要消费下一个VSYNC,请你监听它
  • 当下一个VSYNC来临后,ViewRootImpl将触发遍历整个ViewTree的命中dirty的那些子View的渲染

所以,由此可知(也符合我们的常识认知):View的渲染都将在主线程中执行。那么此时,主线程执行权的争夺者又多了一位——VSYNC消费(View渲染)。而不管VSYNC信号何时来、来得多么快,它都得老老实实的等着当前主线程执行权的持有者释放了执行权之后才去真正消费(doFrame)。

如上图所示,图中描述了几种VSYNC信号在主线程消费时的情况:

  • 无论主线程执行权情况如何,来自非主线程的VSYNC信号所对应的消费过程(doFrame)都必须化作主线程的Message
  • 若VSYNC消费过程已经到了消费时间(Message.when),但其他代码段持有主线程执行权时,只能等待
  • 若已有VSYNC信号处于等待消费状态中,即使后边进入再多VSYNC信号也不会重复插入消费的消息
  • 只有有人需要时VSYNC信号的消费才会在主线程进行,否则主线程不会在意,其消费过程(doFrame)自然也不会被触发

还是以上图为例,即使是在 some code 中有过View.requestLayout的操作,下次的VSYNC信号也只能等待当前的执行权被释放后才会触发。不同于Flutter将渲染线程和执行线程分开,Android将其都在主线程调度。所以,即使你在持有主线程执行权的情况下对某个View执行了更新操作,这个更新也只能等到下一个VSYNC消费时才真正生效。

总结

  • Android主线程的执行权非常宝贵,不只是开发者代码在争取。常见的,生命周期的调度、View的渲染(代码层面的渲染过程)都会争夺执行权,所以在主线程上的事情,虽不见得越少越好,但一定是越快越好
  • 从渲染流畅性角度讲,一方面,我们要做到尽量不长时间、过渡占用主线程的执行权,让其能够较快的调度给VSYNC信号的消费过程。另一方面,每一次VSYNC信号的消费即是一帧,这一帧的绘制主要取决于View的onMeasure、onLayout、onDraw,让最少的View变dirty,让三个过程都尽量快,就会减少更多丢帧的情况发生
  • 打破误区。Android中,并不是我们在主线程设置完View的数据后,就会有其他线程默默取走进行渲染,要记得,它在等你!

发散问题

你能想到几种办法去感知下一帧的渲染耗时?Choreographer的postFrameCallback可以吗?

猜你喜欢

转载自juejin.im/post/7039539517484695588
今日推荐