Android渲染机制基础认知
我们都知道,计算机是基于二进制数据流来进行工作的,而且又知道,计算机五大组成部分是各司其职的,其中屏幕就是专门来"渲染"图像的,既然要显示图像,肯定要有显示的数据,这些数据从哪来呢?答案就是cpu(「这里为了方便,把cpu、gpu、sf等统一称为cpu」),这些数据由cpu提供,cpu经过各种运算,将数据写入一块内存中,这块内存叫做「帧缓冲」,我们可以将帧缓冲理解为一个M*N矩阵,数据从上到下一行一行保存,显示器在显示的时候,从上到下逐行扫描,依次显示在屏幕上,我们把这样的一屏数据叫做「一帧」,当一帧数据渲染完后,就开始新一轮扫描,如果CPU「正好」(不正好后面再说)也把下一帧数据写入帧缓冲,那么就会显示下一帧画面,如此循环,我们就看到了不断变化的画面,也就是图像。这个过程很简单,但是实现起来却很难,具体有两点:
-
屏幕需要在16.7毫秒内绘制完一帧,因为根据研究,16.7ms正符合人类能觉察到卡顿的分割点,如果低于16.7ms,则可能感觉卡顿,高于16.7ms则没必要。
-
CPU需要在屏幕渲染完毕后,正好把下一帧数据写入帧缓冲。如果早了,那么屏幕上就会绘制一半上一帧的数据,一半下一帧的数据。比如:绘制到第一帧的a行时,cpu把下一帧数据送进来了,屏幕会接着从a+1行接着绘制,这样导致前a行是第一帧的数据,后面几行是第二帧的数据,在我们看来就是两张图片撕开各取一部分拼起来,这叫做「撕裂」。如果晚了,那么屏幕会在下一次继续绘制上一帧,导致画面没有变化,这样就会出现画面不变的情况,在我们看起来就是卡了,也叫做「卡顿」。所以,CPU和屏幕的这个交互时机很重要。
渲染流程线:UI对象—->CPU处理为多维图形,纹理 —–通过OpeGL ES接口调用GPU—-> GPU对图进行光栅化(Frame Rate ) —->硬件时钟(Refresh Rate)—-垂直同步—->投射到屏幕。
用一句话来概括一下Android应用程序显示的过程:Android应用程序调用SurfaceFlinger服务把经过测量、布局和绘制后的Surface渲染到屏幕上。
CPU: 中央处理器,它集成了运算,缓冲,控制等单元,包括绘图功能.CPU负责包括Measure,Layout,Record,Execute的计算操作,将对象处理为多维图形,纹理(Bitmaps、Drawables等都是一起打包到统一的纹理)。
GPU:一个类似于CPU的专门用来处理Graphics(图形)的处理器, 作用用来帮助加快栅格化操作,当然,也有相应的缓存数据(例如缓存已经光栅化过的bitmap等)机制。
OpenGL ES是手持嵌入式设备的3DAPI,跨平台的、功能完善的2D和3D图形应用程序接口API,有一套固定渲染管线流程。
DisplayList 在Android把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在DisplayList的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。
栅格化是将图片等矢量资源,转化为一格格像素点的像素图,显示到屏幕上,通俗的说所谓的栅格化就是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示,用来解决那些复杂的XML布局文件和标记语言,使之转化成用户能看懂的图像,但是这不是直接转换的,XML布局文件需要在CPU中首先转换为多边形或者纹理,然后再传递给GPU进行格栅化,对于栅格化,跟OpenGL有关,格栅化是一个特别费时的操作。
垂直同步VSYNC:让显卡的运算和显示器刷新率一致以稳定输出的画面质量。它告知GPU在载入新帧之前,要等待屏幕绘制完成前一帧。
屏幕刷新率(Hz):屏幕在一秒内刷新的次数,Android手机一般都是60Hz,也就是一秒刷新60次,当然也有高刷的,但是60Hz足矣。
SurfaceFlinger:Android系统服务,负责管理Android系统的帧缓冲区,即显示屏幕。
Surface:Android应用的每个窗口对应一个画布(Canvas),即Surface,可以理解为Android应用程序的一个窗口。
任何时候View中的绘制内容发生变化时,都会重新执行创建DisplayList,渲染DisplayList,更新到屏幕上等一 系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。
举个例子,当View的大小发生改变,DisplayList就会重新创建,然后再渲染,而当View发生位移,则DisplayList不会重新创建,而是执行重新渲染的操作.
所以View的渲染时间一般由Cpu测量Display List的时间+OpenGL渲染Display List所需要的时间+CPU等待GPU处理的时间共同组成。
Android应用程序的显示过程包含了两个部分(应用侧绘制、系统侧渲染)
Android目前有两种绘制模型:基于软件的绘制模型和硬件加速的绘制模型(从Android 3.0开始全面支持)。
在基于软件的绘制模型下,CPU主导绘图,视图按照两个步骤绘制:
1. 让View层次结构失效
2. 绘制View层次结构
当应用程序需要更新它的部分UI时,都会调用内容发生改变的View对象的invalidate()方法。无效(invalidation)消息请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。不幸的是,这种方法有两个缺点:
1. 绘制了不需要重绘的视图(与脏区域相交的区域)
2. 掩盖了一些应用的bug(由于会重绘与脏区域相交的区域)
注意:在View对象的属性发生变化时,如背景色或TextView对象中的文本等,Android系统会自动的调用该View对象的invalidate()方法。
在基于硬件加速的绘制模式下,GPU主导绘图,绘制按照三个步骤绘制:
1. 让View层次结构失效
2. 记录、更新显示列表
3. 绘制显示列表
这种模式下,Android系统依然会使用invalidate()方法和draw()方法来请求屏幕更新和展现View对象。但Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。另一个优化是,Android系统只需要针对由invalidate()方法调用所标记的View对象的脏区进行记录和更新显示列表。没有失效的View对象则能重放先前显示列表记录的绘制指令来进行简单的重绘工作。
使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,以便下次重用,然后再调用OpenGL完成绘制。
硬件加速提高了Android系统显示和刷新的速度,但它也不是万能的,它有三个缺陷:
1. 兼容性(部分绘制函数不支持或不完全硬件加速)
2. 内存消耗(OpenGL API调用就会占用8MB,而实际上会占用更多内存)
3. 电量消耗(GPU耗电)
Android应用程序在图形缓冲区中绘制好View层次结构后,这个图形缓冲区会被交给SurfaceFlinger服务,而SurfaceFlinger服务再使用OpenGL图形库API来将这个图形缓冲区渲染到硬件帧缓冲区中。
Android应用程序为了能够将自己的UI绘制在系统的帧缓冲区上,它们就必须要与SurfaceFlinger服务进行通信,
Android应用程序与SurfaceFlinger服务是运行在不同的进程中的,因此,它们采用某种进程间通信机制来进行通信。由于Android应用程序在通知SurfaceFlinger服务来绘制自己的UI的时候,需要将UI数据传递给SurfaceFlinger服务,例如,要绘制UI的区域、位置等信息。一个Android应用程序可能会有很多个窗口,而每一个窗口都有自己的UI数据,因此,Android系统的匿名共享内存机制就派上用场了。
每一个Android应用程序与SurfaceFlinger服务之间,都会通过一块匿名共享内存来传递UI数据。
但是单纯的匿名共享内存在传递多个窗口数据时缺乏有效的管理,所以匿名共享内存就被抽象为一个更上流的数据结构SharedClient,
在每个SharedClient中,最多有31个SharedBufferStack,每个SharedBufferStack都对应一个Surface,即一个窗口。这样,我们就可以知道为什么每一个SharedClient里面包含的是一系列SharedBufferStack而不是单个SharedBufferStack:一个SharedClient对应一个Android应用程序,而一个Android应用程序可能包含有多个窗口,即Surface。从这里也可以看出,一个Android应用程序至多可以包含31个窗口。
每个SharedBufferStack中又包含了N个缓冲区(<4.1 N=2; >=4.1 N=3),即显示刷新机制中即将提到的双缓冲和三重缓冲技术。
显示刷新机制
我们知道,撕裂是因为:cpu太快 从而导致 屏幕还没渲染完毕 就把正在渲染的数据 给覆盖掉了,那么我们可以限制cpu的速度吗?当然可以,但是不划算,因为这样就等于把cpu的长处给扼杀了,所以我们只要让cpu的数据不覆盖掉屏幕正在渲染的数据即可,也就是说,给cpu新来的数据提供一个存放点,而不是往帧缓冲里面写,这个存放点叫做「后缓冲(BackBuffer)」,相应的,「帧缓冲(FrameBuffer)也叫做前缓冲」,这样,cpu新来的数据就会放在后缓冲,而屏幕则继续从前缓冲取数据来渲染,等到后缓冲数据写入完了,前后缓冲的数据就会交换,屏幕此时读取的数据就是后缓冲的数据,也就是下一帧的数据,循环往复,我们就看到了画面,这就是“双缓冲”的技术。双缓冲意味着要使用两个缓冲区(SharedBufferStack中),其中一个称为Front Buffer,另外一个称为Back Buffer。UI总是先在Back Buffer中绘制,然后再和Front Buffer交换,渲染到显示设备中。理想情况下,这样一个刷新会在16ms内完成(60FPS),下图就是描述的这样一个刷新过程(Display处理前Front Buffer,CPU、GPU处理Back Buffer。
但现实情况并非这么理想。
1. 时间从0开始,进入第一个16ms:Display显示第0帧,CPU处理完第一帧后,GPU紧接其后处理继续第一帧。三者互不干扰,一切正常。
2. 时间进入第二个16ms:因为早在上一个16ms时间内,第1帧已经由CPU,GPU处理完毕。故Display可以直接显示第1帧。显示没有问题。但在本16ms期间,CPU和GPU却并未及时去绘制第2帧数据(注意前面的空白区),而是在本周期快结束时,CPU/GPU才去处理第2帧数据。
3. 时间进入第3个16ms,此时Display应该显示第2帧数据,但由于CPU和GPU还没有处理完第2帧数据,故Display只能继续显示第一帧的数据,结果使得第1帧多画了一次(对应时间段上标注了一个Jank)。
通过上述分析可知,此处发生Jank的关键问题在于,为何第1个16ms段内,CPU/GPU没有及时处理第2帧数据?原因很简单,CPU可能是在忙别的事情,不知道该到处理UI绘制的时间了。可CPU一旦想起来要去处理第2帧数据,时间又错过了!
为解决这个问题,Android 4.1中引入了VSYNC,这类似于时钟中断。结果如下图所示:
由上图可知,每收到VSYNC中断,CPU就开始处理各帧数据。整个过程非常完美。
不过,仔细琢磨后却会发现一个新问题:上图中,CPU和GPU处理数据的速度似乎都能在16ms内完成,而且还有时间空余,但是如果数据的准备时间太久,有可能因为 主线程耗时阻塞,xml 布局文件层次过多冗余臃肿,绘制操作不当(onDraw中频繁创建对象) ,导致back buffer 缓冲数据迟迟没有准备好,那么屏幕上就会一直显示 frame buffer ,造成卡顿视觉。
- 当CPU / GPC 准备B Buffer 内容时间过长,导致第一个VSYNC信号到来时不能交付 back Buffer ,那么屏幕上显示的还是之前的那块 PRE Buffer , 并且 B Buffer 内容准备完成后,还需要等待下一个 VSYNC 信号才能交付。
- 因为在第二个 VSYNC 信号到来时,两块 Buffer 都已经被占用(一块用来显示 ,一块被 B Buffer 准备工作持有),所以当下一次绘制内容也存在延迟的情况也会造成连锁卡顿。(同一帧画面显示 2 次及以上)
解决上面问题的办法就是引入第三块 Buffer , 在渲染 B 超时而且 Buffer A 又用于屏幕显示时,可以用第三块 Buffer 来进行C 的准备工作,这样便减少了后面的一次 Jank 发生。
系统大部分情况下都会使用两个Buffer 来完成显示,只有在某一帧的处理时间超过 2 次 VSYNC 信号周期才会使用第三块 Buffer。
View刷新源码追踪
View刷新是从ViewRootImpl.requestLayout()方法开始的:
-
//ViewRootImpl
-
-
@Override
-
public void requestLayout() {
-
if (!mHandlingLayoutInLayoutRequest) {
-
checkThread();
//检查是否在主线程
-
mLayoutRequested =
true;
//mLayoutRequested 是否measure和layout布局。
-
scheduleTraversals();
-
}
-
}
-
-
-
void scheduleTraversals() {
-
if (!mTraversalScheduled) {
//同一帧内不会多次调用遍历
-
mTraversalScheduled =
true;
-
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
//设置同步屏障
-
-
//注册垂直同步信号
-
mChoreographer.postCallback(
-
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable,
null);
-
-
}
-
}
-
//收到垂直同步信号后,执行刷新流程
-
final
class TraversalRunnable implements Runnable {
-
@Override
-
public void run() {
-
doTraversal();
-
}
-
}
-
final TraversalRunnable mTraversalRunnable =
new TraversalRunnable();
-
-
-
void doTraversal() {
-
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
//移除同步消息屏障
-
-
performTraversals();
//View的绘制流程正式开始。
-
}
-
//Choreographer.java
-
-
private void postCallbackDelayedInternal(int callbackType,
-
Object action, Object token, long delayMillis) {
-
-
synchronized (mLock) {
-
final
long now = SystemClock.uptimeMillis();
-
final
long dueTime = now + delayMillis;
-
//保存action,也就是ViewRootImpl中传过来的mTraversalRunnable,留待收到vsync信号后执行
-
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
-
-
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);
-
}
-
}
-
}
调用到Choreographer.postCallback() 后,最终调用到了FrameDisplayEventReceiver.scheduleVsync(),
-
private
final
class FrameDisplayEventReceiver extends DisplayEventReceiver
-
implements
Runnable {
//实现了Runnable
-
private
boolean mHavePendingVsync;
-
private
long mTimestampNanos;
-
private
int mFrame;
-
-
public FrameDisplayEventReceiver(Looper looper, int vsyncSource) {
-
super(looper, vsyncSource);
-
}
-
//接收vsync信号
-
@Override
-
public void onVsync(long timestampNanos, long physicalDisplayId, int frame) {
-
...
-
mTimestampNanos = timestampNanos;
-
mFrame = frame;
-
//发送一个异步消息,消息还是由自己的run()处理
-
Message msg = Message.obtain(mHandler,
this);
-
msg.setAsynchronous(
true);
-
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
-
}
-
-
@Override
-
public void run() {
-
mHavePendingVsync =
false;
-
//会回调之前保存的Callback Runnable,进行屏幕刷新,回到了ViewRootImpl的TraversalRunnable
-
doFrame(mTimestampNanos, mFrame);
-
}
-
}
scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。
这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次onVsync()方法回调。FrameDisplayEventReceiver 继承 DisplayEventReceiver , 主要是用来接收同步脉冲信号 VSYNC。
doFrame也采用了一个boolean遍历mFrameScheduled保证每次VSYNC中,只执行一次,可以看到,为了保证16ms只执行一次重绘,加了好多次层保障。doFrame里除了UI重绘,其实还处理了很多其他的事,比如检测VSYNC被延迟多久执行,掉了多少帧,处理Touch事件(一般是MOVE),处理动画,以及UI,当doFrame在处理Choreographer.CALLBACK_TRAVERSAL的回调时(mTraversalRunnable),才是真正的开始处理View重绘。
流程中多个boolean变量保证了每16ms最多执行一次UI重绘,这也是目前Android存在60FPS上限的原因。
注: VSYNC同步信号需要用户主动去请求才会收到,并且是单次有效。
转载:https://blog.csdn.net/u012216131/article/details/115704825