深入浅出Android屏幕刷新原理

如需转载请评论或简信,并注明出处,未经允许不得转载

目录

前言

现在Android的应用界面越来越复杂,很多时候页面中还有各种动画,所以页面卡顿、掉帧等问题就随之而来,所以就想研究一下屏幕刷新的原理,以便于更快的定位和解决问题

基本概念

Android的屏幕刷新中涉及到最重要的三个概念(为便于理解,这里先做简单介绍)

CPU:执行应用层的measure、layout、draw等操作,绘制完成后将数据提交给GPU

GPU:进一步处理数据,并将数据缓存起来

屏幕:由一个个像素点组成,以固定的频率(16.6ms,即1秒60帧)从缓冲区中取出数据来填充像素点

总结一句话就是:CPU 绘制后提交数据、GPU 进一步处理和缓存数据、最后屏幕从缓冲区中读取数据并显示

我们开发过程中主要关心CPU绘制部分,对GPU和屏幕基本不用关心。所以,看到这里,有的人可能就会想说,我对view的绘制流程(measure、layout、draw)已经非常熟悉,至于GPU和屏幕,和我也没有太大关系吧。其实这里面还有更多的细节值得我们去探索,了解和掌握了这些细节,有助于我们解决一些实际开发过程中的问题,我们不妨一步步往下看

双缓冲机制

看完上面的流程图,我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制),如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢?

这个其实和我们平时使用代码管理工具Git的一些思路有相似之处,首先我们有一个master分支,对应线上版本的代码,当有新的需求来的时候,我们往往不会在master分支上直接进行开发,都会拉出一个新的分支,比如develop分支,在develop分支上开发新需求,等开发完成测试通过后才会合并到master分支

所以,在屏幕刷新中,Android系统引入了双缓冲机制。GPU只向Back Buffer中写入绘制数据,且GPU会定期交换Back Buffer和Frame Buffer,也就是让Back Buffer 变成Frame Buffer交给屏幕进行绘制,让原先的Frame Buffer变成Back Buffer进行数据写入。交换的频率也是60次/秒,这就与屏幕的刷新频率保持了同步

虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,CPU并不能保证在16.6ms内就完成绘制数据的计算,所以这里系统又做了一个处理

当你的应用正在往Back Buffer中填充数据时,系统会将Back Buffer锁定。如果到了GPU交换两个Buffer的时间点,你的应用还在往Back Buffer中填充数据,GPU会发现Back Buffer被锁定了,它会放弃这次交换

这样做的后果就是手机屏幕仍然显示原先的图像,这就是我们常常说的丢帧,所以为了避免丢帧的发生,我们就要尽量减少布局层级,减少不必要的View的invalidate调用,减少大量对象的创建(GC也会占用CPU时间)等等。对这方面有兴趣的可以看我的性能优化专题下的文章

Choreographer

我们看下面这张图,这里已经是基于双缓冲机制,且应用层的优化已经做得非常好,绘制时间均少于16.6ms,但依然出现了丢帧,为什么呢?

原因是第2帧虽然绘制时间少于16.6ms,但是绘制开始的时间距离vsync信号(就是一个发起屏幕刷新的信号,Vertical Synchronization的缩写)发出的时间比较短暂,导致当vsync信号来的时候,第2帧还没有绘制完成,所以Back Buffer依然是锁定的状态,也就出现了丢帧

如果我们可以保证每次绘制开始的时间和vsync信号发起的时间一致(如下图所示),是不是就可以解决这个问题呢?

Android在每一帧中实际上只是在完成三个操作,分别是输入(Input)动画(Animation)绘制(Draw)。在Android4.1(API 16)之后,Android系统开始加入Choreographer这个类,这个类名翻译过来是“舞蹈指导”,字面上的意思就是指挥以上三个UI操作一起完成一支舞蹈。这个类就可以解决vsync和绘制不同步的问题,其实它的原理用一句话总结就是往Choreographer里发一个消息,最快也要等到下一个vsync信号来的时候才会开始处理消息

下面我们通过源码分析来看看Choreographer的实现原理

Activity中的布局首次绘制,以及每次调用View 的 invalidate() 时,都会调用到ViewRootImp#requestLayout(),对于这块不是很清楚的具体可以看最全的View绘制流程(上)— Window、DecorView、ViewRootImp的关系,所以我们接下来分析一下ViewRootImp#requestLayout()里面做了什么

ViewRootImp#requestLayout()

public void requestLayout() {
    if (!mHandlingLayoutInLayoutRequest) {
        //检查是否是主线程,不然会抛出异常
        checkThread();
        mLayoutRequested = true;
        scheduleTraversals();
    }
}

ViewRootImp#scheduleTraversals()

如果在同一帧中出现多次requestLayout()调用,其实最终也只会绘制一次,为什么呢?我们可以看到下面有个mTraversalScheduled标志位,稍后我们可以看看这个标志位是哪里被置为false的

void scheduleTraversals() {
    if (!mTraversalScheduled) {         
        mTraversalScheduled = true;
        //添加同步消息屏障,这个方法也比较关键,这里先不关心,我们说完Choreographer再分析
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //向Choreographer中发送消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //...
    }
}

Choreographer#postCallbackDelayedInternal()

mChoreographer.postCallback()接着会调用这个方法

  private void postCallbackDelayedInternal(int callbackType,
            Object action, Object token, long delayMillis) {
        synchronized (mLock) {
            final long now = SystemClock.uptimeMillis();
            final long dueTime = now + delayMillis;
            //将消息以当前的时间戳放进mCallbackQueue 队列里
            mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);

            if (dueTime <= now) {
                //如果没有设置消息延时,直接执行
                scheduleFrameLocked(now);
            } else {
                //消息延时,但是最终依然会调用scheduleFrameLocked
                Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
            }
        }
    }

Choreographer#scheduleFrameLocked()

private void scheduleFrameLocked(long now) {
    if (!mFrameScheduled) {
        mFrameScheduled = true;
        if (USE_VSYNC) {
            if (isRunningOnLooperThreadLocked()) {
                //如果当前线程是Choreographer的工作线程,我理解就是主线程
                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);
            }
    }
}

接下来最终会调用到一个native方法

private void scheduleVsyncLocked() {
    mDisplayEventReceiver.scheduleVsync();
}
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 {
        nativeScheduleVsync(mReceiverPtr);
    }
}
@FastNative
private static native void nativeScheduleVsync(long receiverPtr);

native方法我们在Android Studio中不能直接查看,这里我们换一种思路。前面Choreographer#postCallbackDelayedInternal()方法中,我们看到了将消息以当前的时间戳放进队列里,那消息什么时候被取出来执行呢?

CallbackQueue

private final class CallbackQueue {
    private CallbackRecord mHead;

    public boolean hasDueCallbacksLocked(long now) {
        return mHead != null && mHead.dueTime <= now;
    }

    //这就是取出消息的方法
    public CallbackRecord extractDueCallbacksLocked(long now) {
        CallbackRecord callbacks = mHead;
        if (callbacks == null || callbacks.dueTime > now) {
            return null;
        }

        CallbackRecord last = callbacks;
        CallbackRecord next = last.next;
        while (next != null) {
            if (next.dueTime > now) {
                last.next = null;
                break;
            }
            last = next;
            next = next.next;
        }
        mHead = next;
        return callbacks;
    }

    //添加消息
    public void addCallbackLocked(long dueTime, Object action, Object token) {...}

    //删除消息
    public void removeCallbacksLocked(Object action, Object token) {...}
}

跟踪代码发现,这个CallbackQueue#extractDueCallbacksLocked()会被Choreographer#doCallbacks()调用,Choreographer#doCallbacks()又会被Choreographer#doFrame()调用,最终我们跟到了FrameDisplayEventReceiver

FrameDisplayEventReceiver

因为上面的native方法我们没有跟进去分析,担心给大家绕晕了,我们会用一个新的章节来分析native层做的事情,这里先直接给出结论

nativeScheduleVsync()会向SurfaceFlinger注册Vsync信号的监听,VSync信号由SurfaceFlinger实现并定时发送,当Vsync信号来的时候就会回调FrameDisplayEventReceiver#onVsync(),这个方法给发送一个带时间戳Runnable消息,这个Runnable消息的run()实现就是FrameDisplayEventReceiver# run(), 接着就会执行doFrame()

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) {
        if (builtInDisplayId != SurfaceControl.BUILT_IN_DISPLAY_ID_MAIN) {
            scheduleVsync();
            return;
        }
        long now = System.nanoTime();
        if (timestampNanos > now) {
            timestampNanos = now;
        }

        if (mHavePendingVsync) {
        } else {
            mHavePendingVsync = true;
        }

        mTimestampNanos = timestampNanos;
        mFrame = frame;
        Message msg = Message.obtain(mHandler, this);
        //设置异步消息
        msg.setAsynchronous(true);
        mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
    }

    @Override
    public void run() {
        mHavePendingVsync = false;
        doFrame(mTimestampNanos, mFrame);
    }
}

doFrame()会计算当前时间与时间戳的间隔,间隔越大表示这一帧处理的时间越久,如果间隔超过一个周期,就会去计算跳过了多少帧,并打印出一个日志,这个日志我想很多人可能都见过

Log.i(TAG, "Skipped " + skippedFrames + " frames!  "
        + "The application may be doing too much work on its main thread.");

最终doFrame()会从mCallbackQueue 中取出消息并按照时间戳顺序调用mTraversalRunnablerun()函数,mTraversalRunnable就是最初被加入到Choreographer中的Runnable()

//ViewRootImp 
mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);

TraversalRunnable

doTraversal()中就会开始我们View的绘制流程,View的绘制流程不是本文的重点,感兴趣的可以看最全的View绘制流程(下)— Measure、Layout、Draw

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

ViewRootImp#doTraversal()

void doTraversal() {
    if (mTraversalScheduled) {
        mTraversalScheduled = false;
        //移除同步消息屏障
        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

        if (mProfile) {
            Debug.startMethodTracing("ViewAncestor");
        }

        performTraversals();

        if (mProfile) {
            Debug.stopMethodTracing();
            mProfile = false;
        }
    }
}

到此为止,从触发绘制到屏幕真正开始绘制的过程就基本讲完了,但是这里还有最后一个细节没有进行分析

同步消息屏障

还记不记得前面说有mHandler.getLooper().getQueue().postSyncBarrier()这个方法还没有进行分析,这个方法的作用是什么呢?

void scheduleTraversals() {
    if (!mTraversalScheduled) {         
        mTraversalScheduled = true;
        //☆
        mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        //向Choreographer中发送消息
        mChoreographer.postCallback(
                Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
        //...
    }
}

我们知道,Android是基于消息机制的,每一个操作都是一个Message,如果在触发绘制的时候,消息队列中还有很多消息没有被执行,那是不是意味着要等到消息队列中的消息执行完成后,绘制消息才能被执行到,那么依然无法保证Vsync信号和绘制的同步,所以依然可能出现丢帧的现象

还记不记得我们之前在Choreographer#scheduleFrameLocked()FrameDisplayEventReceiver#onVsync()中提到,我们会给与Message有关的绘制请求设置成异步消息(msg.setAsynchronous(true)),为什么要这么做呢?这时候MessageQueue#postSyncBarrier()就发挥它的作用了,简单来说,它的作用就是一个同步消息屏障,能够把我们的异步消息(也就是绘制消息)的优先级提到最高

MessageQueue#postSyncBarrier()

主线程的 Looper 会一直循环调用 MessageQueuenext() 来取出队头的 Message 执行,当 Message 执行完后再去取下一个。当 next() 方法在取 Message 时发现队头是一个同步屏障的消息时,就会去遍历整个队列,只寻找设置了异步标志的消息,如果有找到异步消息,那么就取出这个异步消息来执行,否则就让 next() 方法陷入阻塞状态。如果 next() 方法陷入阻塞状态,那么主线程此时就是处于空闲状态的,也就是没在干任何事。所以,如果队头是一个同步屏障的消息的话,那么在它后面的所有同步消息就都被拦截住了,直到这个同步屏障消息被移除,否则主线程就一直不会去处理同步屏障后面的同步消息

那这么同步屏障是什么时候被移除的呢?

其实我们就是在我们上面提到的ViewRootImp#doTraversal()方法中

总结

本文讲了屏幕刷新的基本原理,以及双缓冲机制、Choreographer的作用、同步消息屏障,不同的地方出了问题都可能引起丢帧,所以了解这些细节有助于我们更好的排查项目开发过程中的问题,最后,来梳理一下屏幕刷新的流程图

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

推荐阅读更多精彩内容