Choreographer原理及应用

一、Android屏幕刷新机制

1.帧、帧速率、刷新率

是指一个画面
帧速率(frame rate)指的是显卡1秒钟渲染好并发送给显示器多少张画面。
刷新率指的是显示器逐行扫描刷新的速度。以 60 Hz 刷新率的屏幕为例,就是1s会刷60帧,一帧需要1000 / 60 ,约等于16ms,这个速度快到普通人眼感受不到屏幕在扫描。

2.画面撕裂

数据离开CPU到达显示屏,中间经历比较关键的步骤:

1.从总线进入GPU:将CPU送来的数据送到北桥(简单理解成连接显卡等高速设备的),再送到GPU里面进行处理
2.将芯片处理完的数据送到显存,转化成模拟信号display显示。

因为图像绘制和屏幕读取这一帧数据使用的是一块Buffer,屏幕读取数据过程是无法确保这个Buffer不会被修改。由于屏幕是逐行扫描,它不会被打断仍然会继续上一行的位置扫描,当出现Buffer里有些数据根本没被显示器显示完就被重写了(即Buffer里的数据是来自不同帧的混合),这样就出现了画面撕裂的现象

3.双缓存

针对上面的问题关键:图像绘制和屏幕读取这一帧数据使用的是一块Buffer
可以想到的一种解决方案是:不让它们使用同一块Buffer,用两块让它们各自为战不就好了,这么想的思路确实是对的。分析下这个具体过程:
当图像绘制和屏幕显示有各自的Buffer后,GPU将绘制完的一帧图像写入到后缓存(Back Buffer),显示器显示的时候只会去扫描前缓存的数据(Frame Buffer),在显示器未扫描完一帧前,前缓存区内数据不改变,屏幕就只会显示一帧的数据,避免了撕裂。

目前Android的双缓冲是非常有效的,当GPU将一帧写入一个后缓冲的存储器,而存储器中的次级区域被称为帧缓冲,当写入下一帧时,它会开始填充后缓冲,而帧缓冲保持不变。此时刷新屏幕,它将使用帧缓冲(事先已经绘制好了的),而不是使用正在处于绘制状态的后缓冲,这就是VSYNC的作用。


VSYNC的作用_jpg.jpg

4.三缓存

双缓存虽然可以有效的避免画面的撕裂,但是当第1个 VSync 到来时GPU还在处理数据,这时缓冲区在处理数据B,被占用了,此时的VBlank阶段就无法进行缓冲区交换,屏幕依然显示前缓冲区的数据A。此时只有等到下一个信号到来时,此时 GPU 已经处理完了,那么才可以交换缓冲区,此时屏幕就会显示交互后缓冲区的数据B了

为了减小卡顿,后面再加一个Buffer(这里叫它中Buffer)参与,让添加的这个中Buffer和后Buffer交换,这样既不会影响到显示器读取前Buffer,又可以在后Buffer缓冲区不能处理时,让中Buffer来处理,可以适当缓解卡顿概率

二、Choreographer原理

1. View绘制流程

ActivityThread.handleResumeActivity()->WindowManagerImpl.addView()->WindowManagerGlobal.addView()->ViewRootImpl.setView()

ViewRootImpl.java
/**
* We have one child
*/
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {
        if (mView == null) {
            mView = view;
            ...

            //注释1 开始三大流程(测量、布局、绘制)
            requestLayout();
            ...
            //注释2 添加View到WindowManagerService,这里是利用Binder跨进程通信,调用Session.addToDisplay()
            //将Window添加到屏幕
            res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                    getHostVisibility(), mDisplay.getDisplayId(), mWinFrame,
                    mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
                    mAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel);
            ...
        }
    }
}

从ViewRootImpl.requestLayout()开始,既是View的首次绘制流程

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

requestLayout()会走到scheduleTraversals()方法

2. Choreographer 编舞者

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}
final TraversalRunnable mTraversalRunnable = new TraversalRunnable();

void scheduleTraversals() {
    //注释1 标记是否已经开始了,开始了则不再进入
    if (!mTraversalScheduled) {
        mTraversalScheduled = true;
        //注释2 同步屏障,保证绘制消息(是异步的消息)的优先级
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //注释3 监听VSYNC信号,下一次VSYNC信号到来时,执行给进去的mTraversalRunnable
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        ...
    }
}

void doTraversal() {
    if (mTraversalScheduled) {
        //标记已经完成
        mTraversalScheduled = false;
        //移除同步屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
        //开始三大流程 measure layout draw
        performTraversals();
        ...
    }
}

Choreographer 初始化,在ViewRootImpl的构造方法里面,我看到了它的初始化。

public ViewRootImpl(Context context, Display display) {
    ...
    ...
    //初始化Choreographer,从getInstance()方法名,看起来像是单例
    mChoreographer = Choreographer.getInstance();
    ...
}

现在我们来说一下Choreographer的postCallback(),也就是ViewRootImpl使用的地方

//Choreographer.java
//ViewRootImpl是使用的这个
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) {
    ...
    postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private final CallbackQueue[] mCallbackQueues;
private void postCallbackDelayedInternal(int callbackType,
        Object action, Object token, long delayMillis) {
    synchronized (mLock) {
        final long now = SystemClock.uptimeMillis();
        final long dueTime = now + delayMillis;
        //将mTraversalRunnable存入mCallbackQueues数组callbackType处的队列中
        mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
        
        //传入的delayMillis是0,这里dueTime是等于now的
        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);
        }
    }
}

通过调用scheduleVsyncLocked()来监听VSYNC信号,这个信号是由硬件发出来的,信号来了的时候才开始绘制工作。

//Choreographer.java
private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            //走这里

            // 如果当前线程是初始化Choreographer时的线程,直接申请VSYNC,否则立刻发送一个异步消息到初始化Choreographer时的线程中申请VSYNC
            if (isRunningOnLooperThreadLocked()) {
                scheduleVsyncLocked();
            } else {
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
                msg.setAsynchronous(true);
                mHandler.sendMessageAtFrontOfQueue(msg);
            }
        } else {
            //这里是未开启VSYNC的情况,Android 4.1之后默认开启
            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);
        }
    }
}

mDisplayEventReceiver是一个FrameDisplayEventReceiver,FrameDisplayEventReceiver继承自DisplayEventReceiver。在DisplayEventReceiver里面有一个方法scheduleVsync(),这个方法是用来注册监听VSYNC信号的,它是一个native方法,水平有限,暂不继续深入了。

当有VSYNC信号来临时,native层会回调DisplayEventReceiver的dispatchVsync方法

//Choreographer.java
private final FrameDisplayEventReceiver mDisplayEventReceiver;

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}

private final class FrameDisplayEventReceiver extends DisplayEventReceiver implements Runnable {
    ...
}

//DisplayEventReceiver.java
public void scheduleVsync() {
    if (mReceiverPtr == 0) {
        Log.w(TAG, "Attempted to schedule a vertical sync pulse but the display event "
                + "receiver has already been disposed.");
    } else {
        //注册监听VSYNC信号,会回调dispatchVsync()方法
        nativeScheduleVsync(mReceiverPtr);
    }
}

当收到VSYNC信号时,回调dispatchVsync方法,走到了onVsync方法,这个方法被子类FrameDisplayEventReceiver覆写了的

//DisplayEventReceiver.java
// Called from native code.
@SuppressWarnings("unused")
private void dispatchVsync(long timestampNanos, int builtInDisplayId, int frame) {
    onVsync(timestampNanos, builtInDisplayId, frame);
}
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
}

在onVsync()方法中,其实主要内容就是发个消息(应该是为了切换线程),然后执行run方法。而在run方法中,调用了Choreographer的doFrame方法。这个方法有点长,我们来理一下。

//Choreographer.java

//frameTimeNanos是VSYNC信号回调时的时间
void doFrame(long frameTimeNanos, int frame) {
    final long startNanos;
    synchronized (mLock) {
        if (!mFrameScheduled) {
            return; // no work to do
        }
        ...
        long intendedFrameTimeNanos = frameTimeNanos;
        startNanos = System.nanoTime();
        //jitterNanos为当前时间与VSYNC信号来时的时间的差值,如果Looper有很多异步消息等待处理(或者是前一个异步消息处理特别耗时,当前消息发送了很久才得以执行),那么处理当来到这里时可能会出现很大的时间间隔
        final long jitterNanos = startNanos - frameTimeNanos;
        
        //mFrameIntervalNanos是帧间时长,一般手机上为16.67ms
        if (jitterNanos >= mFrameIntervalNanos) {
            final long skippedFrames = jitterNanos / mFrameIntervalNanos;
            //想必这个日志大家都见过吧,主线程做了太多的耗时操作或者绘制起来特别慢就会有这个
            //这里的逻辑是当掉帧个数超过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.");
            }
            final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
            frameTimeNanos = startNanos - lastFrameOffset;
        }
        ...
    }

    try {
        AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);

        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 {
        AnimationUtils.unlockAnimationClock();
    }
}

从mCallbackQueues数组中找到callbackType对应的CallbackRecord,然后执行队列里面的所有元素(CallbackRecord)的run方法。然后也就是执行到了ViewRootImpl的scheduleTraversals时,postCallback传过来的mTraversalRunnable(是一个Runnable)

//Choreographer.java
void doCallbacks(int callbackType, long frameTimeNanos) {
    CallbackRecord callbacks;
    synchronized (mLock) {
        final long now = System.nanoTime();
        //根据callbackType取出相应的CallbackRecord
        callbacks = mCallbackQueues[callbackType].extractDueCallbacksLocked(
                now / TimeUtils.NANOS_PER_MS);
        if (callbacks == null) {
            return;
        }
        mCallbacksRunning = true;
        ...
    }
    try {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, CALLBACK_TRACE_TITLES[callbackType]);
        for (CallbackRecord c = callbacks; c != null; c = c.next) {
            //
            c.run(frameTimeNanos);
        }
    } finally {
        ...
    }
}
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 {
            //会走到这里来,因为ViewRootImpl的scheduleTraversals时,postCallback传过来的token是null。
            ((Runnable)action).run();
        }
    }
}

从doTraversal()开始就是View的三大流程(measure、layout、draw)了。Choreographer的使命也基本完成了。

//ViewRootImpl.java
final class TraversalRunnable implements Runnable {
    @Override
    public void run() {
        doTraversal();
    }
}

上面就是Choreographer的工作流程。总结一下就是

1. 从ActivityThread.handleResumeActivity开始,ActivityThread.handleResumeActivity()->WindowManagerImpl.addView()->WindowManagerGlobal.addView()->初始化ViewRootImpl->初始化Choreographer->ViewRootImpl.setView()
2. 在ViewRootImpl的setView中会调用requestLayout()->scheduleTraversals(),然后是建立同步屏障
3. 通过Choreographer线程单例的postCallback()提交一个任务mTraversalRunnable,这个任务是用来做View的三大流程的(measure、layout、draw)
4. Choreographer.postCallback()内部通过DisplayEventReceiver.nativeScheduleVsync()向系统底层注册VSYNC信号监听,当VSYNC信号来临时,会回调DisplayEventReceiver的dispatchVsync(),最终会通知FrameDisplayEventReceiver.onVsync()方法。
5. 在onVsync()中取出之前传入的任务mTraversalRunnable,执行run方法,开始绘制流程。

三、Choreographer应用

在了解了Choreographer的工作原理之后,我们来点实际的,将Choreographer这块的知识利用起来,拿来进行卡顿检测。

Choreographer 流畅度监测

通过设置Choreographer的FrameCallback,可以在每一帧被渲染的时候记录下它开始渲染的时间,这样在下一帧被处理时,我们可以根据时间差来判断上一帧在渲染过程中是否出现掉帧。Android中,每发出一个VSYNC信号都会通知界面进行重绘、渲染,每一次同步周期为16.6ms,代表一帧的刷新频率。每次需要开始渲染的时候都会回调doFrame(),如果某2次doFrame()之间的时间差大于16.6ms,则说明发生了UI有点卡顿,已经在掉帧了,拿着这个时间差除以16.6就得出了掉过了多少帧。

/**
 * 细化帧数
 * 适用于快速定位帧率监控
 */
public class ChoreographerHelper {
    private static final String TAG = "ChoreographerHelper";
    static long lastFrameTimeNanos = 0;

    public static void start() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
            Choreographer.getInstance().postFrameCallback(new Choreographer.FrameCallback() {
                @Override
                public void doFrame(long frameTimeNanos) {
                    //上次回调时间
                    if (lastFrameTimeNanos == 0) {
                        lastFrameTimeNanos = frameTimeNanos;
                        Choreographer.getInstance().postFrameCallback(this);
                        return;
                    }
                    long diff = (frameTimeNanos - lastFrameTimeNanos) / 1_000_000;
                    if (diff > 16.6f) {

                        //掉帧数
                        int droppedCount = (int) (diff / 16.6);
                        if (droppedCount > 2) {
                            Log.w(TAG, "UI线程超时(超过16ms)当前:" + diff + "ms" + " , 丢帧:" + droppedCount);
                        }
                    }
                    lastFrameTimeNanos = frameTimeNanos;
                    Choreographer.getInstance().postFrameCallback(this);
                }
            });
        }
    }
}

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

推荐阅读更多精彩内容