Android绘制&显示系统
Android 显示基础
Tearing 屏幕撕裂
同一时刻显示 2 个帧不同的画面,双缓冲可以解决该问题。
Jank 一帧显示 2 次
一帧数据在屏幕上连续出现 2 次
减少 jank 出现解决:
- vsync
- 三缓存
Double Buffer 双缓存?
双缓存技术,两块 buffer,一块 back buffer 用于 CPU/GPU 后台绘制,另外一块 frame buffer 用于显示;back buffer 准备就绪后,它们进行交换。双缓冲很大程度上降低了 screening tearing 了 (屏幕撕裂,同一时刻显示了 2 个帧不同的画面)
什么是 Vsync?
如果双缓存的交换实际是在是在 back buffer 准备完成后进行,当 back buffer 准备完毕后双缓存交换,此时屏幕还没有完整显示出上一帧的内容,就会有 jank 问题 (掉帧)
垂直同步,解决双缓存何时交换 buffer 的问题。
VSync 是硬件每隔 1/屏幕刷新频率
,一般是 60HZ = 1/60 = 16.67ms
Android 屏幕刷新机制
屏幕刷新机制历史
1、Android4.1 之前
Double Buffer Drawing without VSync 双缓存 drawing 未用 vsync,CPU 和 GPU 计算时机不定,如果在一个 vsync 快结束才开始计算工作,等到下一个 vsync 到来时,计算还未完成,显示的还是上一次的结果,很容易出现 jank(一帧显示了 2+ 次)
2、Android4.1 及以后
Project Butter 黄油计划,引入了 VSync、Triple Buffer 和 Choreographer。在收到 VSync 信号时,CPU 和 GPU 就开始工作准备下一帧的渲染,这样 CPU 和 GPU 就有完整的 16.67ms 来处理数据,减少了大量的 jank。
3、三级缓存 Triple Buffer
三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。
三缓存作用,2 个缓存区被 GPU 和 display 占据的时候,开辟一个缓冲区给 CPU 用,一般来说都是用双缓冲,需要的时候会开启 3 缓冲,三缓冲的好处就是使得动画更为流程,但是会导致 lag,从用户体验来说,就是点击下去到呈现效果会有延迟。所以默认不开三缓冲,只有在需要的时候自动开启
屏幕刷新机制
一个典型的显示系统中,一般包括 CPU、GPU 和 display 三部分:
- CPU 负责计算数据(View 的 measure/layout/draw),把计算好的数据交给 GPU
- GPU 会对图形数据进行渲染,渲染好后放到 buffer 里存起来
- display 负责把 buffer 里的数据呈现到屏幕上
显示过程简单的说就是:CPU/GPU 准备好数据,存入 buffer,display 每隔一段时间去 buffer 里取数据,然后显示出来,display 读取的频率是固定的,比如每隔 16ms(60HZ) 读一次,但 CPU/GPU 写数据是完全无规律的,这样容易出现 jank(一帧显示 2+ 次)。
Android4.1 后的黄油计划,让 CPU/GPU 在 vsync 信号到来时就工作,降低出现 jank 的情况。
Android 中用 Choreographer 来协调 vsync 信号
- 第 0VSync,CPU/GPU 计算第 1 帧的数据存到 Back/Frame Buffer,Display 展示第 0 帧数据
- 第 1VSync,CPU/GPU 计算第 2 帧的数据存到 Back/Frame Buffer,Display 从 Frame Buffer 取出第 1 帧数据展示
- 第 2VSync,CPU/GPU 计算第 3 帧的数据存到 Back/Frame Buffer,Display 从 Frame Buffer 取出第 2 帧数据展示
- 第 3VSync,CPU/GPU 计算第 4 帧的数据存到 Back/Frame Buffer,Display 从 Frame Buffer 取出第 3 帧数据展示
- 第 4VSync,Display 从 Frame Buffer 取出第 4 帧数据展示,App 不需要刷新界面了,App 不会再接收到 VSync 信号,也就不会再让 CPU 去绘制视图树来计算下一帧画面了
- 第 5 及 +VSync,Display 一直展示第 4 帧的数据,VSync 还是会继续发出,只是我们的 App 不再请求接收了
VSync
Vsync 分发
Vsync 信号可以由硬件产生,也可以用软件模拟,不过现在基本上都是硬件产生,负责产生硬件 Vsync 的是 HWC(HWComposer)。
HWC HAL 通过 callback 函数,把 VSYNC 信号传给 DispSyncThread,DispSyncThread 传给 EventThread。
HWComposer:硬件生成 vsync;VsyncThread:软件模拟生成 vsync
渲染层 (App) 与 Vsync 打交道的是 Choreographer,而合成层与 Vsync 打交道的,则是 SurfaceFlinger。SurfaceFlinger 也会在 Vsync 到来的时候,将所有已经准备好的 Surface 进行合成操作
Choreographer 编舞者
Choreographer 是什么?
Google 在 Android 4.1 提出的 Project Butter 黄油计划对 Android Display 系统进行了优化:在收到 VSync pulse 后,将马上开始下一帧的渲染。即一旦收到 VSync 通知,CPU 和 GPU 就立刻开始计算然后把数据写入 buffer。
文档说,Choreographer 是协调动画时间脉冲(timing pulse,例如 vsync),输入事件和绘制。
译为 “ 舞蹈指导 “,用于接收显示系统的 VSync 信号,在下⼀帧渲染时控制执行一些操作。Choreographer 的 postCallback 方法用于发起添加回调,这个添加的回调将在下⼀帧被渲染时执行。
Choreographer 作用?
Choreographer 的引入,主要是配合 Vsync,给上层 App 的渲染提供一个稳定的 Message 处理的时机,也就是 Vsync 到来的时候 ,系统通过对 Vsync 信号周期的调整,来控制每一帧绘制操作的时机. 目前大部分手机都是 60Hz 的刷新率,也就是 16.6ms 刷新一次,系统为了配合屏幕的刷新频率,将 Vsync 的周期也设置为 16.6 ms,每个 16.6 ms,Vsync 信号唤醒 Choreographer 来做 App 的绘制操作 ,这就是引入 Choreographer 的主要作用。
Choreographer 应用:
- View 变更(View 的 measure\layout\draw)
View 一个小小改动需要重绘,都是通过 ViewRootImpl,来添加同步屏障,Choreographer 协调 vsync 信号来进行 requestLayout() 注意:一次 vsync 周期内,requestLayout 只会触发一次
- 属性动画
Choreographer 原理 Choreographer.postCallback
- Choreographer 会 post 一个 Runnable,这个 runnable 会根据时间顺序插入到 mCallbackQueues 这个单链表中,接着调用 scheduleFrameLocked,它的作用是:
- 如果当前线程是 Choregrapher 线程的话,直接调用 scheduleVsyncLocked() 方法
- 否则就发送一个异步消息到消息队列里取,这个异步消息不受同步屏障影响,而且这个消息还要插入到消息队列的头部(屏幕刷新的消息是非常紧急的)
- 在 scheduleVsyncLocked 中调用 DisplayEventReceiver 的 scheduleVsync,最终会调用 native 的 nativeScheduleVsync 方法请求下一个 vsync,其内部通过 scoketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets) 创建两个描述符 mSendFd、mReceiverFd 以实现类似生产者消费者的管道机制,然后回调到 Java 层的 dispatchVsync() 方法,里面会回调 onVsync() 方法,内部最终会调用 doFrame() 方法,最终执行前面 postCallbak 的 Runnable 的 run() 方法。
Choreographer 小结
- 使用 Choreographer 必须是在 Looper 线程
- Choreographer 是线程唯一的实例,保存在 ThreadLocal
- Choreographer 通过 postCallbackXXX 提交任务,postCallback 提交 Runnable,postFrameCallback 提交 FrameCallback
- Choreographer 定义了 5 种类型的任务(Input/ANIMATION/INSETS_ANIMATION/TRAVERSAL/COMMIT ),按顺序执行
- Choreographer 中所有的 Handler 消息都是异步消息,不受同步屏障影响
Choreographer 用途
Choreographer 的 postFrameCallback() 计算丢帧情况
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// Application.java
public void onCreate() {
super.onCreate();
//在Application中使用postFrameCallback
Choreographer.getInstance().postFrameCallback(new FPSFrameCallback(System.nanoTime()));
}
public class FPSFrameCallback implements Choreographer.FrameCallback {
private static final String TAG = "FPS_TEST";
private long mLastFrameTimeNanos = 0;
private long mFrameIntervalNanos;
public FPSFrameCallback(long lastFrameTimeNanos) {
mLastFrameTimeNanos = lastFrameTimeNanos;
mFrameIntervalNanos = (long)(1000000000 / 60.0);
}
@Override
public void doFrame(long frameTimeNanos) {
//初始化时间
if (mLastFrameTimeNanos == 0) {
mLastFrameTimeNanos = frameTimeNanos;
}
final long jitterNanos = frameTimeNanos - mLastFrameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if(skippedFrames>30){
//丢帧30以上打印日志
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
}
mLastFrameTimeNanos=frameTimeNanos;
//注册下一帧回调
Choreographer.getInstance().postFrameCallback(this);
}
}
Android 图形系统
Surface
什么是 Surface?
Surface 是一个包含需要渲染到屏幕上的像素对象。屏幕上的每一个窗口都有自己的 Surface,而 SurfaceFlinger 会按照正确的 Z 轴顺序,将它们合成在屏幕上。
一个 Surface 会有多个缓冲区来进行双缓冲渲染,显示在屏幕上称为前端缓冲区,还没有显示在屏幕上的称为后端缓冲区,这样应用程序可以先在后端缓冲区绘制下一帧的内容,每隔一段时间交换两块缓冲区,这样就不需要等待所有内容都绘制完毕,屏幕上就可以显示出内容。
一个 Activity 有一个 Window,对应一个 Surface。
Surface 创建?
Java 层的 Surface 是 ViewRootImpl 的成员变量,public final Surface mSurface = new Surface();
SurfaceView
SurfaceView 就是一个嵌入了独立 Surface 的特殊 View,Surface 中有一个独立的画布 Canvas 用于绘制内容,SurfaceView 本质上是这个 Surface 的容器,用于控制 Surface 的格式、尺寸等基础信息。
SurfaceView 显示内容时,会在 Window 上挖一个洞,SurfaceView 绘制的内容显示在这个洞里,其他的 View 继续显示在 Window 上。
SurfaceView 是一种特殊 View,SurfaceView 持有一个独立的 Surface,专门用于一些特殊且耗时的绘制。
SurfaceView 的背景
默认情况下 SurfaceView 渲染时会显示黑色的背景,如果当我们需要显示透明的背景可以使用如下的代码。弊端是 SurfaceView 会显示在 Window 的顶层,遮住其他的 View。
1
2
surfaceHolder.setFormat(PixelFormat.TRANSPARENT)
setZOrderOnTop(true)
SurfaceView 的画布获取时为何要加锁?
因为 SurfaceView 是可以在子线程中执行绘制的,如果不对画布加锁,那么多个子线程同时更新画布就会产生无法预期的情况,所以需要加锁。
Android 图形显示流程
无论开发者使用什么渲染 API,一切内容都会渲染到 Surface 上。Surface 表示缓冲区队列中的生产方,而缓冲区队列通常会被 SurfaceFlinger 消耗。在 Android 平台上创建的每个窗口都由 Surface 提供支持。所有被渲染的可见 Surface 都被 SurfaceFlinger 合成到屏幕。
面试题
屏幕刷新相关题
丢帧 (掉帧),是说这一帧延迟显示还是丢弃不再显示?
延迟显示,因为缓存交换的时机只能等下一个 VSync 了。
布局层级较多/主线程耗时是如何造成丢帧的呢?
布局层级较多/主线程耗时 会影响 CPU/GPU 的执行时间,大于 16.6ms 时只能等下一个 VSync 了
避免丢帧的方法之一是保证每次绘制界面的操作要在 16.6ms 内完成,如果某次用户点击屏幕导致的界面刷新操作是在某一个 16.6ms 帧快结束的时候,那么即使这次绘制操作小于 16.6 ms,按道理不也会造成丢帧么?
代码里调用了某个 View 发起的刷新请求 invalidate,这个重绘工作并不会马上就开始,而是需要等到下一个 VSync 来的时候 CPU/GPU 才开始计算数据存到 Buffer,下下一帧数据屏幕才从 Buffer 拿到数据展示。
也就是说一个绘制操作后,至少需要等 2 个 vsync,在第 3 个 vsync 信号到来才会真正展示
- 当前 vsync:发起重绘
- 下一个 vsync:CPU/GPU 开始工作,将数据存到 buffer
- 下下一个 vysnc: display 从 buffer 取数据显示
Android 每隔 16.6 ms 刷新一次屏幕到底指的是什么意思?是指每隔 16.6ms 调用 onDraw() 绘制一次么?
Android 每隔 16.6 ms 刷新一次屏幕其实是指底层会以这个固定频率来切换每一帧的画面,而这个每一帧的画面数据就是我们 App 在接收到屏幕刷新信号之后去执行遍历绘制 View 树工作所计算出来的屏幕数据。
而 app 并不是每隔 16.6ms 的屏幕刷新信号都可以接收到,只有当 app 向底层注册监听下一个屏幕刷新信号之后,才能接收到下一个屏幕刷新信号到来的通知。而只有当某个 View 发起了刷新请求时,App 才会去向底层注册监听下一个屏幕刷新信号。
小结:现在主流屏幕都是 60HZ,即 16.67ms 刷新一次屏幕,App 只有在需要重绘的时候发起刷新请求,App 才会注册下一个 vsync 信号来处理,并不是每隔 16.67ms 就会绘制一次。
如果界面没有重绘,还会每隔 16ms 刷新屏幕么?
界面没有重绘,就不会收到 vsync 信号,但是屏幕还是会以每秒 60 帧的数据刷新的,这个画面数据用的是旧的,所以看起来没有什么变化。
measure/layout/draw 走完,界面就立刻刷新了吗?
不是。measure/layout/draw 走完后,只是 CPU 计算数据完成,会在下一个 VSync 到来时进行缓存交换,屏幕才能显示出来。
如果在屏幕快要刷新的时候才去绘制会丢帧么?
代码里面发起的重绘不会马上执行,它都是等到下次 vsync 信号来的时候才开始,所以什么时候发起都没太大关系。
- 当前 vsync:发起重绘
- 下一个 vsync:CPU/GPU 开始工作,将数据存到 buffer
- 下下一个 vysnc: display 从 buffer 取数据显示
屏幕刷新使用 双缓存、三缓存,这又是啥意思呢?
双缓存是 Back buffer、Frame buffer,用于解决 screen tearing 画面撕裂(屏幕上下显示了不同的帧)
三缓存增加一个 Back buffer,用于减少 Jank(一帧显示了 2+ 次)。
有了同步屏障消息的控制就能保证每次一接收到屏幕刷新信号就第一时间处理遍历绘制 View 树的工作么?
只能说,同步屏障是尽可能去做到,但并不能保证一定可以第一时间处理。因为,同步屏障是在 scheduleTraversals() 被调用时才发送到消息队列里的,也就是说,只有当某个 View 发起了刷新请求时,在这个时刻后面的同步消息才会被拦截掉。如果在 scheduleTraversals() 之前就发送到消息队列里的工作仍然会按顺序依次被取出来执行。