Android卡顿检测方案

栏目: IOS · Android · 发布时间: 8年前

内容简介:Android卡顿检测方案

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

Android卡顿检测方案

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

  • 分析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阻塞效果

Android卡顿检测方案

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

Android卡顿检测方案

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

Android卡顿检测方案

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

Android卡顿检测方案

源码解读

当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);

    }

	...

}

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

HTML Dog

HTML Dog

Patrick Griffiths / New Riders Press / 2006-11-22 / USD 49.99

For readers who want to design Web pages that load quickly, are easy to update, accessible to all, work on all browsers and can be quickly adapted to different media, this comprehensive guide represen......一起来看看 《HTML Dog》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具

html转js在线工具
html转js在线工具

html转js在线工具