Android卡頓檢測方案

ty647226 7年前發布 | 17K 次閱讀 安卓開發 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/

 

 本文由用戶 ty647226 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!