Android的屏幕刷新原理

Android的屏幕刷新中涉及到最重要的三个概念:

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

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

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

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

Android的屏幕刷新原理_第1张图片

 

我们很容易想到一个问题,屏幕是以16.6ms的固定频率进行刷新的,但是我们应用层触发绘制的时机是完全随机的(比如我们随时都可以触摸屏幕触发绘制)如果在GPU向缓冲区写入数据的同时,屏幕也在向缓冲区读取数据,会发生什么情况呢?

有可能屏幕上就会出现一部分是前一帧的画面,一部分是另一帧的画面,这显然是无法接受的,那怎么解决这个问题呢?

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

Android的屏幕刷新原理_第2张图片

虽然我们引入了双缓冲机制,但是我们知道,当布局比较复杂,或设备性能较差的时候,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依然是锁定的状态,也就出现了丢帧

Android的屏幕刷新原理_第3张图片

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

Android的屏幕刷新原理_第4张图片

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

追溯源码可以可以得知:

ViewRootImp#scheduleTraversals()

 mTraversalBarrier =mHandler.getLooper().getQueue().postSyncBarrier();//同步屏障

 mChoreographer.postCallback(
           Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);  ////向 Choreographer中发送消息

Choreographer#scheduleFrameLocked()

//发一条消息到主线程
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);///设置消息为异步消息,其实就是一个标志位
mHandler.sendMessageAtFrontOfQueue(msg);//插到消息队列头部,可以理解为设置最高优先级

FrameDisplayEventReceiver

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

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

ViewRootImp#doTraversal()

doTraversal()中就会开始我们View的绘制流程

void doTraversal() {

        //...

        mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);//移除同步消息屏障

}

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

 

 

你可能感兴趣的:(android,studio,页面刷新,安卓)