细品Choreographer

今天我们来品一品Choreographer,这个东西翻译成中文是 编舞师 的意思,为啥要这么叫呢?品完你就知道了

开胃菜:初识Choreographer

Choreographer的由来——黄油计划

引入 Vsync 之前的 Android 版本,渲染一帧相关的Message中间是没有间隔的,上一帧绘制完,下一帧的 Message 紧接着就开始被处理。这样的问题就是,帧率不稳定,可能高也可能低,如果在该处理绘制的时候CPU去处理其他任务了,就会造成丢帧,会有很明显的卡顿感,如下图👇


choreographer_1.png

为了解决UI卡顿的问题,Google 在 2012 年的 I/O 大会上宣布了 Project Butter 黄油计划,并且在 Android 4.1 中正式开启了这个机制。黄油计划 对 Android Display 系统进行了重构,引入了三个核心元素,即 VSYNC、Triple Buffer 和 Choreographer,其主要目的就是提供一个稳定的帧率输出机制,让软件层和硬件层可以以共同的频率一起工作。👇


choreographer_2.png

另外,Android 在 4.1 还对 Handler 机制进行了略微改造,使之支持 Asynchronous Message(异步消息) 和 Synchronization Barrier(同步屏障)。

Choreographer的职能

Choreographer的引入,主要是配合Vsync,给上层App的渲染提供一个稳定的绘制处理的时机,也就是Vsync到来的时候。Choreographer可以接收系统的VSync信号,统一管理应用的输入、动画和绘制等任务的执行时机。Android 的 UI 绘制任务将在它的统一指挥下,井然有序的完成。这就是引入 Choreographer的主要作用,业内一般通过它来监控应用的帧率。

Choreographer的踪迹

我们在使用systrace的时候,很容易发现Choreographer的踪迹👇:


choreographer_3.png

通过trace我们发现:input事件的处理和绘制流程traversal都是由Choreographer来触发的,我们接下来从源码的角度来看一下它的工作流程

主菜:源码分析

Choreographer的初始化

和Choreographer关系最为密切的就是ViewRootImpl了,在ViewRootImpl的构造函数中是通过Choreographer.getInstance()来获取Choreographer实例的

public static Choreographer getInstance() {
    return sThreadInstance.get();
}

private static final ThreadLocal<Choreographer> sThreadInstance =
        new ThreadLocal<Choreographer>() {
    @Override
    protected Choreographer initialValue() {
        Looper looper = Looper.myLooper();
        if (looper == null) {
            //当前线程必须要有Looper
            throw new IllegalStateException("The current thread must have a looper!");
        }
        return new Choreographer(looper, VSYNC_SOURCE_APP);
    }
};

可以看到,Choreographer是由ThreadLocal管理的,每个线程一个单利,而且这个线程必须要有Looper,因为Choreographer申请和接收VSync信号需要依赖native层的Looper。接下来,我们去看看它的构造方法:

private Choreographer(Looper looper, int vsyncSource) {
    mLooper = looper;
    mHandler = new FrameHandler(looper);
    mDisplayEventReceiver = USE_VSYNC   //Android 4.1之后默认为true
            ? new FrameDisplayEventReceiver(looper, vsyncSource)
            : null;
    mLastFrameTimeNanos = Long.MIN_VALUE;
    //Nanos 纳秒 ,1秒 = 1000毫秒 = 1000000微秒 = 1000000000纳秒
    //一帧的时间,出于计算精度的考虑,精确到纳秒
    mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());    //getRefreshRate()获取屏幕刷新率
    
    //mCallbackQueue是一个容量为4的数组,每一个元素内部维护了一个callback链表,4种事件就是通过这4个链表来维护的。链表是按照执行时间的先后顺序排序
    mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
    for (int i = 0; i <= CALLBACK_LAST; i++) {
        mCallbackQueues[i] = new CallbackQueue();
    }
}

在初始化的过程中,除了一些变量的赋值操作以外,有两个关键步骤:
1.创建了一个FrameDisplayEventReceiver,这个东西是用来申请和接收Vsync信号的
2.初始化CallbackQueue数组,容量是4,对应4种callback类型(以前是3种,在Android 6.0的时候添加的第4种)每个CallbackQueue内部维护一个链表,按照时间先后排序。CallBack类型分别为:

//输入回调,最先调用
public static final int CALLBACK_INPUT = 0;

//动画回调,traversals之前调用
public static final int CALLBACK_ANIMATION = 1;

//traversals回调,处理布局和绘制。 在处理完所有其他异步消息之后执行
public static final int CALLBACK_TRAVERSAL = 2;

//提交回调,处理当前帧绘制后的操作,traversals之后执行。Android 6.0添加的
public static final int CALLBACK_COMMIT = 3;

CallbackQueue

Choreographer初始化的时候有一个重要的步骤是初始化CallbackQueue,我们来简单的说一下CallbackQueue,这里就不贴代码了,感兴趣的朋友可以自己去看一下

  • 每个callback都会被封装成一个CallbackRecord,CallbackRecord以链表的方式储存。CallbackRecord内部记录了链表的next节点、callback的执行时间、callback的执行行为(runnable.run或者doFrame回调)
  • CallbackQueue内部持有着CallbackRecord链表的头元素
  • CallbackQueue有三个方法:
    1. extractDueCallbacksLocked:取出满足时间条件的callBack,返回一个链表
    2. addCallbackLocked:添加一个callback,按照执行时间先后排序
    3. removeCallbacksLocked 删除callback

postCallback

postCallback是Choreographer核心逻辑的入口,ViewRootImpl与Choreographer的交互主要就是通过postCallback。接下来我们从postCallback入手,看一看Choreographer核心逻辑的流程

public void postCallback(int callbackType, Runnable action, Object token) {
    postCallbackDelayed(callbackType, action, token, 0);
}

public void postCallbackDelayed(int callbackType,
        Runnable action, Object token, long delayMillis) {
    if (action == null) {
        throw new IllegalArgumentException("action must not be null");
    }
    if (callbackType < 0 || callbackType > CALLBACK_LAST) {
        throw new IllegalArgumentException("callbackType is invalid");
    }
    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}

private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    if (DEBUG_FRAMES) {
        Log.d(TAG, "PostCallback: type=" + callbackType
                + ", action=" + action + ", token=" + token
                + ", delayMillis=" + delayMillis);
    }
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        //callBack入队,队列按照dueTime的时间先后顺序排列
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        if (dueTime <= now) {
            //立即执行
            scheduleFrameLocked(now);
        } else {
            //延时执行
            Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
            msg.arg1 = callbackType;
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, dueTime);
        }
    }
}

private final class FrameHandler extends Handler {
    public FrameHandler(Looper looper) {
        super(looper);
    }
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            ...
            case MSG_DO_SCHEDULE_CALLBACK:
                doScheduleCallback(msg.arg1);
                break;
        }
    }
}

void doScheduleCallback(int callbackType) {
    synchronized (mLock) {
        if (!mFrameScheduled) {
            final long now = SystemClock.uptimeMillis();
            //判断这个callBack是否被提前remove
            if (mCallbackQueues[callbackType].hasDueCallbacksLocked(now)) {
                scheduleFrameLocked(now);
            }
        }
    }
}

postCallback最终会调用到postCallbackDelayedInternal,在postCallbackDelayedInternal中首先会将CallBack放入对应的队列,然后判断是否需要立即执行,如果需要立即执行就直接调用scheduleFrameLocked,如果需要延时执行,就通过handler发送一个延时异步消息,最终也是执行scheduleFrameLocked,所以我们接下来看一下scheduleFrameLocked

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {     //标记位,保证在一次VSync周期内不会重复的去申请下一个VSync信号,这个标记位会在doFrame时清除
        mFrameScheduled = true;
        if (USE_VSYNC) {        //使用垂直同步,Android 4.1后默认开启
            if (DEBUG_FRAMES) {
                Log.d(TAG, "Scheduling next frame on vsync.");
            }
            // 如果在 UI 线程上运行,则立即调度 vsync,
            // 否则尽快从 UI 线程发布消息调度 vsync。
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            //不使用垂直同步,通过消息机制模拟周期信号
            final long nextFrameTime = Math.max(
                    mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
            if (DEBUG_FRAMES) {
                Log.d(TAG, "Scheduling next frame in " + (nextFrameTime - now) + " ms.");
            }
            Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
            msg.setAsynchronous(true);
            mHandler.sendMessageAtTime(msg, nextFrameTime);
        }
    }
}

这里的逻辑比较简单,如果当前在主线程,就直接去申请下一个VSync信号,如果不在主线程,就通过Handler发送异步消息去申请VSync信号,如果没有开启VSync,就模拟VSync信号。另外,有一个mFrameScheduled变量用来避免重复的申请VSync信号,因为这个流程是从postCallBack走进来的,在绘制流程中,会执行多次postCallBack,让各种各样的CallBack入队,不能每次postCallBack都申请一次VSync。
调用scheduleVsyncLocked之后,会通过初始化时创建的FrameDisplayEventReceiver去向native层申请下一次VSync信号,当下一个VSync信号来临时,就会调用FrameDisplayEventReceiver.onVsync()

private final class FrameDisplayEventReceiver extends DisplayEventReceiver
        implements Runnable {
    private boolean mHavePendingVsync;
    private long mTimestampNanos;
    private int mFrame;
    public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
        super(looper, vsyncSource);
    }
    @Override
    public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
        ...
        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);   //因为FrameDisplayEventReceiver实现了Runnable,所以这里传入this,接下来就会被执行下面的run方法
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }
    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

在接收到Vsync后,将Receiver本身作为runnable传入异步消息中,并使用handler发送这个异步消息,最终执行的就是doFrame()方法了。需要注意的是,在这里仅仅是用handler发送消息到MessageQueue中,不一定是立刻执行,需要等待MessageQueue中排在前面的消息执行完毕。
我们通过一张流程图来小结一下👇:


choreographer_4.png

流程看起来比较复杂,但是有很多可以简化的地方:

  • 我们假设都是在主线程调用,省略掉handler的调度
  • Android 4.1后,默认开启VSycn,所以省略掉非VSync的情况

我们最终可以得到一个精简版的流程图👇:

choreographer_5.png

通过上图,相信大家已经非常清楚postCallback做了什么事了,那我们接下来继续往下看

doFrame

void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            // 只有申请vsync信号的时候mFrameScheduled才会置为true,doFrame执行后会被置为false。
            // 保证一次申请信号只执行一次绘制流程
            return; // no work to do
        }
        long intendedFrameTimeNanos = frameTimeNanos;   //预期的帧时间
        startNanos = System.nanoTime();
        final long jitterNanos = startNanos - frameTimeNanos;   //抖动时间(当前时间和预期时间差)
        //如果时间差超过了一帧的时间,修正帧时间,修正成距离最近的一帧的时间
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
                Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
                        + "The application may be doing too much work on its main thread.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            
            frameTimeNanos = startNanos - lastFrameOffset;
        }
        if (frameTimeNanos < mLastFrameTimeNanos) {
            // 帧时间似乎倒退了。 可能是由于先前跳过的帧。 等待下一个vsync。
            scheduleVsyncLocked();
            return;
        }
        //FrameInfo初始化,设置:预期的vsync时间,抖动调整后的vsync时间
        mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
        mFrameScheduled = false;
        mLastFrameTimeNanos = frameTimeNanos;
    }
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
        
        //开始处理输入事件,FrameInfo记录
        mFrameInfo.markInputHandlingStart();
        doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
        //开始处理动画,FrameInfo记录
        mFrameInfo.markAnimationsStart();
        doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
        //开始执行 ViewRootImpl#performTraversals(),FrameInfo记录
        mFrameInfo.markPerformTraversalsStart();
        doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
        //处理commit回调
        doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
    } finally {
        AnimationUtils.unlockAnimationClock();
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

doFrame中首先是对帧时间进行了一些修正,初始化了FrameInfo,这个FrameInfo也是Android 6.0引入的,是用来记录doFrame每个步骤的时间的👇:

Pos Name Desc
0 FLAGS 绘制过程中的额外标志位,例如FLAG_WINDOW_LAYOUT_CHANGED
1 INTENDED_VSYNC 预期的vsync时间,不受抖动调整
2 VSYNC 抖动调整的垂直同步时间,这是用作动画和绘图系统的输入
3 OLDEST_INPUT_EVENT 本次处理的输入事件队列中,最早的那个事件的时间
4 NEWEST_INPUT_EVENT 本次处理的输入事件队列中,最后的那个事件的时间
5 HANDLE_INPUT_START 输入事件开始处理的时间
6 ANIMATION_START 动画开始的时间
7 PERFORM_TRAVERSALS_START ViewRootImpl#performTraversals()的开始时间
8 DRAW_START View:draw()开始时间

我们通过adb命令adb shell dumpsys gfxinfo <应用包名> framestats就可以看到相应的日志输出,这个命令会抓取过去2秒内的所有帧信息。

准备工作做完后,就开始处理callback链表,处理顺序是:input、animation、traversal、commit。我们来看一下doCallbacks的实现:

void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
        final long now = System.nanoTime();
        //取出所有满足时间的callBack,返回的是一个链表头
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;
      
        if (callbackType == Choreographer.CALLBACK_COMMIT) {
            final long jitterNanos = now - frameTimeNanos;
            Trace.traceCounter(Trace.TRACE_TAG_VIEW, "jitterNanos", (int) jitterNanos);
            // 如果从收到VSync信号到处理Commit回调的时间间隔超过2帧,修正 mLastFrameTimeNanos
            if (jitterNanos >= 2 * mFrameIntervalNanos) {
                final long lastFrameOffset = jitterNanos % mFrameIntervalNanos
                        + mFrameIntervalNanos;
                if (DEBUG_JANK) {
                    Log.d(TAG, "Commit callback delayed by " + (jitterNanos * 0.000001f)
                            + " ms which is more than twice the frame interval of "
                            + (mFrameIntervalNanos * 0.000001f) + " ms!  "
                            + "Setting frame time to " + (lastFrameOffset * 0.000001f)
                            + " ms in the past.");
                    mDebugPrintNextFrameTimeDelta = true;
                }
                frameTimeNanos = now - lastFrameOffset;
                mLastFrameTimeNanos = frameTimeNanos;
            }
        }
    }
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            //依次执行每个Callback
            c.run(frameTimeNanos);
        }
    } finally {
        synchronized (mLock) {
            mCallbacksRunning = false;
            do {
                //Callback用完了,回收
                final CallbackRecord next = callbacks.next;
                recycleCallbackLocked(callbacks);
                callbacks = next;
            } while (callbacks != null);
        }
        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    }
}

核心逻辑很简单,从CallbackQueue中取出所有满足时间的callback,是一个链表,然后遍历链表依次调用callback的run方法,执行callback中的Runnable或者FrameCallback。我们还是画一个流程图👇


choreographer_6.png

小结

到此,Choreographer在一个VSync信号周期内的工作就完成了,我们来小结一下:

  • postCallback会触发VSync信号的申请,同时会将callback入队,当VSync信号到来时,会通过Handler调度执行doFrame
  • doFrame负责从各个队列中取出符合时间的callback去执行,执行顺序依次是input、animation、traversal、commit
  • native层VSync的接收是通过Looper实现的,VSync信号到来后执行doFrame也是通过Handler.post,所以,当主线程有耗时操作时,将直接影响到VSync信号的接收和doFrame的调度,从而引发丢帧
  • 系统会不停的产生VSync信号,但是应用不会每次都接到VSync信号,当应用需要接收的时候,才去申请下一个VSync信号

总流程图👇


choreographer_7.png

总结

Choreographer的分析到这里就结束了,可以看出来,Choreographer掌控着UI刷新的节奏,这也是它为什么被叫做编舞师:ViewRootImpl就像舞蹈演员,Choreographer负责告诉它什么时候应该做什么动作。

在业内Choreographer常被用来做一些帧率监控的工作,例如Matrix的TraceCanary模块中的FrameTracer,就是基于Choreographer的原理实现的,感兴趣的朋友可以去看一下源码

ok,Choreographer我们已经品完了,希望大家有所收获!溜了溜了。。。

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