ScreenDraw
当手指点击了桌面的App图标时发生了什么 - ProcessOn
Android 屏幕刷新机制
主要参考 https://juejin.cn/post/6863756420380196877#heading-12
省流版:
双缓存:为了解决画面撕裂;画面撕裂来自于只有一个buffer时,正在display的那一帧数据被后一帧的数据覆盖了
Vsync:系统在收到VSync pulse(Vsync脉冲)后,将马上开始下一帧的渲染,(CPU开始计算数据)。
三缓冲:当显示器正在写入FrameBuffer同时GPU也正在写入BackBuffer时,下一次渲染开始了,此时CPU可以使用新增的GraphicBuffer进行计算。减少了Jank。(更多缓冲需要耗费更大的内存)
ChoreoGrapher机制:规定了数据计算开始(measure、layout、draw)的时机(vsync信号),使计算到渲染图像数据能有一个完整的16.6ms:更新ui(request()/invalidate())后编舞者注册vsync信号回调,在下一个vsync信号到时候立刻进行view的测量布局绘制
一、缓冲和Vsync
https://juejin.cn/post/6863756420380196877
1、单缓冲,tearing
由于Gpu跟显示器使用同一个缓冲,导致可能屏幕扫描刷新时,可能读取到的不是同一帧里的图像数据,造成画面撕裂
2、↓ 双缓冲 ↓
双缓冲是为了解决“由于Gpu跟显示器使用同一个缓冲,导致可能屏幕扫描刷新时,可能读取到的不是同一帧里的图像数据,造成画面撕裂”,让绘制和显示器拥有各自的buffer:
GPU 的图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行地址交换。(缓存区交换被称为BufferSwap
,帧传递。)
上面说到的Back Buffer 跟 Frame Buffer地址交换,时间点选择在了 Back buffer完整写入之后,在之后屏幕扫描完一个屏幕(从左到右,从上到下逐行显示每一个像素点,整个过程以60Hz屏为例是16.6ms)后,设备从右下回到左上的这个时间区间(即VBI VerticalBlackingInterval垂直同步间隙),而垂直同步脉冲(vertical sync pulse)就是在VBI时期发出的,脉冲发出时间时立即进行帧传递。
总结下就是:垂直同步脉冲是在屏幕扫描到右下最后一个像素后,重置回到左上的这个时间空隙发出的,所以每16.6ms(60HZ屏)发出一个脉冲信号。收到脉冲信号后,如果GPU的缓冲已经准备好了,就会立即进行帧传递。
但是,双缓存只是规定了图像数据数据写入BackBuffer完成后,FrameBuffer与BackBuffer交换的时机,而数据开始计算时间的不确定,则导致了下一帧中,CPU/GPU未能在帧开始的时候就进行计算,进而导致帧结束时CPU/GPU工作未完成(明明CPU/GPU工作时长小于16.6ms),却还是造成掉帧(jank)。
3、↓ 三缓冲 + Vsync ↓
Android 4.x版本的黄油工程 project butter引入三缓冲与Vsync,提升了性能,促使了Android的普及
3.1、 VSync 机制
Android实现即下述第二节choreographer机制
系统在收到VSync pulse(Vsync脉冲)后,将马上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开始计算然后把数据写入buffer。VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank
3.2、 三缓冲
CPU、GPU、显示器都能尽快拿到 buffer,减少不必要的等待。如果显示器和 GPU 现在都使用着一个 buffer,如果下一次渲染开始了,因为还有一个 buffer 可以用于 CPU 数据的写入,所以可以马上开始下一帧数据的渲染。
三缓冲就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了延迟。
二、Choreographer机制 或称 “drawing with vysnc”
-> requestLayout() / invalidate() 所有UI的变化都是走到ViewRootImpl的scheduleTraversals()方法。
-> scheduleTraversals()
1 | void scheduleTraversals() { |
mChoreographer.postCallback() // 注册监听Vsync信号(只有注册了监听的Vsync才会回调执行)回调处理内容为mTraversalRunnable,事件类型为CALLBACK_TRAVERSAL(CALLBACK_INPUT输入事件、CALLBACK_ANIMATION动画、CALLBACK_INSETS_ANIMATION插入更新动画、CALLBACK_TRAVERSAL绘制、CALLBACK_COMMIT提交,五种类型顺序执行)
postCallbackDelayedInternal
- if (dueTime <= now) : 直接执行scheduleFrameLocked
- else : msg.setAsynchronous(true); 发送延迟到点后的异步消息执行scheduleFrameLocked
-> scheduleFrameLocked()
- 根据 版本未开启VSYN(4.1以下)直接走doFrame
- 确认并切换到Choreographer的handler(异步消息)执行 scheduleVsyncLocked()
-> scheduleVsncLocked()
-> scheduleVsync()
-> nativeScheduleVsync(mReceiverPtr);
- 注册VSYNC信号回调,只有注册监听的那一个Vsync信号才会接收回调最后在onVsync()
这里不应该为软件申请了Vsync信号,Vsync信号是由屏幕/显示设备发出的,无论choreographer是否监听都会在固定的时间间隔发出(60fps->16.6ms),View更新后由choreographer注册监听后才会对下一个Vsync信号回调回来后处理,走doCallbacks ,也就是最初由choreographer注册的回调mChoreographer.postCallback() -> doTraversal() -> 移除同步屏障并真正执行View的measure、layout、draw流程
所以每16ms都会发出Vsync信号,每16ms屏幕都在刷新,但不是每16ms都会走measure、layout、draw
native层注册监听Vsync回调,DisplayEventReceiver::requestNextVsync()
1 | // frameworks/base/core/jni/android_view_DisplayEventReceiver.cpp |
native Vsync信号回调回来了,由FrameDisplayEventReceiver的onVsync方法接收:
-> onVsync()
-> doFrame()
1 | void doFrame(long frameTimeNanos, int frame) { |
doFrame()
-> 按callbacktype依次执行将doCallbacks(),即执行doCallbacks(0、1、2、3、4,? frameTimeNanos)
doCallbacks(int callbackType, long frameTimeNanos)
-> 根据传入的callbackType从mCallbackQuesu中取出对应的队列,执行队列中到达执行时间的CallbackRecord
即依次 将 输入队列、 动画队列、绘制队列、提交队列 中所有的到达执行时间的 元素(CallbackRecord)
执行其中的action(viewRootImpl 中mChoreographer.mpostCallback时传递的mTraversalsRunnable)
1 | void doCallbacks(int callbackType, long frameTimeNanos) { |
callbacktype分为5种(实际上是四种,新版本Android合并后只剩四种事件:输入、动画、遍历traversal、提交):
1 | //输入事件,首先执行 public static final int CALLBACK_INPUT = 0; |
优先级的高低和处理顺序有关,每当收到 VSYNC 信号时,Choreographer 将首先处理 INPUT 类型的任务,然后是 ANIMATION 类型,最后才是 TRAVERSAL 类型。
-> doCallbacks()
-> mTraversalRunnable
-> doTraversal()
1 | void doTraversal() { |
附录 Choreographer部分源码(Android4.1新增)
Choreographer 是线程单例的(ThreadLocal实现),而且必须要和一个 Looper 绑定,因为其内部有一个 Handler 需要和 Looper 绑定,一般是 App 主线程的 Looper 绑定
1 | //Choreographer.class |
三、invalidate/requestLayout流程
invalidate/postInvalidat/requestLayout简要区别:
- invalidate只会调onDraw方法且必须在UI线程中调用
- postInvalidate只会调onDraw方法,可在非UI线程中回调
- requestLayout会调onMeasure、onLayout和onDraw(特定条件下)方法
invalidate
调用 View.invalidate() 方法后会逐级往上调用父 View 的相关方法,最终在 Choreographer 的控制下调用 ViewRootImpl.performTraversals() 方法。只有满足 可见性、尺寸发生变化时 等条件才会执行 measure 和 layout 流程,否则只执行 draw 流程,draw 流程的执行过程与是否开启硬件加速有关:
关闭硬件加速则从 DecorView 开始往下的所有子 View 都会被重新绘制。
开启硬件加速则只有调用 invalidate 方法的 View 才会重新绘制。
requestLayout
调用 View.requestLayout 方法后会依次调用 performMeasure, performLayout 和 performDraw 方法,调用者 View 及其父 View 会重新从上往下进行 measure, layout 流程,一般情况下不会执行 draw 流程(子 View 会通过判断其尺寸/顶点是否发生改变而决定是否重新 measure/layout/draw 流程)。
小结:因此,当只需要进行重绘时可以使用 invalidate 方法,如果需要重新测量和布局则可以使用 requestLayout 方法,而 requestLayout 方法不一定会重绘,因此如果要进行重绘可以再手动调用 invalidate 方法。
子线程更新UI
1、为啥会崩:
1 | -> 视图更新操作invalidata |
1 | -> 视图更新操作requestLayout |
1 | void checkThread() { |
2、子线程更新UI怎么不崩:
- 子线程在ViewRootImpl初始化之前(handleResumeActivity之后初始化)
- ViewRootImpl创建线程 只需要与 View的更新线程 是同一个就行(初始化在子线程,更新在主线程一样会崩)
View.post与getHandler.post
view.post在内部类AttachInfo未实例化之前是会将action通过getRunQueue().post(action)缓存起来,延后到handleResumeActivity中初始化ViewRootImp时调用setView时,会从顶向下调用View/ViewGroup的dispatchAttachToWindow方法,在此方法内缓存的action会被执行
Activity的ViewRootImpl在onResume方法中创建的,具体见下
四、同步消息屏障
在invalidate/requestLayout执行时,最后都会走向ViewRootImpl的scheduleTraversals()方法中,这个方法中会调用mHandler.getLooper().getQueue().postSyncBarrier();
,本质上是往主线程Looper中post一个target==null的消息,作为同步消息屏障,过滤掉所有非异步的消息,只执行异步消息。
同时Choreographer注册Vsync回调,下一个Vsync消息发出后回调回来移除同步消息屏障并执行 performTraversals(也就是measure、layout、draw)
五、ChoreoGrapher什么时候初始化
Activity启动后,执行完ActivitityThread.performResumeActivity(),再执行WindowManagerImpl.addView(),在其内部会执行ViewRootImpl的初始化。Choreographer的初始化就是在ViewRootImpl的构造方法中执行的。
ViewRootImpl的关键全局变量除了Choreographer(掌管绘制相关)外,还有attachInfo()