自定义UI卡顿性能检测工具类

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卡顿。

猜你喜欢

转载自blog.csdn.net/qq_20089667/article/details/83448074