(4.6.15.4)Choreographer全解析
Posted fei20121106
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了(4.6.15.4)Choreographer全解析相关的知识,希望对你有一定的参考价值。
一、前情提要
1.1 测绘流程的起点
我们在(4.1.37.1)深入理解setContentView过程和View绘制过程一文中,我们讲到了测绘流程的起点是在
- —ViewRoot类的requestLayout()方法
- —scheduleTraversals()
- —mChoreographer.postCallback(Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)请求刷新信号
- —mChoreographer内部handler通过msg机制触发mTraversalRunnable
- 启动performTraversals();测绘流程
那么第3步和第4步之间,是如何实现msg入队,msg响应等一系列操作的呢?
1.2 性能卡顿监听
我们在(4.6.24)构建android外网的性能监控平台Hawkeye一文中提到卡顿监听的两种策略:
- 监听Loop中Msg的执行时长
- 监听Choreographer两次postFrameCallback之间是否超过16ms
那么就有一些疑问了:
- 为什么Loop中Msg的执行时长超过1000ms就算卡顿了?我们通常说的卡顿不是说超过了16.66ms么,为何这里要超过500ms,甚至1000ms才算是卡顿?
- Choreographer两次postFrameCallback之间超过16ms就是卡断,但是RecyclerView的onLayout有时需要一两百毫秒那不就是会频繁报警?
二、VSYNC
1、android最高60FPS,是VSYNC决定的,每16ms最多一帧
2、VSYNC要客户端主动申请,才会有。 因此如果要通过编舞者进行性能监听,要不断的在回调里注册回调**
3、有VSYNC到来才会刷新,前提是监听者发起的Msg被执行到;
4、UI没更改,不会请求VSYNC也就不会刷新
5、UI局部重绘其实只是省去了再次构建硬件加速用的DrawOp树
2.1 单缓冲区和画面撕裂
在一个典型的显示系统中,一般包括CPU、GPU、Display三个部分, CPU负责计算帧数据,把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上。如下图:
屏幕刷新频是固定的,比如每16.6ms从buffer取数据显示完一帧,理想情况下帧率和刷新频率保持一致,即每绘制完成一帧,显示器显示一帧。但是CPU/GPU写数据是不可控的,所以会出现buffer里有些数据根本没显示出来就被重写了,即buffer里的数据可能是来自不同的帧的, 当屏幕刷新时,此时它并不知道buffer的状态,因此从buffer抓取的帧并不是完整的一帧画面,即出现画面撕裂。
简单说就是Display在显示的过程中,buffer内数据被CPU/GPU修改,导致画面撕裂。
2.2 双缓存区 和 Vsync
那咋解决画面撕裂呢? 答案是使用 双缓存。
由于图像绘制和屏幕读取 使用的是同个buffer,所以屏幕刷新时可能读取到的是不完整的一帧画面。
双缓存,让绘制和显示器拥有各自的buffer:GPU 始终将完成的一帧图像数据写入到 Back Buffer,而显示器使用 Frame Buffer,当屏幕刷新时,Frame Buffer 并不会发生变化,当Back buffer准备就绪后,它们才进行交换。如下图:
问题又来了:什么时候进行两个buffer的交换呢?
假如是 Back buffer准备完成一帧数据以后就进行,那么如果此时屏幕还没有完整显示上一帧内容的话,肯定是会出问题的。看来只能是等到屏幕处理完一帧数据后,才可以执行这一操作了。
当扫描完一个屏幕后,设备需要重新回到第一行以进入下一次的循环,此时有一段时间空隙,称为VerticalBlanking Interval(VBI)。那,这个时间点就是我们进行缓冲区交换的最佳时间。因为此时屏幕没有在刷新,也就避免了交换过程中出现 screen tearing的状况。
VSync(垂直同步)是VerticalSynchronization的简写,它利用VBI时期出现的vertical sync pulse(垂直同步脉冲)来保证双缓冲在最佳时间点才进行交换。另外,交换是指各自的内存地址,可以认为该操作是瞬间完成。
所以说V-sync这个概念并不是Google首创的,它在早年的PC机领域就已经出现了
2.3 Vsync的弊端
CPU/GPU用一个Buffer, Display用一个Buffer
以时间的顺序来看下将会发生的过程:
- Display显示第0帧数据,此时CPU和GPU渲染第1帧画面,且在Display显示下一帧前完成
- 因为渲染及时,Display在第0帧显示完成后,也就是第1个VSync后,缓存进行交换,然后正常显示第1帧
- 接着第2帧开始处理,是直到第2个VSync快来前才开始处理的。
- 第2个VSync来时,由于第2帧数据还没有准备就绪,缓存没有交换,显示的还是第1帧。这种情况被Android开发组命名为“Jank”,即发生了丢帧。
- 当第2帧数据准备完成后,它并不会马上被显示,而是要等待下一个VSync 进行缓存交换再显示。
所以总的来说,就是屏幕平白无故地多显示了一次第1帧。原因是 第2帧的CPU/GPU计算 没能在VSync信号到来前完成。
- 双缓存的交换 是在Vsyn到来时进行,交换后屏幕会取Frame buffer内的新数据,而实际 此时的Back buffer 就可以供GPU准备下一帧数据了。
- 如果 Vsyn到来时 CPU/GPU就开始操作的话,是有完整的16.6ms的,这样应该会基本避免jank的出现了(除非CPU/GPU计算超过了16.6ms)。
2.4 Android的改进:收到信号就开始计算
为了优化显示性能,Google在Android 4.1系统中对Android Display系统进行了重构,实现了Project Butter(黄油工程):系统在收到VSync pulse后,将马上开始下一帧的渲染。即一旦收到VSync通知(16ms触发一次),CPU和GPU 才立刻开始计算然后把数据写入buffer。如下图:
一句话总结,VSync同步使得CPU/GPU充分利用了16.6ms时间,减少jank。
问题又来了,如果界面比较复杂,CPU/GPU的处理时间较长 超过了16.6ms呢?如下图:
- 在第二个时间段内,但却因 GPU 还在处理 B 帧,缓存没能交换,导致 A 帧被重复显示。
- 而B完成后,又因为缺乏VSync pulse信号,它只能等待下一个signal的来临。于是在这一过程中,有一大段时间是被浪费的。
- 当下一个VSync出现时,CPU/GPU马上执行操作(A帧),且缓存交换,相应的显示屏对应的就是B。这时看起来就是正常的。
- 只不过由于执行时间仍然超过16ms,导致下一次应该执行的缓冲区交换又被推迟了——如此循环反复,便出现了越来越多的“Jank”。
2.5 Android的再次改进:三级缓存Buffer,更多的计算
三缓存就是在双缓冲机制基础上增加了一个 Graphic Buffer 缓冲区,这样可以最大限度的利用空闲时间,带来的坏处是多使用的一个 Graphic Buffer 所占用的内存。
CPU/GPU用2个Buffer, Display用一个Buffer; 譬如下图C就是第三个Buffer
-
减少了Jank: 第一个Jank,是不可避免的。但是在第二个 16ms 时间段,CPU/GPU 使用 第三个 Buffer 完成C帧的计算,虽然还是会多显示一次 A 帧,但后续显示就比较顺畅了,有效避免 Jank 的进一步加剧。
-
带来了延迟:注意在第3段中,A帧的计算已完成,但是在第4个vsync来的时候才显示. 如果是双缓冲,那在第三个vynsc就可以显示了。
三缓冲有效利用了等待vysnc的时间,减少了jank,但是带来了延迟。 所以,是不是 Buffer 越多越好呢?这个是否定的,Buffer 正常还是两个,当出现 Jank 后三个足以。
三、编舞者Choreographer
上面讲到,Google在Android 4.1系统中对Android Display系统进行了优化:在收到VSync pulse后,将马上开始下一帧的渲染。即一旦收到VSync通知,CPU和GPU就立刻开始计算然后把数据写入buffer。
Choreographer 是 Android 4.1 google的黄油计划新增的机制,用于配合系统的 VSYNC 中断信号。其主要用途是接收系统的 VSYNC 信号,统一管理应用的输入、动画和绘制等任务的执行时机。(CALLBACK_INPUT,CALLBACK_ANIMATION,CALLBACK_TRAVERSAL,CALLBACK_COMMIT)
3.1 构造时间:ViewRootImpl实例化时(handleResumeActivity)
public ViewRootImpl(Context context, Display display) {
mChoreographer = Choreographer.getInstance();
}
public static Choreographer getInstance() {
return sThreadInstance.get();
}
// Thread local storage for the choreographer.
private static final ThreadLocal<Choreographer> sThreadInstance =
new ThreadLocal<Choreographer>() {
@Override
protected Choreographer initialValue() {
Looper looper = Looper.myLooper();
if (looper == null) {
throw new IllegalStateException("The current thread must have a looper!");
}
Choreographer choreographer = new Choreographer(looper, VSYNC_SOURCE_APP);
if (looper == Looper.getMainLooper()) {
mMainInstance = choreographer;
}
return choreographer;
}
};
每一个Looper线程都有自己的Choreographer,其他线程发送的回调只能运行在对应Choreographer所属的Looper线程上
3.2 构造函数
public static final int CALLBACK_INPUT = 0;//输入
public static final int CALLBACK_ANIMATION = 1;//动画
public static final int CALLBACK_TRAVERSAL = 2;//视图绘制
public static final int CALLBACK_COMMIT = 3;//提交
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
//构建指定线程的自定义handler,其实对应的是主线程
mHandler = new FrameHandler(looper);
// 根据是否使用了VSYNC来创建一个FrameDisplayEventReceiver对象用于请求并接收Vsync事件
mDisplayEventReceiver = USE_VSYNC
? new FrameDisplayEventReceiver(looper, vsyncSource)
: null;
mLastFrameTimeNanos = Long.MIN_VALUE;//上一次帧绘制时间点
mFrameIntervalNanos = (long)(1000000000 / getRefreshRate());//帧绘制时间间隔,16.7ms
// CALLBACK_LAST + 1 = 4,创建一个容量为4的CallbackQueue数组,用来存放4种不同的Callback
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
// b/68769804: For low FPS experiments.
setFPSDivisor(SystemProperties.getInt(ThreadedRenderer.DEBUG_FPS_DIVISOR, 1));
}
- 持有Lopper,其实对应的是主线程Lopper
- 构建指定线程的自定义handler,其实对应的是主线程
- 根据是否使用了VSYNC来创建一个FrameDisplayEventReceiver对象用于请求并接收Vsync事件
- 变量USE_VSYNC用于表示系统是否是用了Vsync同步机制,该值是通过读取系统属性debug.choreographer.vsync来获取的。
- Choreographer创建了一个大小为4的CallbackQueue队列数组,用于保存不同类型的Callback。
3.2.1 FrameHandler 内部通信器
private static final int MSG_DO_FRAME = 0; //响应引号
private static final int MSG_DO_SCHEDULE_VSYNC = 1; //请求信号
private static final int MSG_DO_SCHEDULE_CALLBACK = 2;//当需要延迟执行时
private final class FrameHandler extends Handler {
public FrameHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_DO_FRAME:
doFrame(System.nanoTime(), 0);
break;
case MSG_DO_SCHEDULE_VSYNC:
doScheduleVsync();
break;
case MSG_DO_SCHEDULE_CALLBACK://再次执行scheduleFrameLocked
doScheduleCallback(msg.arg1);
break;
}
}
}
3.2.2 FrameDisplayEventReceiver 信号监听者
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) {
Log.w(TAG, "Frame time is " + ((timestampNanos - now) * 0.000001f)
+ " ms in the future! Check that graphics HAL is generating vsync "
+ "timestamps using the correct timebase.");
timestampNanos = now;
}
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} 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);
}
}
3.2.3 mCallbackQueues
3.3 核心逻辑
四、UI刷新流程
以Textview为例 ,当我们通过setText改变TextView内容后,UI界面不会立刻改变,APP端会先向VSYNC服务请求,等到下一次VSYNC信号触发后,APP端的UI才真的开始刷新,基本流程如下
在请求VSYNC的时候,会添加一个同步栅栏,防止UI线程中同步消息执行,这样做为了加快VSYNC的响应速度,如果不设置,VSYNC到来的时候,正在执行一个同步消息,那么UI更新的Task就会被延迟执行
4.1 请求刷新流程
4.1.1 View操作向上递归到ViewRootImpl.scheduleTraversals()
--View.java
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
boolean fullInvalidate) {
// Propagate the damage rectangle to the parent view.
final AttachInfo ai = mAttachInfo;
final ViewParent p = mParent;
if (p != null && ai != null && l < r && t < b) {
final Rect damage = ai.mTmpInvalRect;
damage.set(l, t, r, b);
p.invalidateChild(this, damage);
}
- 同TextView类似,View内容改变一般都会调用invalidate触发视图重绘,View会递归的调用父容器的invalidateChild,逐级回溯,最终走到ViewRootImpl的invalidate
--ViewRootImpl.java
void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
- ViewRootImpl的invalidate会触发scheduleTraversals();
页面生命周期handleResumeActivity中,当建立好了decorView与ViewRoot的关联后,ViewRoot类的requestLayout()方法会被调用,以完成应用程序用户界面的初次布局。实际被调用的是ViewRootImpl类的requestLayout()方法,最终回调的也是scheduleTraversals();
// 将UI绘制的mTraversalRunnable加入到下次垂直同步信号到来的等待callback中去
// mTraversalScheduled用来保证本次Traversals未执行前,不会要求遍历两边,浪费16ms内,不需要绘制两次
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 防止同步栅栏,同步栅栏的意思就是拦截同步消息
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// postCallback的时候,顺便请求vnsc垂直同步信号scheduleVsyncLocked
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
<!--添加一个处理触摸事件的回调,防止中间有Touch事件过来-->
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}
- ViewRootImpl会调用scheduleTraversals准备重绘,但是,重绘一般不会立即执行,而是往Choreographer的Choreographer.CALLBACK_TRAVERSAL队列中添加了一个mTraversalRunnable,同时申请VSYNC
- 这个mTraversalRunnable要一直等到申请的VSYNC到来后才会被执行
scheduleTraversals利用mTraversalScheduled保证:
在当前的mTraversalRunnable未被执行前,scheduleTraversals不会再被有效调用,也就是Choreographer.CALLBACK_TRAVERSAL理论上应该只有一个mTraversalRunnable的Task
4.1.2 Choreographer.postCallback请求同步信号并传入回调
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
//缓存Callback到对应队列中CALLBACK_TRAVERSAL
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) {
//申请VSYNC同步信号
scheduleFrameLocked(now);
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
- postCallbackDelayedInternal主要两个动作
- 缓存Callback到对应队列中CALLBACK_TRAVERSAL
- 申请VSYNC同步信号
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true;
if (USE_VSYNC) {
if (isRunningOnLooperThreadLocked()) {
// 请求VSYNC信号,最终会调到Native层,Native处理完成后触发FrameDisplayEventReceiver的onVsync回调,回调中最后也会调用doFrame(long frameTimeNanos, int frame)方法
scheduleVsyncLocked();
} else {// 在UI线程上直接发送一个what=MSG_DO_SCHEDULE_VSYNC的消息,最终也会调到scheduleVsyncLocked()去请求VSYNC信号
// 因为主线程已经有了同步栅栏,所以必须异步信息,消息才能被UI线程执行
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
}
}
}
- mFrameScheduled校验:scheduleFrameLocked跟上一个scheduleTraversals类似,也采用了利用mFrameScheduled来保证:在当前申请的VSYNC到来之前,不会再去请求新的VSYNC,因为16ms内申请两个VSYNC没意义
- mFrameScheduled保证16ms内,只会申请一次垂直同步信号
- scheduleFrameLocked可以被调用多次,但是mFrameScheduled保证下一个vsync到来之前,不会有新的请求发出
- 多余的scheduleFrameLocked调用被无效化
- 触发信号请求
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync();
}
- 通过mDisplayEventReceiver触发信号请求。
- scheduleVsync()方法通过底层nativeScheduleVsync()向SurfaceFlinger 服务注册,即在下一次脉冲接收后会调用 DisplayEventReceiver的dispatchVsync()方法。
- 这里类似于订阅者模式,但是每次调用nativeScheduleVsync()方法都有且只有一次dispatchVsync()方法回调。
这里就解释了:VSYNC要客户端主动申请,才会有
4.2 接受VSYNC信号
等到VSYNC到来后,调用逻辑如下
VSYNC回来流程示意
private final class FrameDisplayEventReceiver extends DisplayEventReceiver
implements Runnable {
private boolean mHavePendingVsync;
private long mTimestampNanos;
private int mFrame;
public FrameDisplayEventReceiver(Looper looper) {
super(looper);
}
@Override
public void onVsync(long timestampNanos, int builtInDisplayId, int frame) {
long now = System.nanoTime();
if (timestampNanos > now) {
<!--正常情况,timestampNanos不应该大于now,一般是上传vsync的机制出了问题-->
timestampNanos = now;
}
<!--如果上一个vsync同步信号没执行,那就不应该相应下一个(可能是其他线程通过某种方式请求的)-->
if (mHavePendingVsync) {
Log.w(TAG, "Already have a pending vsync event. There should only be "
+ "one at a time.");
} else {
mHavePendingVsync = true;
}
<!--timestampNanos其实是本次vsync产生的时间,从Native发过来-->
mTimestampNanos = timestampNanos;
mFrame = frame;
Message msg = Message.obtain(mHandler, this);
<!--由于已经存在同步栅栏,所以VSYNC到来的Message需要作为异步消息发送过去-->
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, timestampNanos / TimeUtils.NANOS_PER_MS);
}
@Override
public void run() {
mHavePendingVsync = false;
<!--这里的mTimestampNanos其实就是本次Vynsc同步信号到来的时候,但是执行这个消息的时候,可能延迟了-->
doFrame(mTimestampNanos, mFrame);
}
}
- 在收到onVsync回调后,FrameDisplayEventReceiver将自己作为Runnable封装到一个Msg中(类似post(callabck)),用Handler发送到主线程Looper中
- 用的是内部自定义Handler,因此处理是在主线程
- postCallback方式,因此会回调Run函数
- 此时主线程Loop已经开启了屏障模式,只处理异步信息,因此这里要标记是异步信息
注意此时标记了startTime
如果期间有其他异步信息被执行,会加长endTime
4.3 触发回调
doFrame执行UI绘制的示意图
- 主线程Looper不断处理异步信息,知道处理到本次Msg,毁掉doFrame
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
synchronized (mLock) {
<!--做了很多东西,都是为了保证一次16ms有一次垂直同步信号,有一次input 、刷新、重绘-->
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
<!--检查是否因为延迟执行掉帧,每大于16ms,就多掉一帧-->
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
<!--跳帧,其实就是上一次请求刷新被延迟的时间,但是这里skippedFrames为0不代表没有掉帧-->
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
<!--skippedFrames很大一定掉帧,但是为 0,去并非没掉帧-->
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
<!--开始doFrame的真正有效时间戳-->
frameTimeNanos = startNanos - lastFrameOffset;
}
if (frameTimeNanos < mLastFrameTimeNanos) {
<!--这种情况一般是生成vsync的机制出现了问题,那就再申请一次-->
scheduleVsyncLocked();
return;
}
<!--intendedFrameTimeNanos是本来要绘制的时间戳,frameTimeNanos是真正的,可以在渲染工具中标识延迟VSYNC多少-->
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
<!--移除mFrameScheduled判断,说明处理开始了,-->
mFrameScheduled = false;
<!--更新mLastFrameTimeNanos-->
mLastFrameTimeNanos = frameTimeNanos;
}
try {
<!--真正开始处理业务-->
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
<!--处理打包的move事件-->
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
<!--处理动画-->
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
<!--处理重绘-->
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos);
<!--不知道干啥的-->
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
- doFrame也采用了一个boolean遍历mFrameScheduled保证每次VSYNC中,只执行一次
- 可以看到,为了保证16ms只执行一次重绘,加了好多次层保障。
- 掉帧计算
- Vsync信号到来时记录一个开始时间,即onVsync()中的mTimestampNanos。
- 在主线程MessageQueue中轮到doFrame()的message执行后(Vsync信号到来有可能不会立马处理,因为这中间主线程可能被其他任务占用),在doFrame()中记录一个结束时间,即下面代码中的startNanos。
- 这两个时间差值就是Vsync信号的处理时延,即掉帧时间。
- 超过30就打印日志
- doFrame里除了UI重绘,其实还处理了很多其他的事,比如检测VSYNC被延迟多久执行,掉了多少帧,处理Touch事件(一般是MOVE),处理动画,以及UI
- 当doFrame在处理Choreographer.CALLBACK_TRAVERSAL的回调时(mTraversalRunnable),才是真正的开始处理View重绘:
--ViewRootlmpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
五、性能监控中的postFrameCallback
public void postFrameCallback(FrameCallback callback) {
postFrameCallbackDelayed(callback, 0);
}
public void postFrameCallbackDelayed(FrameCallback callback, long delayMillis) {
if (callback == null) {
throw new IllegalArgumentException("callback must not be null");
}
postCallbackDelayedInternal(CALLBACK_ANIMATION,
callback, FRAME_CALLBACK_TOKEN, delayMillis);
}
}
- 最终用的是CALLBACK_ANIMATION动画类型
- …
- 和4.1.2一样
- …
- <4.3 doFrame>中doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);,会触发自己定义的回调
由于doFrame的目标是16ms一次,因此我们自己的回调也会是16ms一次。
总结
UI渲染流程
- setContentView实例化成员变量
- handleResumeActivity中出发ViewRootlmpl实例化和setView
- ViewRootlmpl触发requestlayout 内部触发线程验证和scheduleTraversals
- ViewRootlmpl#scheduleTraversals向主线程中丢一个屏障信息,并调用编舞者传入回调请求Vsync信号
- 编舞者向底层请求信号
- 底层返回信号
- 编舞者完善16.67ms计时,并向主线程发送异步信息
- 异步信息被执行时,触发各类型回调(渲染、动画等)
- ViewRootlmpl触发doTraversal(),移除屏障信息并最终通过performTraversals()开启绘制流程
丢帧(掉帧) ,是说 这一帧延迟显示 还是丢弃不再显示 ?
答:延迟显示,因为缓存交换的时机只能等下一个VSync
连续两次setTextView到底会触发几次UI重绘呢?
肯定是一次了,UI必须至少等待16ms的间隔才会绘制下一帧,所以连续两次setTextView只会触发一次重绘
如果界面没动静止了,还会刷新吗?
答:屏幕会固定, 没16.6ms刷新,CPU/GPU不走绘制流程
现在那种120HZ的,如果超过8ms会怎样呢?结果会比60HZ还差么?
超过8ms也会掉帧,但下一次缓存交换也会很快到来(8ms之内),即掉帧的时间变短了,即变细腻了
Loop中Msg的执行时长超过1000ms就算卡顿,为啥不是16.67ms?
- 其实不是严格意义上的UI卡顿,Android的确是在16ms内刷新一帧,但是某些场景下是不需要刷新ui的,而且是16.67ms对于UI操作也太苛刻了
- 这里其实更像是检测主线程耗时操作
- 举个例子,我们可能在主线程操作数据库,可能在主线程解析json,可能在主线程写文件,可能在主线程做一些例如高斯模糊的耗时操作,甚至RecyclerView的layout都可能超过200ms
- 这个时间和16ms其实就没多大关联了,我们要定义的是主线程慢操作的耗时阀值
Choreographer两次postFrameCallback之间超过16ms就是卡断,但是RecyclerView的onLayout有时需要一两百毫秒那不是会频繁报警?
- 我们通常说的16.67ms是指两次编舞者Callback的间隔,注意是触发回调的时间(Msg已经被执行到了), 同时在当前这个msg中,ViewRootlmpl触发doTraversal()
- 理论上两次回调应该是间隔16.67ms,除非主线程有其他任务,导致异步任务一直不能被执行。因此设置16.67ms是可行的。
- 但是实际上一些视图元素的计算时常超过16.67ms是很正常的,因此其实是阀值层面上需要控制
05-13 16:45:39.142 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_LOAD_END1620895539142
05-13 16:45:39.171 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_PARSE_END1620895539171
05-13 16:45:39.186 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_BINDDATA_START1620895539186
05-13 16:45:39.224 1549-1549/com.lazada.android.dev E/yhf: UI线程超时(超过16ms):116ms , 丢帧:7
05-13 16:45:39.510 1549-1549/com.lazada.android.dev E/yhf: LZD_FIRST_NET_BINDDATA_END1620895539510
05-13 16:45:39.853 1549-1549/com.lazada.android.dev E/yhf: UI线程超时(超过16ms):304ms , 丢帧:19
05-13 16:45:39.933 1549-1549/com.lazada.android.dev E/yhf: UI线程超时(超过16ms):88ms , 丢帧:5
譬如上述日志,第一个vh的oncreate到最后一个vh的耗时324ms,编舞者记录了304ms丢帧19
参考文献
以上是关于(4.6.15.4)Choreographer全解析的主要内容,如果未能解决你的问题,请参考以下文章
偶尔的 I/Choreographer(17165):跳过 ## 帧黑屏锁定,Android Xamarin c#