微信号:jszj2014215

介绍:主要分享移动互联网的相关产品和资讯,关注你将学习到更多,在互联网的当下你会赚更多的钱...

Android卡顿检测方案

2017-06-09 11:26 Android技术之家

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

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

  • 分析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 AndroidPerformanceToolsApplicationextends 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 BlockLooperimplements 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 BlockErrorextends Error{    private BlockError(ThreadStackInfoWrapper.ThreadStackInfo threadStackInfo){        super("BlockLooper Catch BlockError", threadStackInfo);
    }    public staticBlockErrorgetUiThread(){
        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 staticBlockErrorgetAllThread(){        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);

    }

	...

}

 

来自:http://blog.coderclock.com/2017/06/04/android/AndroidPerformanceTools-BlockLooper/


 
Android技术之家 更多文章 【官方建议】关于提高Android程序效率的几点建议 App开发架构指南(谷歌官方文档译文) 【实战】Android Data Binding从抵触到爱不释手 带你学开源项目:LeakCanary-如何检测 Activity 是否泄漏 Google放出Android Studio 3.0,包含大量新功能和改进
猜您喜欢 希云cSphere联合极客邦,让Docker实训惠及更多开发者! PHP之OPP解读 旋转立方体 Python开发技能表 什么才是真正的Actor模型