首页 > 代码库 > Android黄油计划之Choreographer原理解析

Android黄油计划之Choreographer原理解析

     搞客户端开发,时间也有点了,但是每次想起来,总感觉自己掌握的东西零零散散,没有一点集在的感觉,应用层的懂,framework的也懂,框架啥的了解一点,分层的思想也有一些,JVM的原理啊,内存分配和管理啊,运行机制啊啥的也知道一点,每次下班或者没事了,就在考虑,自己应该有一个主攻方向,往这个方向集中发展一下,首选的几个目标应该是非常清楚的,我们要掌握android,那么关于android的View机制、动画原理这些都是必须要掌握的,所以呢,自己想在这几个方面花些时间,好好研究一下,这样才能使自己更具竞争力。

     好了,不管是要了解View机制,还是android动画,我们应该都需要有Choreographer的知识,明白系统刷新机制到底是怎么样的,这样才能对其他方面有更好的辅助。本章博客,我们就来学习一下Android中的Choreographer的运行机制。

     我们都知道,应用层的一个Activity对应一个根View(也就是一个DecorView)、一个WindowState、一个ViewRootImpl,每个对象都非常重要,都是在Activity添加过程中重量级的对象,DecorView是当前Activity的根View,它里面管理着当前界面的View树;WindowState对象是当前Activity窗口在系统侧WindowManagerService中代理对象;ViewRootImpl则肩负着View的标准三步曲的处理和事件分发,而View绘制也是由Choreographer指导的,Choreographer的英文意思就是编舞者、舞蹈指挥,看着非常形象。那我们就从Choreographer对象的构建开始说起吧,它的构建是在ViewRootImpl的构造方法中的,代码如下:

<script src="https://code.csdn.net/snippets/1889952.js" type="text/javascript"></script>

     从构造方法中可以看到Choreographer是单例模式的,也就是一个ViewRootImpl对象对应一个Choreographer,当界面需要重绘时,都会调用到ViewRootImp类的scheduleTraversals()方法,这里的实现也比较简单,代码如下:

<script src="https://code.csdn.net/snippets/1955970.js" type="text/javascript"></script>

     mTraversalScheduled表示是否已经发起重绘,每次scheduleTraversals()方法调用之后,就会将它置为true,然后在下次调用doTraversal()又先将它置为false,然后调用mChoreographer.postCallback()添加一个Runnable,请注意,第一个参数是Choreographer.CALLBACK_TRAVERSAL,在Choreographer当前,添加的类型一共有三种,分别是:CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL,分别表示事件回调、动画回调、绘制回调。postCallback()方法是转而调用postCallbackDelayed()方法的,最后一个参数delayMillis传的是0,表示当前的重绘不需要延时,我们跟进去看一下添加的postCallbackDelayed()方法的代码:

<script src="https://code.csdn.net/snippets/1955992.js" type="text/javascript"></script>
     首先判断参数action是否为空,action就是我们要回调的对象,回调对象都为空了,那我们还干啥呢?其实判断callbackType,在整个过程中,只定义了上面描述的三种类型的事件,如果传入的type值不符合,那就抛出一个IllegalArgumentException("callbackType is invalid")异常。参数正常了,继续调用postCallbackDelayedInternal()进一步处理。postCallbackDelayedInternal()方法的代码如下:

<script src="https://code.csdn.net/snippets/1956005.js" type="text/javascript"></script>
     此处获取当前时间,然后加上要延迟的时间,作为当前Callback的时间点,以这个时间点作为标准,把Callback对象添加到mCallbackQueues[callbackType]队列当中,这块的逻辑和Looper、MessageQueue、Handler中添加Message的逻辑很相似,大家可以对比学习。然后判断dueTime <= now,这块的逻辑看了半天,我确实没看懂,dueTime会有比now小的情况吗,也就是传进来的delayMillis小于0,再往上讲,就是当前要添加的回调要在上一次添加的回调之前,这感觉不太可能吧?如果有弄懂的朋友,烦请解答一下。此处应该是执行else分支,往当前的队列中添加一个Message,那么通过Handler机制就会进行处理,此处的mHandler是一个FrameHandler对象,我们来看一下FrameHandler的代码:
<script src="https://code.csdn.net/snippets/1956029.js" type="text/javascript"></script>
     这里的message消息也比较简单,MSG_DO_FRAME指系统在没有使用Vsync机制的时候,使用异步消息来刷新屏幕,当然,大家一定要理解,此处的刷新其实只是刷新屏幕工作的很小一部分,只是回调ViewRootImpl方法中添加的Runnable对象,最终是调用根View的draw方法,让每个子View有把自己的图像元素填充到分配好的显存当中,而要完全显示,还有很多工作要作,最终是在SurfaceFlinger类中对所有窗口的View进行合成,然后渲染,最终post到FrameBuffer上,才能显示出来的;MSG_DO_SCHEDULE_VSYNC当然就是指系统使用Vsync来刷新了;MSG_DO_SCHEDULE_CALLBACK就是指添加Callback或者FrameCallback完成的消息了。好了,我们继续看MSG_DO_SCHEDULE_CALLBACK的消息处理,它是调用doScheduleCallback(msg.arg1)来进行处理的,msg.arg1是刚才添加消息时的类型。我们整个看一下handleMessage()方法的代码,发现非常简单,这也是一个非常好的习惯,我们平时的代码当中,也应该尽量这样实现,这样一眼就可以看出来这个方法所要作的事情,把具体的处理放到每个细节方法中去。我们来看一下doScheduleCallback()方法的实现:
<script src="https://code.csdn.net/snippets/1956036.js" type="text/javascript"></script>
     mFrameScheduled和ViewRootImpl的scheduleTraversals()方法中的变量mTraversalScheduled作用是一样的,也是判断当前是否正在执行添加,然后调用(mCallbackQueues[callbackType].hasDueCallbacksLocked(now))判断是否已处理过Callback事务,该方法的判断也很简单,(mHead != null && mHead.dueTime <= now),如果当前队列头不为空,并且队列头元素的时间点小于当前的时间点,那就说明是之前添加的,则需要对它进行处理;相反,如果队列头为空或者添加的时间点大于当前的时间点,也就是要延迟处理,则不需要任何操作。条件符合的话,就调用scheduleFrameLocked(now)进一步处理,我们来看一下scheduleFrameLocked()方法的实现:
<script src="https://code.csdn.net/snippets/1956042.js" type="text/javascript"></script>
     此片一开始就把mFrameScheduled赋值为true,表示事务开始执行了,那么上面doScheduleCallback()方法当中的代码此该就不会再执行了。接下来的逻辑以USE_VSYNC分开,意思也非常明了,就是系统是否使用Vsync刷新机制,它是通过获取系统属性得到的,private static final boolean USE_VSYNC =  SystemProperties.getBoolean("debug.choreographer.vsync", true)。如果使用了Vsync垂直同步机制,则一步判断当前线程是否具备消息循环,如果有消息循环,则立即请求下一次Vsync信号,如果不具有消息循环,则通过当前进程的主线程请求Vsync信号;如果没有使用Vsync机制,则使用异步消息延时执行屏幕刷新。是否具有消息循环是通过调用isRunningOnLooperThreadLocked()方法完成判断的,它的实现很简单,return Looper.myLooper() == mLooper。因为当Choreographer对象在创建的时候,参数looper就是调用Looper looper = Looper.myLooper()获取回来的,也就是说当前进程肯定是有消息循环的,所以此处的判断为true,其他两个分支:当前线程不具备消息循环和系统未使用Vsync同步机制的逻辑,我们就不分析了,大家有兴趣的话,可以自己跟踪一下。进入if分支,继续调用scheduleVsyncLocked()方法进行处理,它的实现非常简单,就是调用mDisplayEventReceiver.scheduleVsync()来请求下一次Vsync信号。
     看到这里,是不是感觉逻辑有点多了,开始乱了,转来转去的,系统到底要干啥?呵呵,我们暂停下来梳理一下,系统作了这么多事情最终的目的就是在下一次Vsync信号到来的时候,将Choreographer当中的三个队列中的事务执行起来,这些事务是应用层ViewRootImpl在scheduleTraversals()方法中添加进去的,在Choreographer当中,我们要先将外边传进来的Callback放入队列,然后就要去请求Vsync信号,因为Vsync信号是定时产生的,你不请求,它就不会理你,当然你收不到回调,也就不知道啥时候通知ViewRootImpl执行View的measure、layout、draw了,这样说一下,大家清楚我们要干什么了吗?我第一次看Choreographer类的代码时候,看了半天,也是乱了,所以这里大概理一下。
     好,我们搞清楚目的了,继续往前走,我们现在已经将Callback添加到队列中了,下一步要作的就是请求Vsync信号了。mDisplayEventReceiver是一个FrameDisplayEventReceiver对象,我们来看一下它的代码定义:
<script src="https://code.csdn.net/snippets/1956084.js" type="text/javascript"></script>
     我们可以看到这里的mTimestampNanos时间定义都是纳秒级别的,因为Vsync信号是用来同步屏幕刷新频率的,所以对时间的要求非常高,才采用了纳秒级别的,如果大家对Vsync信号的产生机制不了解的话,可以看我前面的博客:Vsync垂直同步信号分发和SurfaceFlinger响应执行渲染流程分析(一),mDisplayEventReceiver类变量是在Choreographer的构造方法中赋值的,我们继续来看它的scheduleVsync()方法的实现,因为FrameDisplayEventReceiver类是继承DisplayEventReceiver的,而它没用对scheduleVsync()方法重写,所以是调用父类的:
<script src="https://code.csdn.net/snippets/1956087.js" type="text/javascript"></script>
     它的实现很简单,判断描述符mReceiverPtr是否合法,如果非法就打印日志,什么也不作了,合法的话,就继续调用native方法nativeScheduleVsync(mReceiverPtr)来请求Vsync信号。nativeScheduleVsync()方法实现在android_view_DisplayEventReceiver.cpp当中,是通过定义JNINativeMethod gMethods[]来定义方法调用指针的,因为此类的代码不多,这里就全部贴出来,方便大家查看:
<script src="https://code.csdn.net/snippets/1956107.js" type="text/javascript"></script>
     我们来看一下nativeScheduleVsync方法的定义,{ "nativeScheduleVsync", "(J)V", (void*)nativeScheduleVsync },这里需要说明一下,java方法和JNI方法存在着对应关系,"(J)V"括号里边的表示该方法的入参,括号外边的表示返回值J表示long,而返回值V表示Void,关于这个,大家可以看我之前的博客:JNI字段描述符“([Ljava/lang/String;)V”,也是转载别人的,呵呵。好了,我们继续看这个方法的实现,它将java层传进来的描述符强制转换为NativeDisplayEventReceiver对象,这样的处理在JNI当中是非常多见的,大家要熟悉。然后调用它的scheduleVsync()方法,最后根据返回值判断当前请求Vsync信号是否成功,如果status非0,则抛出RuntimeException异常。很明显,我们从这都可以猜出,正常情况下,返回的status应该为0了。
     我们继续来看NativeDisplayEventReceiver::scheduleVsync()方法的处理逻辑。首先检查mWaitingForVsync,如果当前正在请求Vsync信号,则就不需要重复请求了,只有在当前未请求的时候,才需要发出新的请求,然后调用processPendingEvents()将当前队列中还存在receiver处理掉,因此方法与我们的流程不相关,这里就不展开了,大致是使用pipe机制将mReceiver中还存在的receiver一一读出,大家如果了解Linux机制的话,就知道pipe机制对应了两个管道,管道中的数据被读出之后,也就相应的从管道中移除了,所以不需要两端对数据做任何移除的处理,每一个receiver处理完成后,就设置一下gotVsync = true,
*outTimestamp = ev.header.timestamp,*outId = ev.header.id,*outCount = ev.vsync.count,gotVsync的意思就是当前的receiver已经收到Vsync信号通知了。好了,我们回到主流程,scheduleVsync()方法当中处理完队列中的receiver后,就开始调用mReceiver.requestNextVsync()请求新的Vsync信号了,mReceiver是一个DisplayEventReceiver对象,我们来看一下requestNextVsync()方法的实现,因这个类的代码也很少,这里就直接全部贴出来了:
<script src="https://code.csdn.net/snippets/1956164.js" type="text/javascript"></script>
     requestNextVsync()方法中直接调用mEventConnection->requestNextVsync()来请求Vsync信号,mEventConnection对象是在DisplayEventReceiver类的构造函数中创建的,mEventConnection = sf->createDisplayEventConnection(),sf就是SurfaceFlinger对象,SurfaceFlinger类的createDisplayEventConnection()实现也非常简单,就是调用mEventThread->createEventConnection(),这又回到我们之前的博客了,大家可以去看一下。
     EventThread一直在无限循环threadLoop()中请求Vsync信号的,当收到一个Vsync信号后,会调用status_t err = conn->postEvent(event)来进行分发,conn也就是上面的EventThread::Connection对象了,最后经过处理,回调到NativeDisplayEventReceiver::handleEvent(int receiveFd, int events, void* data)方法当中,这里同样processPendingEvents()处理完队列中的回调后,就调用dispatchVsync(vsyncTimestamp, vsyncDisplayId, vsyncCount)开始分发了,在NativeDisplayEventReceiver::dispatchVsync()这个方法中是通过当前的native层的执行环境env回调到java层的,env->CallVoidMethod(mReceiverObjGlobal,
gDisplayEventReceiverClassInfo.dispatchVsync, timestamp, id, count),再往下就回调到java层中DisplayEventReceiver类的dispatchVsync()方法中了。它里边的实现就是调用onVsync(),而FrameDisplayEventReceiver复写了onVsync()方法,所以就执行到Choreographer.FrameDisplayEventReceiver中的onVsync()方法了。
     转了好大一圈,我们终于又从native层回来了。好,我们继续java层往下分析,Vsync信号拿回来了,大家应该也知道,我们的目的快达到了!!
     onVsync()方法中以this为对象,向mHandler中添加了一个消息,消息处理的时候,就会调用它的run()方法了。run方法中直接调用doFrame()来进行处理。我们来看一下它的实现:
<script src="https://code.csdn.net/snippets/1956205.js" type="text/javascript"></script>
     如果(frameTimeNanos < mLastFrameTimeNanos)满足,则说明我们已经错过了本次的Vsync信号了,那么这种情况下,就什么也不用处理,重新获取下一次信号了。如果没有错过的话,那就进一步三次调用doCallbacks()分别对应三种事件类型来分发了。三种事件的顺序也是定义的顺序:CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL,这也是他们的处理优先级,输入事件放在第一,也是为了能尽快响应用户的操作,但是即使这样,Android的流畅性还是不如IOS,当然,这个原因就是其他方面的了,我们这里就不探讨了。我们来看一下doCallbacks()方法的实现:
<script src="https://code.csdn.net/snippets/1956220.js" type="text/javascript"></script>
     这里就是将每种类型的事件队列中的元素取出来,通过for循环一一调用他们的run()方法了,调用完成后,将队列中的Callback回收掉。而这里的CallbackRecord对象就是我们在ViewRootImpl类当中添加的InvalidateOnAnimationRunnable、mConsumedBatchedInputRunnable、mTraversalRunnable这三类对象了,那么回到View的流程中,收到Vsync信号后,就会回调mTraversalRunnable的run()方法,再次发起一次measure、layout、draw流程,那么也就和Vsync信号对接上了。
     好了,到这里呢,我们整个流程也就分析完了,希望对大家有所帮助,谢谢大家!

Android黄油计划之Choreographer原理解析