應用的流暢度最直接的影響了 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,喜歡自取。
作者: 紀喜才(@D_clock愛喫蔥花),YY Android開發工程師,熱愛開源並學習開源,熱衷於技術分享。個人博客:http://blog.coderclock.com/,Github 地址:https://github.com/D-clock
責編: 唐小引,歡迎技術投稿、約稿、給文章糾錯,請發送郵件至[email protected]。
感謝作者的辛苦撰文分享,技術之路,共同進步。