文章目录
以下内容,参考至 面对 UI 卡顿,如何入手分析解决问题
什么导致了卡顿
内存抖动是造成 UI 卡顿的最主要原因。内存抖动的原因是频繁的创建和回收对象。而回收对象的工作是 GC 进程为我们实现的。GC在工作时会触发Stop The World。因此,频繁的发生 GC 是导致卡顿的主要原因。
怎么分析卡顿问题
对于UI性能分析, Systrace 是目前使用最广的工具,它收集相关程序的运行数据,生成一个网页文件,通过 Chrome 查看该网页文件,可以看到各个方法运行的时间。
Android为我们提供了运行Systrace的脚本,在 android-sdk/platform-tools/systrace 文件夹中(我们可以在环境变量中添加该地址)。在连接 adb 的条件下,运行以下命令
python systrace.py --time=20 -o newtrace.html
会在当前目录生成一个 newtrace.html 的网页文件,我们通过 Chrome 查看 newtrace.html 。
在使用 Systrace 分析UI性能问题时,我们先学习一下屏幕的刷新机制。
CPU&GPU
我们知道,UI线程在抢占到CPU 时间片的执行权后,会做一些 UI 的绘制工作,还会处理用户的输入,View动画等事件。
CPU绘制或处理事件后,会在 ReaderThread 线程中将这些数据提交给 GPU。GPU 负责对这些数据进行 栅格化(Rasterzation)操作,并将数据缓存到一个 Buffer (缓冲区) 中。
最后手机屏幕再从 Buffer 中读取数据显示到屏幕上。实际上真正对 Buffer 中数据进行合成显示的是 SurfaceFlinger。
在上图中,有一个典型的生产者/消费者模式, GPU 向缓冲区写数据,手机屏幕从缓冲区读数据。我们知道该模式是存在并发问题的,因为它符合并发的定义:多个线程同时操作共享变量。
为了避免这类问题,Android 引入了双缓冲机制:CPU 向 BackBuffer 中提交数据,GPU 对数据做栅格化,完成之后将其交换(swap)到 Frame Buffer 中,最后被手机屏幕从 FrameBuffer 中读栅格化后的数据。
看到这里,我们不难有个疑惑:交换(swap)操作是何时触发的? 我第一个想到的是 只要CPU有数据写入BackBuffer,GPU将数据栅格化后就执行swap。但是如果数据量比较大,GPU栅格化处理数据的时间期间,CPU又写入了数据,此时,就会产生矛盾。那么 Android 是如何实现的那?
由于GPU 合成的数据会经常更新,所以它会定期的交换BackBuffer和FrameBuffer的数据,从而保证屏幕上显示的是最新数据。如果当 CPU 正在向 BackBuffer 写入数据时,GPU 会将BackBuffer锁定。如果此时正好到了交换两个Buffer的时候,那么这次swap会被忽略放弃,直接导致的结果就是屏幕上显示的依然是上一帧的数据,这就是我们常数的丢帧。因此,为了保证App能够流畅运行,我们需要在每帧 16ms(屏幕刷新率,跟硬件有关) 以内处理完所有的 CPU和GPU 的计算,绘制,渲染等操作。
Vsync
由于屏幕刷新率和GPU绘制帧率并不一定是一致的,且View绘制的时机也是随机的(调用requestLayout或invalidate的时机不固定),因此也会存在丢帧的情况。
screen refresh rate–屏幕刷新率。指的是手机屏幕每秒钟可以刷新多少次。目前在大多数的厂商手机上的屏幕刷新率是 60HZ,也就是以 16.6ms 进行一次刷新。
frame rate – GPU 绘制帧率,指的是 GPU 每秒能够合成绘制多少帧。
到这里,我们不难想到,万一CPU与GPU的操作开始的比较晚,最新的数据没有交换到 FrameBuffer,那屏幕拿到的还是前一帧的数据,岂不是要丢帧?是的,确实会丢帧。
那么怎么解决这个问题那? 首先,我们看一下出现这个问题的根本原因是什么 —— CPU与GPU 的工作开始太晚!! 那么我们只需要让他们开始的时间同屏幕刷新保持一致不就可以了。那么怎么实现的那。
Android为了解决上面的问题,引入了 Vsync 机制。每隔 16ms 硬件层发出 Vsync 信号,应用层接收到 Vsync 信号后触发 UI 的渲染流程,同时 Vsync 也会触发 SurfaceFlinger 读取BackBuffer数据进行合成,完成后GPU 进行缓冲区的交换操作,最终屏幕从 FrameBuffer中读取最新数据。
问题:
应用层是怎么接收到 Vsync 信号的?
Choreographer
Choreographer翻译过来是 编舞者, 它 负责接收硬件层发出的 Vsync 信号并进行 View 绘制的操作。
ViewRootImpl.java
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true; // 与 invalidate 的区别
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier(); // 向MessageQueue 发出一个消息屏障,阻塞同步消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
*****
}
}
Choreographer.java
private final CallbackQueue[] mCallbackQueues; //
private Choreographer(Looper looper, int vsyncSource) {
mLooper = looper;
mHandler = new FrameHandler(looper);
------
// mCallbackQueues 是一个固定容量为4的CallbackQueue类型数组
mCallbackQueues = new CallbackQueue[CALLBACK_LAST + 1];
for (int i = 0; i <= CALLBACK_LAST; i++) {
mCallbackQueues[i] = new CallbackQueue();
}
}
public void postCallback(int callbackType, Runnable action, Object token) {
postCallbackDelayed(callbackType, action, token, 0);
}
public void postCallbackDelayed(int callbackType,
Runnable action, Object token, long delayMillis) {
----
postCallbackDelayedInternal(callbackType, action, token, delayMillis);
}
private void postCallbackDelayedInternal(int callbackType,
Object action, Object token, long delayMillis) {
synchronized (mLock) {
final long now = SystemClock.uptimeMillis();
final long dueTime = now + delayMillis;
// 将 任务加入回调队列中。可以去看下源码,同 handler 的message一样 callbackQueue也使用链表实现了对象池
mCallbackQueues[callbackType].addCallbackLocked(dueTime, action, token);
if (dueTime <= now) { // 如果到期时间到了
scheduleFrameLocked(now);
} else { // 向MessageQueue发送一个异步消息。至于为什么是异步消息,你猜?
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
msg.arg1 = callbackType;
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, dueTime);
}
}
}
private void scheduleFrameLocked(long now) {
if (!mFrameScheduled) {
mFrameScheduled = true; // 一次操作只能进入一次
if (USE_VSYNC) { // 使用 vsync 信号
if (isRunningOnLooperThreadLocked()) {
scheduleVsyncLocked();
} else {
Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_VSYNC);
msg.setAsynchronous(true);
mHandler.sendMessageAtFrontOfQueue(msg);
}
} else {
final long nextFrameTime = Math.max(
mLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now);
Message msg = mHandler.obtainMessage(MSG_DO_FRAME);
msg.setAsynchronous(true);
mHandler.sendMessageAtTime(msg, nextFrameTime);
}
}
}
// 注册 vsync 信号
private void scheduleVsyncLocked() {
mDisplayEventReceiver.scheduleVsync(); // 执行了DisplayEventReceiver 中的scheduleVsync(),内部调用了一个native方法
}
// 接收到 vsync 信号后的处理
void doFrame(long frameTimeNanos, int frame) {
final long startNanos;
// 1. 进行掉帧逻辑计算,并添加用于性能分析的 Trace 日志;
synchronized (mLock) {
if (!mFrameScheduled) {
return; // no work to do
}
long intendedFrameTimeNanos = frameTimeNanos;
startNanos = System.nanoTime();
final long jitterNanos = startNanos - frameTimeNanos;
if (jitterNanos >= mFrameIntervalNanos) {
final long skippedFrames = jitterNanos / mFrameIntervalNanos;
if (skippedFrames >= SKIPPED_FRAME_WARNING_LIMIT) {
Log.i(TAG, "Skipped " + skippedFrames + " frames! "
+ "The application may be doing too much work on its main thread.");
}
final long lastFrameOffset = jitterNanos % mFrameIntervalNanos;
frameTimeNanos = startNanos - lastFrameOffset;
}
if (frameTimeNanos < mLastFrameTimeNanos) {
scheduleVsyncLocked();
return;
}
if (mFPSDivisor > 1) {
long timeSinceVsync = frameTimeNanos - mLastFrameTimeNanos;
if (timeSinceVsync < (mFrameIntervalNanos * mFPSDivisor) && timeSinceVsync > 0) {
scheduleVsyncLocked();
return;
}
}
mFrameInfo.setVsync(intendedFrameTimeNanos, frameTimeNanos);
mFrameScheduled = false;
mLastFrameTimeNanos = frameTimeNanos;
}
// 2. 执行各种 callbacks并做性能跟踪
try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, "Choreographer#doFrame");
AnimationUtils.lockAnimationClock(frameTimeNanos / TimeUtils.NANOS_PER_MS);
mFrameInfo.markInputHandlingStart();
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos); // 执行输入事件回调任务,例如:点击
mFrameInfo.markAnimationsStart();
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos); // 执行animation 回调任务
mFrameInfo.markPerformTraversalsStart();
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); // 执行刷新UI任务
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
} finally {
AnimationUtils.unlockAnimationClock();
Trace.traceEnd(Trace.TRACE_TAG_VIEW);
}
}
// vsync 信号的接收器
FrameDisplayEventReceiver.java
// 接收 vsync 信号的回调
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) {
timestampNanos = now;
}
if (mHavePendingVsync) {
} else {
mHavePendingVsync = true;
}
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;
doFrame(mTimestampNanos, mFrame);
}
总结:
- 当我们刷新UI 时,会向Choreographer维护的 mCallbackQueues 中添加type为 CALLBACK_TRAVERSAL 的一个 CallbackQueue,接着会订阅一次 vsync 信号。Android硬件会 每隔 16.6ms 会向订阅 vsync 信号的接收者 发送一个 vsync 信号,这样做的好处比较明显,当UI 没有发生改变时,我们不需要注册 vsync 信号,也就不会执行刷新操作。
- Choreographer 起到了一个承上启下的作用
- 承上 接收应用层的各种 callback 任务,包括 input, animation, traversal绘制,这些任务不会立即执行,而是先被缓存到 mCallbackQueues 中
- 启下 内部类 FrameDisplayEventReceiver 接收来自硬件层的 vsync 信号,然后在 doFrame 中执行 mCallbackQueues 中的 callback 任务,例如 traversal 事件,就会执行 ViewRootImpl 中的 mTraversalRunnable,最后执行绘制流程。
注意:每次订阅只能收到一次 vsync 信号,如果需要收到下次信号则需要重新订阅。比如 Animation 的实现就是在订阅一次信号之后,紧接着再次调用 nativeScheduleVsync 方法订阅下次 vsync 信号,因此会不断地刷新 UI。
Systrace 工具
看到上面Android整个刷新屏幕的机制后,我最大的感触就是:时间,时间,时间。 既然 Systrace 是目前使用最广的UI性能分析工具,那它肯定把每一个方法的执行时间都记录了下来,我们只需要在网页文件中,找到耗时最长的一些方法,然后去优化他们,就能解决性能部分问题。
卡顿常出现的场景
- 在 animation 监听回调中创建对象或执行了比较耗时的计算操作,或者 animation 有开始没有结束
- 在 自定义view 的draw方法创建对象或执行了比较耗时的计算操作
- 在 自定义 view 的measure或layout中执行了耗时操作
- 在 执行decodeBitmap时用时比较长。