当前位置 博文首页 > 历史上的今天:Android 卡顿检测方案

    历史上的今天:Android 卡顿检测方案

    作者:[db:作者] 时间:2021-09-01 10:46

    应用的流畅度最直接的影响了 App 的用户体验,轻微的卡顿有时导致用户的界面操作需要等待一两秒钟才能生效,严重的卡顿则导致系统直接弹出 ANR 的提示窗口,让用户选择要继续等待还是关闭应用。

    所以,如果想要提升用户体验,就需要尽量避免卡顿的产生,否则用户经历几次类似场景之后,只会动动手指卸载应用,再顺手到应用商店给个差评。关于卡顿的分析方案,已经有以下两种:

    • 分析 trace 文件。通过分析系统的/data/anr/traces.txt,来找到导致 UI 线程阻塞的源头,这种方案比较适合开发过程中使用,而不适合线上环境;
    • 使用 BlockCanary 开源方案。其原理是利用 Looper 中的 loop 输出的>>>>> Dispatching to<<<<< Finished to 这样的 log,这种方案适合开发过程和上线的时候使用,但也有个弊端,就是如果系统移除了前面两个 log,检测可能会面临失效。

    下面就开始说本文要提及的卡顿检测实现方案,原理简单,代码量也不多,只有 BlockLooperBlockError 两个类。

    基本使用

    在 Application 中调用 BlockLooper.initialize 进行一些参数初始化,具体参数项可以参照 BlockLooper 中的 Configuration 静态内部类,当发生卡顿时,则会在回调(非 UI 线程中)OnBlockListener

    public class AndroidPerformanceToolsApplication extends Application {
        private final static String TAG = AndroidPerformanceToolsApplication.class.getSimpleName();
        @Override
        public void onCreate() {
            super.onCreate();
            // 初始化相关配置信息
            BlockLooper.initialize(new BlockLooper.Builder(this)
                    .setIgnoreDebugger(true)
                    .setReportAllThreadInfo(true)
                    .setSaveLog(true)
                    .setOnBlockListener(new BlockLooper.OnBlockListener() {//回调在非 UI 线程
                        @Override
                        public void onBlock(BlockError blockError) {
                            blockError.printStackTrace();//把堆栈信息输出到控制台
                        }
                    })
                    .build());
        }
    }

    在选择要启动(停止)卡顿检测的时候,调用对应的 API。

    BlockLooper.getBlockLooper().start();//启动检测
    BlockLooper.getBlockLooper().stop();//停止检测

    使用上很简单,接下来看一下效果演示和源码实现。

    效果演示

    制造一个 UI 阻塞效果:

    看看 AS 控制台输出的整个堆栈信息:

    定位到对应阻塞位置的源码:

    当然,对线程的信息 BlockLooper 也不仅输出到控制台,也会帮你缓存到 SD 上对应的应用缓存目录下,在 SD 卡上的/Android/data/对应 App 包名/cache/block/下可以找到,文件名是发生卡顿的时间点,后缀是 trace。

    源码解读

    当 App 在 5s 内无法对用户做出的操作进行响应时,系统就会认为发生了 ANR。BlockLooper 实现上就是利用了这个定义,它继承了 Runnable 接口,通过 initialize 传入对应参数配置好后,通过 BlockLooper 的 start()创建一个 Thread 来跑起这个 Runnable,在没有 stop 之前,BlockLooper 会一直执行 run 方法中的循环,执行步骤如下:

    • Step1. 判断是否停止检测 UI 线程阻塞,未停止则进入 Step2;
    • Step2. 使用 uiHandler 不断发送 ticker 这个 Runnable,ticker 会对 tickCounter 进行累加;
    • Step3. BlockLooper 进入指定时间的 sleep(frequency 是在 initialize 时传入,最小不能低于 5s);
    • Step4. 如果 UI 线程没有发生阻塞,则 sleep 过后,tickCounter 一定与原来的值不相等,否则一定是 UI 线程发生阻塞;
    • Step5. 发生阻塞后,还需判断是否由于 Debug 程序引起的,不是则进入 Step6;
    • Step6. 回调 OnBlockListener,以及选择保存当前进程中所有线程的堆栈状态到 SD 卡等。
    public class BlockLooper implements Runnable {
        ...
        private Handler uiHandler = new Handler(Looper.getMainLooper());
        private Runnable ticker = new Runnable() {
            @Override
            public void run() {
                tickCounter = (tickCounter + 1) % Integer.MAX_VALUE;
            }
        };
        ...
        private void init(Configuration configuration) {
            this.appContext = configuration.appContext;
            this.frequency = configuration.frequency < DEFAULT_FREQUENCY ? DEFAULT_FREQUENCY : configuration.frequency;
            this.ignoreDebugger = configuration.ignoreDebugger;
            this.reportAllThreadInfo = configuration.reportAllThreadInfo;
            this.onBlockListener = configuration.onBlockListener;
            this.saveLog = configuration.saveLog;
        }
        @Override
        public void run() {
            int lastTickNumber;
            while (!isStop) { //Step1
                lastTickNumber = tickCounter;
                uiHandler.post(ticker); //Step2
                try {
                    Thread.sleep(frequency); //Step3
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }
                if (lastTickNumber == tickCounter) { //Step4
                    if (!ignoreDebugger && Debug.isDebuggerConnected()) { //Step5
                        Log.w(TAG, "当前由调试模式引起消息阻塞引起 ANR,可以通过 setIgnoreDebugger(true)来忽略调试模式造成的 ANR");
                        continue;
                    }
                    BlockError blockError; //Step6
                    if (!reportAllThreadInfo) {
                        blockError = BlockError.getUiThread();
                    } else {
                        blockError = BlockError.getAllThread();
                    }
                    if (onBlockListener != null) {
                        onBlockListener.onBlock(blockError);
                    }
                    if (saveLog) {
                        if (StorageUtils.isMounted()) {
                            File logDir = getLogDirectory();
                            saveLogToSdcard(blockError, logDir);
                        } else {
                            Log.w(TAG, "sdcard is unmounted");
                        }
                    }
                }
            }
        }
        ...
        public synchronized void start() {
            if (isStop) {
                isStop = false;
                Thread blockThread = new Thread(this);
                blockThread.setName(LOOPER_NAME);
                blockThread.start();
            }
        }
        public synchronized void stop() {
            if (!isStop) {
                isStop = true;
            }
        }
        ...
        ...
    }

    介绍完 BlockLooper 后,再简单说一下 BlockError 的代码,主要有 getUiThread 和 getAllThread 两个方法,分别用户获取 UI 线程和进程中所有线程的堆栈状态信息,当捕获到 BlockError 时,会在 OnBlockListener 中以参数的形式传递回去。

    public class BlockError extends Error {
        private BlockError(ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo) {
            super("BlockLooper Catch BlockError", threadStackInfo);
        }
        public static BlockError getUiThread() {
            Thread uiThread = Looper.getMainLooper().getThread();
            StackTraceElement[] stackTraceElements = uiThread.getStackTrace();
            ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo = new ThreadStackInfoWrapper(getThreadNameAndState(uiThread), stackTraceElements)
                    .new ThreadStackInfo(null);
            return new BlockError(threadStackInfo);
        }
        public static BlockError getAllThread() {
            final Thread uiThread = Looper.getMainLooper().getThread();
            Map<Thread, StackTraceElement[]> stackTraceElementMap = new TreeMap<Thread, StackTraceElement[]>(new Comparator<Thread>() {
                @Override
                public int compare(Thread lhs, Thread rhs) {
                    if (lhs == rhs) {
                        return 0;
                    } else if (lhs == uiThread) {
                        return 1;
                    } else if (rhs == uiThread) {
                        return -1;
                    }
                    return rhs.getName().compareTo(lhs.getName());
                }
            });
            for (Map.Entry<Thread, StackTraceElement[]> entry : Thread.getAllStackTraces().entrySet()) {
                Thread key = entry.getKey();
                StackTraceElement[] value = entry.getValue();
                if (value.length > 0) {
                    stackTraceElementMap.put(key, value);
                }
            }
            //Fix 有时候 Thread.getAllStackTraces()不包含 UI 线程的问题
            if (!stackTraceElementMap.containsKey(uiThread)) {
                stackTraceElementMap.put(uiThread, uiThread.getStackTrace());
            }
            ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo = null;
            for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraceElementMap.entrySet()) {
                Thread key = entry.getKey();
                StackTraceElement[] value = entry.getValue();
                threadStackInfo = new ThreadStackInfoWrapper(getThreadNameAndState(key), value).
                        new ThreadStackInfo(threadStackInfo);
            }
            return new BlockError(threadStackInfo);
        }
        ...
    }

    总结

    以上就是 BlockLooper 的实现,非常简单,相信大家都看得懂。源码地址:https://github.com/D-clock/AndroidPerformanceTools,喜欢自取。

    作者: 纪喜才(@D_clock爱吃葱花),YY Android开发工程师,热爱开源并学习开源,热衷于技术分享。个人博客:http://blog.coderclock.com/,Github 地址:https://github.com/D-clock
    责编: 唐小引,欢迎技术投稿、约稿、给文章纠错,请发送邮件至tangxy@csdn.net。
    感谢作者的辛苦撰文分享,技术之路,共同进步。

    cs