深入浅出 Android 屏幕刷新原理

code小生 一个专注大前端领域的技术平台公众号回复Android加入安卓技术群

作者:Geekholt
链接:https://www.jianshu.com/p/0a54aa33ba7d
声明:本文已获Geekholt授权发表,转发等请联系原作者授权

目录

前言

现在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信号(就是一个发起屏幕刷新的信号)发出的时间比较短暂,导致当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(),对于这块不是很清楚的具体可以看https://www.jianshu.com/p/c5df0ac39e01,所以我们接下来分析一下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的绘制流程不是本文的重点,感兴趣的可以看https://www.jianshu.com/p/3366e4bec7ce

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

相关阅读

Android 搜索结果显示高亮(有数据滑动底部自动刷新)
Android多屏幕适配之字体大小、行间距和字间距
Android 手机 全面屏(18:9屏幕)适配指南
ImageView scaleType 各种不同效果解析
Android 通过Java sdk的方式接入OpenCv

如果你有写博客的好习惯

欢迎投稿

点个在看,小生感恩❤️

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值