應用的流暢度最直接的影響了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/