Android动画卡顿分析
最近处理了一个动画卡顿的问题,记录一下分析的过程;
问题分析
看了一下,出现卡顿的是一个位移的动画,具体的实现是通过执行View的startAnimation
方法,通过自定义Animation重写applyTransformation
方法,通过对view进行layout
实现的
//开始执行动画
getItemView().startAnimation(mLayoutTranslationAnimation);
...
//位移实现
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
...
layout(endPos, getItemView().getTop(), endPos + getItemView().getWidth(), getItemView().getBottom());
...
}
猜测问题可能是:
- 动画在同一时间被重复执行了,导致位移动画的起点被重新设置,导致图标位移的跳变
- 在执行动画的过程中,主线程阻塞了导致图标的绘制时间间隔变大,导致图标位移的跳变
排查问题
第一点很容易排查,在执行动画的地方加一下日志或者断点就可以确认是否重复执行了;发现的确重复执行了,但是代码中已经做了处理不会导致问题出现;
为了确认是阻塞导致的,在applyTransformation
方法中加入日志,查看执行的时间间隔,发现每次动画的执行都有一次位置更新的间隔大于50ms,应该就是这次导致的动画有跳变;那应该就是在动画执行的过程中有主线程的操作时间较长,导致了卡顿;
//Looper大概的运行机制
public static void loop() {
...
for (;;) {
...
// This must be in a local variable, in case a UI event sets the logger
Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
//执行主线程的任务
msg.target.dispatchMessage(msg);
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
...
}
}
这里使用了一个比较简单的方法去分析具体发生卡顿的地方,根据Handler机制,主线程执行的任务都是通过msg.target.dispatchMessage(msg)
去执行的,在执行前后会通过Printer
打印相关的日志信息,可以通过设置自己实现的Printer
对象去获取每个任务执行的时间,判断该任务是否会造成卡顿;为了打印出耗时较长的任务的堆栈信息,在开始执行任务时候就需要延迟去判断是否超时了,获取当前堆栈信息;
getMainLooper().setMessageLogging(s -> {
if (s.startsWith(">>>>> Dispatching to")) {
lastTime = System.currentTimeMillis();
new Thread(() -> {
final long time = lastTime;
SystemClock.sleep(33);
if (lastTime == time) {
Arrays.stream(getMainLooper().getThread().getStackTrace())
.forEach(e -> CameraLog.e("tyhj:", "aric_stack, " + e.getClassName() + "." + e.getMethodName() + "(" + e.getFileName()
+ ":" + e.getLineNumber() + ")"));
}
}).start();
} else if (s.startsWith("<<<<< Finished to")) {
long time = System.currentTimeMillis() - lastTime;
if (time > 30) {
CameraLog.e("tyhj", "message timeOut time is " + time);
}
}
});
如果动画执行帧率要求是是30fps,那么刷新时间为33ms,在最理想的情况下(前面只有一个任务在执行的时候),这个任务的执行时间必须小于33ms;所以这里在33ms后去判断当前的任务是否执行结束了,没结束就说明会造成动画卡顿问题;然后在动画执行前后也加上日志,如果动画前后检测到有主线程任务超时就说明是该任务导致的动画卡顿;
aric_stack, com.camera.gl.SurfaceTextureScreenNail.setMultiBlur(SurfaceTextureScreenNail.java:1443)
aric_stack, com.camera.ui.preview.CameraScreenNail$3.run(CameraScreenNail.java:617)
aric_stack, android.os.Handler.handleCallback(Handler.java:938)
aric_stack, android.os.Handler.dispatchMessage(Handler.java:99)
aric_stack, android.os.Looper.loop(Looper.java:269)
aric_stack, android.app.ActivityThread.main(ActivityThread.java:8301)
aric_stack, java.lang.reflect.Method.invoke(Method.java:-2)
aric_stack, com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:612)
aric_stack, com.android.internal.os.ZygoteInit.main(ZygoteInit.java:992)
message timeOut time is 49
intervalTime is 64 interval is 13
通过日志可以看出是SurfaceTextureScreenNail.setMultiBlur
方法执行超时了;看代码是加锁未考虑主线程会被阻塞的问题,通过执行前后的日志可以进一步确认的确是这里造成的阻塞;将锁放在子线程或者根据代码去掉锁都可以解决该问题
public void setMultiBlur(boolean isMultiBlur) {
synchronized (mLock) {
mbMultiBlur = isMultiBlur;
}
}
问题总结
- APP交互卡顿的问题可以通过Handler机制简单的去定位;
- 可以利用这个机制对APP的性能进行检测监控,有利于APP性能和体验的提升,也有类似的第三方SDK比如BlockCanary。
- 在可能发生阻塞的地方需要考虑会不会阻塞主线程,防止引入性能问题