Android 卡頓檢測方案

應用的流暢度最直接的影響了 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
責編: 唐小引,歡迎技術投稿、約稿、給文章糾錯,請發送郵件至[email protected]
感謝作者的辛苦撰文分享,技術之路,共同進步。

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