UI卡顿作为严重影响用户体验的性能指标,检测UI卡顿是一个很容易让开发者头疼的活。其原因:UI线程进行了耗时操作。说起来简单,但是真正实施起来怎么检测呢?
随着app迭代升级,内部代码量的增多,如果检测方法不正确,可能会导致事倍功半的效果;如果有良好的工具或工具类做铺垫,那检测UI卡顿会是一件很快乐的事(仅仅是检测,忽略解决方案哈)。
因此封装一个优质的检测UI卡顿的工具类尤其重要!
关于handler的工作原理,网上资料铺天盖地。当我们结合源码走一遍内部代码流程时,在Looper的检索发来的消息时是在loop()方法里处理的,会发现有这么一段:
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
final long slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;
final long traceTag = me.mTraceTag;
if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
}
final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
final long end;
try {
msg.target.dispatchMessage(msg);
end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag);
}
}
if (slowDispatchThresholdMs > 0) {
final long time = end - start;
if (time > slowDispatchThresholdMs) {
Slog.w(TAG, "Dispatch took " + time + "ms on "
+ Thread.currentThread().getName() + ", h=" +
msg.target + " cb=" + msg.callback + " msg=" + msg.what);
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// Make sure that during the course of dispatching the
// identity of the thread wasn't corrupted.
final long newIdent = Binder.clearCallingIdentity();
if (ident != newIdent) {
Log.wtf(TAG, "Thread identity changed from 0x"
+ Long.toHexString(ident) + " to 0x"
+ Long.toHexString(newIdent) + " while dispatching to "
+ msg.target.getClass().getName() + " "
+ msg.callback + " what=" + msg.what);
}
msg.recycleUnchecked();
}
我只关注消息是怎么传递的,即msg.target.dispatchMessage(msg)的执行。
实际上此方法执行前 有个Printer对象 logging,此对象会在msg.target.dispatchMessage(msg)执行前后以此打印了
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
及
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
那么我就可以通过
Looper.getMainLooper().setMessageLogging()
这个方法来检测每发送一个消息的前后执行了多长时间!!具体代码:
public class CheckUISmoothUtil {
private static final String START = ">>>>> Dispatching";
private static final String END = "<<<<< Finished";
public static boolean isDebug = true;
public static void check() {
if (isDebug) {
Looper.getMainLooper().setMessageLogging(x -> {
if (x.startsWith(START)) {//发送消息之前匹配到Looper源码里的START
CheckUILogPrinter.getInstance().startMonitor();
}
if (x.startsWith(END)) {//发送消息之后匹配到Looper源码里的END
CheckUILogPrinter.getInstance().removeMonitor();
}
});
}
}
}
这样的话我可以自定义一个阀值比如500ms,如果匹配到Looper分发消息之前发送的“>>>>> Dispatching
”,那么就立刻启动一个定时任务,时长为阀值,任务内容是打印堆栈信息;如果匹配到Looper分发消息之后发送的“<<<<< Finished
”就再把这个任务移除。这样做的后果是:只有代码里每个消息实际执行时间大于阀值才会被打印出堆栈信息,否则不打印。仅仅通过查看打印日志就能找到app卡顿的元凶!
具体实现代码:
public class CheckUILogPrinter {
public static final long TIME_BLOCK = 500;
private static CheckUILogPrinter mInstance = new CheckUILogPrinter();
private static Runnable mLogRunnable = () -> {
StringBuilder sb = new StringBuilder();
StackTraceElement[] elements = Looper.getMainLooper().getThread().getStackTrace();
for (StackTraceElement element : elements) {
sb.append(element.toString() + "\n");
}
Log.e("CheckUILogPrinter", "这个地方比较卡哦!以下是堆栈信息:");
Log.e("CheckUILogPrinter", sb.toString());
Log.e("CheckUILogPrinter", "卡顿日志打印结束");
};
private HandlerThread mHandlerThread = new HandlerThread("CheckUILogPrinter");
private Handler mHandler;
private CheckUILogPrinter() {
mHandlerThread.start();
mHandler = new Handler(mHandlerThread.getLooper());
}
public static CheckUILogPrinter getInstance() {
return mInstance;
}
public void startMonitor() {
mHandler.postDelayed(mLogRunnable, TIME_BLOCK);
}
public void removeMonitor() {
mHandler.removeCallbacks(mLogRunnable);
}
}
用法直接在Application里的onCreate方法里:
CheckUISmoothUtil.check();
检测举例1:
10-27 18:03:24.609 19096-19108/com.example.administrator.myapplication E/TAG: 这个地方比较卡哦!以下是堆栈信息:
10-27 18:03:24.609 19096-19108/com.example.administrator.myapplication E/TAG: java.lang.VMThread.sleep(Native Method)
java.lang.Thread.sleep(Thread.java:1013)
java.lang.Thread.sleep(Thread.java:995)
com.example.administrator.myapplication.MainActivity.onResume(MainActivity.java:119)
android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1209)
android.app.Activity.performResume(Activity.java:5310)
android.app.ActivityThread.performResumeActivity(ActivityThread.java:2776)
android.app.ActivityThread.handleResumeActivity(ActivityThread.java:2815)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2248)
android.app.ActivityThread.access$800(ActivityThread.java:135)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1196)
android.os.Handler.dispatchMessage(Handler.java:102)
android.os.Looper.loop(Looper.java:136)
android.app.ActivityThread.main(ActivityThread.java:5019)
java.lang.reflect.Method.invokeNative(Native Method)
java.lang.reflect.Method.invoke(Method.java:515)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
dalvik.system.NativeStart.main(Native Method)
10-27 18:03:24.619 19096-19108/com.example.administrator.myapplication E/TAG: 卡顿日志打印结束
10-27 18:03:25.289 19096-19096/com.example.administrator.myapplication E/tag: time=140s
定位代码:主线程睡了1.2秒
举例2:
10-29 09:22:52.544 7681-7717/com.example.administrator.myapplication E/TAG: com.example.administrator.myapplication.MainActivity.pintLog(MainActivity.java:134)
com.example.administrator.myapplication.MainActivity.onResume(MainActivity.java:124)
android.app.Instrumentation.callActivityOnResume(Instrumentation.java:1292)
android.app.Activity.performResume(Activity.java:6286)
android.app.ActivityThread.performResumeActivity(ActivityThread.java:3438)
android.app.ActivityThread.handleResumeActivity(ActivityThread.java:3491)
android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2799)
android.app.ActivityThread.access$900(ActivityThread.java:186)
android.app.ActivityThread$H.handleMessage(ActivityThread.java:1597)
android.os.Handler.dispatchMessage(Handler.java:111)
android.os.Looper.loop(Looper.java:194)
android.app.ActivityThread.main(ActivityThread.java:5905)
java.lang.reflect.Method.invoke(Native Method)
java.lang.reflect.Method.invoke(Method.java:372)
com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1127)
com.android.internal.os.ZygoteInit.main(ZygoteInit.java:893)
10-29 09:22:52.544 7681-7717/com.example.administrator.myapplication E/TAG: 卡顿日志打印结束
定位代码:124行printLog()方法有问题
点进去一看,里面算法逻辑有问题,有个死循环。
实际开发中,常出现UI卡顿的地方:
1.主线程进行耗时操作,比如bitmap创建,IO流读写,甚至json转bean类数据量大的话都有可能很耗时
2.算法漏洞引发的,比如主线程里有死循环或递归;也可能是for循环嵌套,这样大O记法可是指数增长!
3.对于android开发来说for循环,或者调用比较频繁的方法里动态addView也可能会造成UI卡顿。