Android9.0 Choreographer 源码分析

参考:
//www.greatytc.com/p/996bca12eb1d
//www.greatytc.com/p/dd32ec35db1d
//www.greatytc.com/p/c2d93861095a
//www.greatytc.com/p/6f2043570de4

一.概念

1.Choreographer接收显示系统的VSync信号,在下一个frame渲染时控制输入(Input)、动画(Animation)、绘制(Draw)三个UI操作。其实UI显示的时候每一帧要完成的事情只有这三种。


1.png

2.开发者可以使用Choreographer#postFrameCallback设置自己的callback与Choreographer交互,你设置的callCack会在下一个frame被渲染时触发。
3.Callback目前有4种类型,Input、Animation、Draw、Commit。
4.每个进程的UI线程都有自己的choreographer,应用层的一个Activity对应一个根View(也就是一个DecorView)、一个WindowState、一个ViewRootImpl
6.如果掉帧数>=30的话会打印下面的log:

I/Choreographer: Skipped 55 frames!  The application may be doing too much work on its main thread.

同时也可以修改默认30帧,方法如下:

getprop debug.choreographer.skipwarning      //读取
setprop debug.choreographer.skipwarning 35    //修改
setprop ctl.restart surfaceflinger; setprop ctl.restart zygote    //重启

二.代码分析

1.请求vsync

app必须首先请求vsync,然后接收vsync后才进行绘制,而且每次都需要请求vsync,不是一劳永逸。

1.1 代码UML图

2.png

ViewRootImpl#scheduleTraversals遍历视图树的每一个节点,完成一个树形视图的绘制过程。每当ViewRootImpl安排一次遍历scheduleTraversals,将mTraversalScheduled置true,表示已经schedule过了traversal,防止多次安排,然后发送同步栅栏。当执行遍历doTraversal或主动取消遍历unscheduleTraversals时,会关掉标志位mTraversalScheduled,取消同步栅栏。委托Choreographer安排遍历,请求信号。
mTraversalScheduled表示是否已经发起重绘,每次scheduleTraversals()方法调用之后,就会将它置为true,然后在下一帧的时候调用doTraversal()又先将它置为false,然后调用mChoreographer.postCallback()添加一个Runnable。

frameworks/base/core/java/android/view/ViewRootImpl.java
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
  }

@Override
public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

void invalidate() {
    mDirty.set(0, 0, mWidth, mHeight);
    if (!mWillDrawSoon) {
        scheduleTraversals();
    }
}

同样也存在其他的input和animation的postCallback:

mChoreographer.postCallback(Choreographer.CALLBACK_INPUT, ...
mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, ...

下面看下这3种方式在ViewRootImpl中的调用:

//方式1:CALLBACK_TRAVERSAL
void scheduleTraversals() {
...
mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }
    final TraversalRunnable mTraversalRunnable = new TraversalRunnable();
//方式2:CALLBACK_INPUT
final class ConsumeBatchedInputRunnable implements Runnable {
        @Override
        public void run() {
            doConsumeBatchedInput(mChoreographer.getFrameTimeNanos());
        }
}
final ConsumeBatchedInputRunnable mConsumedBatchedInputRunnable =
      new ConsumeBatchedInputRunnable();

void scheduleConsumeBatchedInput() {
    mChoreographer.postCallback(Choreographer.CALLBACK_INPUT,
          mConsumedBatchedInputRunnable, null);
//方式3:CALLBACK_ANIMATION
final class InvalidateOnAnimationRunnable implements Runnable {
...
    public void run() {
    ...
        for (int i = 0; i < viewRectCount; i++) {
                final View.AttachInfo.InvalidateInfo info = mTempViewRects[i];
                info.target.invalidate(info.left, info.top, info.right, info.bottom);
                info.recycle();
        }

   private void postIfNeededLocked() {
        mChoreographer.postCallback(Choreographer.CALLBACK_ANIMATION, this, null);

1.2 流程图

3.png

ViewRootImpl负责接收input、animation、traversal,交由“编舞者”Choreographer处理。下面分析下过程:
1)CallbackQueue数组,数组元素是CallbackQueue对象,内部包含指向CallbackRecord链表的头指针。四种callbackType类型:CALLBACK_INPUT、CALLBACK_ANIMATION、CALLBACK_TRAVERSAL、CALLBACK_COMMIT。每一种callbackType类型代表数组的一个索引;CallbackQueue#addCallbackLocked方法,创建一个CallbackRecord对象,封装dueTime执行时间(当前时间+延迟),任务action和token。将CallbackRecord插入链表,按照dueTime时间排序决定插入位置,dueTime小的位于链表的前面。
2)当dueTime还没有到的话(做了延迟)就会向UI线程发消息,做到异步,FrameHandler主要就是处理这些动作,详细的代码见下。如果dueTime到了或者是UI线程已经处理完了这些消息后会调用scheduleFrameLocked。
可以看到FrameHandler主要处理MSG_DO_FRAME、MSG_DO_SCHEDULE_VSYNC、MSG_DO_SCHEDULE_CALLBACK动作;

frameworks/base/core/java/android/view/Choreographer.java
    private final class FrameHandler extends Handler {
        public FrameHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MSG_DO_FRAME:
                    doFrame(System.nanoTime(), 0);
                    break;
                case MSG_DO_SCHEDULE_VSYNC:
                    doScheduleVsync();
                    break;
                case MSG_DO_SCHEDULE_CALLBACK:
                    doScheduleCallback(msg.arg1);
                    break;
            }
        }
    }

3)根据mFrameScheduled(该帧是否已经处理过了)的值决定是否做最终的doFrame;mFrameScheduled控制scheduleFrameLocked和doFrame,两者互相影响。在第一次的scheduleFrameLocked中mFrameScheduled会被置为true,表示有帧需要处理了,在doFrame中会判断这个值,如果为false的话就表示没有帧处理,即no work to do,因为为true,所以在处理完成后会将值置为false,进行下一次循环。
Choreographer中有dump方法会打印该值,一会儿研究一下~
4)然后判断是否有vsync,没有的话在发送MSG_DO_FRAME给FrameHandler去处理doFrame;
5)然后判断vsync在UI thread还是在其它,如果是非UI的话发送MSG_DO_SCHEDULE_VSYNC给FrameHandler去处理doScheduleVsync,最终都会调用scheduleVsyncLocked。
6)最后调用native的vsync。

2.接收vsync

2.2代码UML图

java层接收到的vsync从底层上报的,具体流程如下:

01-02 15:12:37.132  1400  1400 W System.err: java.lang.RuntimeException
01-02 15:12:37.133  1400  1400 W System.err:    at android.view.Choreographer$FrameDisplayEventReceiver.onVsync(Choreographer.java:850)
01-02 15:12:37.133  1400  1400 W System.err:    at android.view.DisplayEventReceiver.dispatchVsync(DisplayEventReceiver.java:171)
01-02 15:12:37.133  1400  1400 W System.err:    at android.os.MessageQueue.nativePollOnce(Native Method)
01-02 15:12:37.133  1400  1400 W System.err:    at android.os.MessageQueue.next(MessageQueue.java:325)
01-02 15:12:37.133  1400  1400 W System.err:    at android.os.Looper.loop(Looper.java:142)
01-02 15:12:37.133  1400  1400 W System.err:    at android.app.ActivityThread.main(ActivityThread.java:6722)
01-02 15:12:37.133  1400  1400 W System.err:    at java.lang.reflect.Method.invoke(Native Method)
01-02 15:12:37.133  1400  1400 W System.err:    at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:449)
01-02 15:12:37.133  1400  1400 W System.err:    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:807)
4.png

2.2流程图

5.png

FrameDisplayEventReceiver接收vsync(onvsync)后会做doFrame动作。

3.总结

6.png

ViewRootimpl在处理完上一帧数据后会重新遍历下view树会postCallback请求下一帧的vsync,driver产生vsync后会回调onVsync函数接收,传入frameNanos和frame,接收后调用doFrame动作。
ViewRootImpl postCallback -> TraversalRunnable run, 那么我们可以自定义方法回调doFrame,详细原因可见下面doCallback部分的分析:

public class FPSFrameCallback implements Choreographer.FrameCallback{
@Override
  public void doFrame(long frameTimeNanos){
      //do something
  }
}

Choreographer.getInstance().postFrameCallback(new FPSFrameCallback());

4.doFrame

4.1流程图

doFrame.png

当出现掉帧时,会对 frameTimeNanos 进行修正,修正到最后一次 VSync 的时间;当 frameTimeNanos < mLastFrameTimeNanos 时,请求 VSync 然后 return,相当于忽略本次信号,等待下一个信号。但这个条件不太可能通过,有可能的情况比如 System.nanoTime() 发生倒退(比如修改手机时间),反正这个条件通过就代表出现异常情况。

4.2代码流程

void doFrame(long frameTimeNanos, int frame) {    
  final long startNanos;    
  synchronized (mLock) {        
    if (!mFrameScheduled) { //判断是否有callback需要执行,mFrameScheduled会在postCallBack的时候置为true(象征vsync信号到了),一次frame执行时置为false(象征该帧已渲染完毕)       
      return; // no work to do        
    }
    //打印跳frame时间        
    if (DEBUG_JANK && mDebugPrintNextFrameTimeDelta) {            
      mDebugPrintNextFrameTimeDelta = false;            
      Log.d(TAG, "Frame time delta: "                    
              + ((frameTimeNanos - mLastFrameTimeNanos) *  0.000001f) + " ms");        
    }
    //设置当前frame的Vsync信号到来时间        
    long intendedFrameTimeNanos = frameTimeNanos;        
    startNanos = System.nanoTime();//实际开始执行当前frame的时间
    //时间差        
    final long jitterNanos = startNanos - frameTimeNanos;        
    if (jitterNanos >= mFrameIntervalNanos) {
      //时间差大于一个时钟周期,认为跳frame            
      final long skippedFrames = jitterNanos / mFrameIntervalNanos;//跳帧数
      //跳frame数大于默认值,打印警告信息,默认值为30,控制台输出            
      if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {                
         Log.i(TAG, "Skipped " + skippedFrames + " frames!  "                        
                    + "The application may be doing too much work on its main thread.");            
      }
      //计算实际开始当前frame与时钟信号的偏差值            
      final long lastFrameOffset = jitterNanos % mFrameIntervalNanos; 
      //打印偏差及跳帧信息           
      if (DEBUG_JANK) {                
        Log.d(TAG, "Missed vsync by " + (jitterNanos * 0.000001f) + " ms "                        
                  + "which is more than the frame interval of "                        
                  + (mFrameIntervalNanos * 0.000001f) + " ms!  "                        
                  + "Skipping " + skippedFrames + " frames and setting frame "                        
                  + "time to " + (lastFrameOffset * 0.000001f) + " ms in the past.");            
       }
       //修正偏差值,忽略偏差,为了后续更好地同步工作            
       frameTimeNanos = startNanos - lastFrameOffset;        
    }
    //若时间回溯,则不进行任何工作,等待下一个时钟信号的到来
    //这里为什么会发生时间回溯我没搞明白,大概是未知时钟错误引起?注释里说的maybe 好像不太对        
    if (frameTimeNanos < mLastFrameTimeNanos) {            
    if (DEBUG_JANK) {                
      Log.d(TAG, "Frame time appears to be going backwards.  May be due to a "                        
                + "previously skipped frame.  Waiting for next vsync.");            
   }
   //请求下一次时钟信号            
   scheduleVsyncLocked();            
   return;        
  }
 //记录当前frame信息,intendedFrameTimeNanos(底层上传的vsync时间戳)
 //frameTimeNanos(真实的vsync时间戳,可能往后推几个vsync)
 mFrameInfo.setVsync(intendedFrameTimeNanos,frameTimeNanos);        
 mFrameScheduled = false;
 //记录上一次frame开始时间,修正后的        
 mLastFrameTimeNanos = frameTimeNanos;    
 }    
  try {
    //执行相关callBack        
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");        
    mFrameInfo.markInputHandlingStart();        
    doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);        
    mFrameInfo.markAnimationsStart();        
    doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);        
    mFrameInfo.markPerformTraversalsStart();        
    doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);        
    doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);    
  } finally {        
    Trace.traceEnd(Trace.TRACE_TAG_VIEW);    
  }    
  if (DEBUG_FRAMES) {        
    final long endNanos = System.nanoTime();        
    Log.d(TAG, "Frame " + frame + ": Finished, took "                
              + (endNanos - startNanos) * 0.000001f + " ms, latency "                
              + (startNanos - frameTimeNanos) * 0.000001f + " ms.");    
   }
}

4.3 doCallbacks

void doCallbacks(int callbackType, long frameTimeNanos) {
   CallbackRecord callbacks;
   ...
   callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                    now / TimeUtils.NANOS_PER_MS);
   ...
    Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
            for (CallbackRecord c = callbacks; c != null; c = c.next) {
                if (DEBUG_FRAMES) {
                    Log.d(TAG, "RunCallback: type=" + callbackType
                            + ", action=" + c.action + ", token=" + c.token
                            + ", latencyMillis=" + (SystemClock.uptimeMillis() - c.dueTime));
                }
                c.run(frameTimeNanos);
            }

private static final class CallbackRecord {
        public CallbackRecord next;
        public long dueTime;
        public Object action; // Runnable or FrameCallback
        public Object token;

        public void run(long frameTimeNanos) {
            if (token == FRAME_CALLBACK_TOKEN) {
                ((FrameCallback)action).doFrame(frameTimeNanos);
            } else {
                ((Runnable)action).run();
            }
        }
 }

1)callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked( now / TimeUtils.NANOS_PER_MS);得到执行时间在当前时间之前的所有CallBack,保存在单链表中。每种类型的callback按执行时间先后顺序排序分别存在一个单链表里面。为了保证当前callback执行时新post进来的callback在下一个frame时才被执行,这个地方extractDueCallbacksLocked会将需要执行的callback和以后执行的callback断开变成两个链表,新post进来的callback会被放到后面一个链表中。当前frame只会执行前一个链表中的callback,保证了在执行callback时,如果callback中Post相同类型的callback,这些新加的callback将在下一个frame启动后才会被执行。
2)接下来,看一大段注释,如果类型是CALLBACK_COMMIT,并且当前frame渲染时间超过了两个时钟周期,则将当前提交时间修正为上一个垂直同步信号时间。为了保证下一个frame的提交时间和当前frame时间相差为一且不重复。
这个地方注释挺难看懂,实际上这个地方CALLBACK_COMMIT是为了解决ValueAnimator的一个问题而引入的,主要是解决因为遍历时间过长导致动画时间启动过长,时间缩短,导致跳帧,这里修正动画第一个frame开始时间延后来改善,这时候才表示动画真正启动。为什么不直接设置当前时间而是回溯一个时钟周期之前的时间呢?看注释,这里如果设置为当前frame时间,因为动画的第一个frame其实已经绘制完成,第二个frame这时候已经开始了,设置为当前时间会导致这两个frame时间一样,导致冲突。详细情况请看官方针对这个问题的修改(https://link.jianshu.com/?t=https://android.googlesource.com/platform/frameworks/base/+/c42b28d%5E!/);
3)最重要的是在CallbackRecord的run方法中根据token决定是走doFrame还是走runnable的run,看下面的postFrameCallback方法可以知道答案,对应前面自定的Callback时回调它的doFrame函数。

public void postFrameCallback(FrameCallback callback) {
    postFrameCallbackDelayed(callback, 0);
}

public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
    if (callback == null) {
       throw new IllegalArgumentException("callback must not be null");
    }

    postCallbackDelayedInternal(CALLBACK_ANIMATION,
           callback, FRAME_CALLBACK_TOKEN, delayMillis);
}

4.4 performTraversals

Choreographer doCallbacks -> TraversalRunnable run -> ViewRootImpl doTraversal -> ViewRootImpl performTraversal -> performDraw

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容