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 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 ,喜歡自取。

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章