9

深入浅出Android屏幕刷新原理 - 简书

 3 years ago
source link: https://www.jianshu.com/p/0a54aa33ba7d?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

深入浅出Android屏幕刷新原理

42020.04.15 13:38:09字数 2,783阅读 10,121

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

webp
webp

现在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的作用、同步消息屏障,不同的地方出了问题都可能引起丢帧,所以了解这些细节有助于我们更好的排查项目开发过程中的问题,最后,来梳理一下屏幕刷新的流程图


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK