【转载】Android卡顿检测方案

摘要:
[化身]当然,有关线程BlockLooper的信息不仅会输出到控制台,还会帮助您将其缓存到SD卡上相应的应用程序缓存目录。您可以在SD卡上的/Android/data/对应的应用程序包名称/cache/block/下找到它。文件名是果酱的时间点,后缀是trace。!

应用的流畅度最直接的影响了App的用户体验,轻微的卡顿有时导致用户的界面操作需要等待一两秒钟才能生效,严重的卡顿则导致系统直接弹出ANR的提示窗口,让用户选择要继续等待还是关闭应用。
avatar
所以,如果想要提升用户体验,就需要尽量避免卡顿的产生,否则用户经历几次类似场景之后,只会动动手指卸载应用,再顺手到应用商店给个差评。关于卡顿的分析方案,已经有以下两种:

  • 分析trace文件。通过分析系统的/data/anr/traces.txt,来找到导致UI线程阻塞的源头,这种方案比较适合开发过程中使用,而不适合线上环境;
  • 使用BlockCanary开源方案。其原理是利用Looper中的loop输出的>>>>> Dispatching to和<<<<< Finished to这样的log,这种方案适合开发过程和上线的时候使用,但也有个弊端,就是如果系统移除了前面两个log,检测可能会面临失效; 下面就开始说本文要提及的卡顿检测实现方案,原理简单,代码量也不多,只有BlockLooper和BlockError两个类。
  • ###基本使用 在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阻塞效果
    ![avatar](https://diycode.b0.upaiyun.com/photo/2017/a0711bf9eca0a69690c22bfa6d6e51b0.gif)
    看看AS控制台输出的整个堆栈信息
    ![avatar](https://diycode.b0.upaiyun.com/photo/2017/777131394ce6727a9edfcbbeb27ed32e.png)
    定位到对应阻塞位置的源码
    ![avatar](https://diycode.b0.upaiyun.com/photo/2017/814026342900e882d5d1e328d3afc197.png)
    当然,对线程的信息BlockLooper也不仅输出到控制台,也会帮你缓存到SD上对应的应用缓存目录下,在SD卡上的/Android/data/对应App包名/cache/block/下可以找到,文件名是发生卡顿的时间点,后缀是trace。
    ![avatar](https://diycode.b0.upaiyun.com/photo/2017/9474c33d1e8341ec517a7cd2fa7dfcc1.gif)
    ###源码解读
    当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 stackTraceElementMap = new TreeMap(new Comparator() {
                @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 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 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 ,喜欢自取。 转载自:http://blog.coderclock.com/2017/06/04/android/AndroidPerformanceTools-BlockLooper/

    免责声明:文章转载自《【转载】Android卡顿检测方案》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

    上篇c#里excel转换成csv的万能方法[整理]多项式/生成函数题目泛刷下篇

    宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

    相关文章

    python基础整理5——多进程多线程和协程

    进程与线程 1.进程 我们电脑的应用程序,都是进程,假设我们用的电脑是单核的,cpu同时只能执行一个进程。当程序处于I/O阻塞的时候,CPU如果和程序一起等待,那就太浪费了,cpu会去执行其他的程序,此时就涉及到切换,切换前要保存上一个程序运行的状态,才能恢复,所以就需要有个东西来记录这个东西,就可以引出进程的概念了。 进程就是一个程序在一个数据集上的一次...

    winform知识

    控件相关 1.文本框/label高度 文本框Multiline属性,设为true就可以了。改完高度后再将此属性改回来,要不然多行文本框,按回去就去下一行了。 label的改autoSize属性,设为false就可以了。 2.控件中文字居中 TextAlign属性:MiddleCenter 3.颜色属性 直接输入 #xxxx 4.如何去掉button按钮的...

    JS的"多线程"

    这个系列的文章名为“JavaScript 进阶”,内容涉及JS中容易忽略但是很有用的,偏JS底层的,以及复杂项目中的JS的实践。主要来源于我几年的开发过程中遇到的问题。小弟第一次写博客,写的不好的地方请诸位斧正,觉得还有一些阅读价值的请帮忙分享下。这个“JavaScript 进阶”是一个系列文章,请大家鼓励鼓励,我尽快更新。另外,如果你有比较好的话题,也可...

    多线程之旅七——GUI线程模型,消息的投递(post)与处理

    基于消息的GUI构架 在过去的日子中,大部分编程语言平台的GUI构架几乎没有发生变化。虽然在细节上存在一些差异,比如在功能和编程风格上,但大部分都是采用了相同的构架来响应用户输入以及重新绘制屏幕。这种构架可以被总结为“单线程且基于消息”。   Message msg; While(GetMessage(msg)) { TranslateMess...

    安卓开发复习笔记(一)

    第一章 安卓应用开发特色: • 四大组件Activity,Service,Broadcast Receiver,Content Provider ️• SQLite 数据库轻量级,运算速度极快的嵌入式关系型数据库,不仅支持sql语句,还可以通过安卓封装好的API进行操作,让存储和读取数据变得特别方便。 • 地理位置定位Android手机都内置GPS,结合强...

    网络socket编程实现并发服务器——多线程编程

    一、多线程简介1、什么是线程?       线程在操作系统原理中是这样描述的:线程是进程的一条执行路径。线程在Unix系统下,通常被称为轻量级的进程,线程虽然不是进程,但却可以看作是Unix进程的表亲,所有的线程都是在同一进程空间运行,这也意味着多条线程将共享该进程中的全部系统资源,如虚拟地址空间,文件描述符和信号处理等等。但同一进程中的多个线程有各自的调...