本文首發於微信公衆號「Android開發之旅」,歡迎關注git
用戶在使用咱們應用的時候,不少問題是很難被及時的發現的好比內存佔用高,耗費流量等,可是一旦發生卡頓就會被用戶直觀的感覺到。因此應用卡頓是很影響用戶體驗的。另一方面,對於開發者來講,卡頓的問題很難定位,發生問題的緣由錯綜複雜,好比:代碼問題、內存問題、繪製問題以及IO操做等等。並且線上發生的卡頓問題在線下咱們很難復現,由於這和用戶當時的系統環境有很大的關係,所以咱們須要在用戶發送卡頓的時候記錄下用戶使用的場景等。好比:內存消耗,磁盤空間,用戶行爲路徑等等。github
目前Android Studio以及自帶了CPU Profiler工具,它能夠以圖形化的形式展現執行的時間、調用棧等信息。收集的信息比較全面,包含了全部線程。可是因爲其收集信息全面,致使了運行時內存開銷嚴重,App函數總體運行都會變慢,可能會帶偏咱們的優化方向。後端
使用方式: Debug.startMethodTracing(); ... Debug.stopMethodTracing(); 最終生成的文件在sd卡:Android/data/packagename/files目錄下。性能優化
Systrace以前文章已經講解過,它是輕量級的框架,並且開銷小,能夠直觀反映CPU的利用率並且右側alter能夠針對一些問題給出相關的建議。 好比繪製慢或者GC頻繁等。bash
Android2.3引入的一個工具類:嚴苛模式。是一種運行時檢測機制。能夠幫助開發人員檢測代碼當中不規範的問題。StrictMode主要檢測線程策略和虛擬機策略。微信
線程策略包括:網絡
自定義的耗時調用,detectCustimSlowCalls框架
磁盤讀取操做,detectDiskReadside
網絡操做,detectNetwork函數
虛擬機策略:
Activity泄漏,detectActivityLeaks
Sqlite對象泄漏,detectLeakedSqlLiteObjects
檢測實例數量,setClassInstanceLimit
咱們在Application中使用:
private void initStrictMode() {
if (BuildConfig.DEBUG) {
//線程
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectCustomSlowCalls() //API等級11,使用StrictMode.noteSlowCode
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()// 或者直接使用 .detectAll() 手機所有信息
.penaltyLog() //在Logcat 中打印違規異常信息,還能夠選擇彈框提示或者直接奔潰等
.build());
//虛擬機
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.setClassInstanceLimit(StrictModeTest.class, 1)
.detectLeakedClosableObjects() //API等級11
.penaltyDropBox()
.build());
}
}
複製代碼
StrictMode自己也是耗性能的,因此咱們只在debug模式下開啓。當出現不符合檢測策略的時候就會在控制檯打印日誌,輸入StrictMode關鍵詞過濾便可。
CPU Profiler 和 Systrace 都是適合線下使用的,沒法帶到線上。那咱們如何作到線上監測卡頓呢?
咱們都知道一個進程中只有個Looper對象,咱們經過查看Looper源碼發現,在其loop方法中的死循環中有個mLogging對象,在執行的時候打印了一個Dispatching to日誌,執行完成的時候有打印了一個Finished to日誌。如:
public static void loop() {
// ....省略開始代碼...
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// This must be in a local variable, in case a UI event sets the logger
final Printer logging = me.mLogging;
if (logging != null) {
//重點 開始打印
logging.println(">>>>> Dispatching to " + msg.target + " " +
msg.callback + ": " + msg.what);
}
// ...省略中間代碼...
if (logging != null) {
//重點 完成打印
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
// ...省略最後代碼...
}
}
複製代碼
因此咱們能夠自定義Printer對象,讓Handler的日誌都經過咱們自定義的Printer進行打印,而後收集日誌信息,匹配Dispatching to和Finished to字段,若是在設定的某個時間內只有Dispatching to字段而沒有Finished to字段,那麼就說明發生了卡頓。發生卡頓後咱們就收集此時的調用棧信息。相反若是兩個字段都存在則說明應用運行的很流暢。
字段Printer設置給mLogging對象:
Looper.getMainLooper().setMessageLogging(new Printer() {
@Override
public void println(String log) {
Log.e("printer","==println=="+log);
}
});
複製代碼
代碼中的log字段就是咱們須要的Dispatch和Finished字段,咱們監測這兩個字段並收集調用棧信息將其發送到後端進行分析使用。
那麼這裏其實還存在一個問題就是可能咱們收集的信息不夠準確,爲何呢?就是咱們收集的調用棧信息是最後收集的,那麼這個時候有可能卡頓已經執行完成了,此刻蒐集到的信息有可能不是卡頓發生的關鍵信息。就像OOM同樣,它是一個隨時都有可能發生的。因此咱們須要高頻率的收集日誌信息,高頻率的收集對後端有必定的壓力,而咱們高頻收集的信息有很大一部分也是重複的,因此就須要日誌去重操做。
ANR異常全稱 Application Not Responding,即應用無響應。若是你的應用程序有一段時間響應不夠靈敏,系統會向用戶顯示一個對話框,這個對話框稱做應用程序無響應對話框,用戶能夠選擇「等待」而讓程序繼續運行,也能夠選擇「強制關閉」。因此一個流暢的合理的應用程序中不能出現anr。由於這很影響用戶的使用體驗,固然因爲廠商深度定製系統的緣由,在某些手機上發生ANR也不會彈框的。
發生ANR到彈框在不一樣的組件之間時間定義是不同的,按鍵是5秒。前臺廣播10秒,後臺廣播60秒。前臺服務20秒,後臺服務200秒。這些數據都定義在AMS中,讀者能夠去看看。
ANR發生執行的流程:
ANR的日誌在data/anr/traces.txt目錄下。
咱們在線下的時候能夠直接經過ADB命令來查看日誌:
adb pull data/anr/traces.txt 你的目錄 這樣能夠詳細分析CPU、IO、鎖等操做的問題所在。
線上咱們可使用FileObserver監控文件變化,可是這種方法在高版本系統中有權限問題。另一種就是使用AnrWatchDog框架。這也是一個開源框架,地址:github.com/SalomonBrys… 它的原理就是經過修改值的方式判斷UI線程是否發生卡頓。
這個庫使用也很是簡單,首先在gradle中配置:
compile 'com.github.anrwatchdog:anrwatchdog:1.4.0'
複製代碼
而後在Application中進行初始化:
new ANRWatchDog().start();
複製代碼
這樣就能夠了。默認檢測到卡頓就直接拋ANRError異常將應用奔潰,咱們能夠複寫Listener接口來抓取堆棧信息。
ANRWatchDog是繼承之Thread線程的,那麼咱們就看下核心方法run方法中的代碼邏輯。
// post的操做
private final Runnable _ticker = new Runnable() {
@Override public void run() {
_tick = 0;
_reported = false;
}
};
複製代碼
@Override
public void run() {
// 首先對線程進行重命名
setName("|ANR-WatchDog|");
long interval = _timeoutInterval;
while (!isInterrupted()) {
boolean needPost = _tick == 0;
_tick += interval;
if (needPost) {
// 發送post
_uiHandler.post(_ticker);
}
try {
// 睡眠
Thread.sleep(interval);
} catch (InterruptedException e) {
_interruptionListener.onInterrupted(e);
return ;
}
// If the main thread has not handled _ticker, it is blocked. ANR.
if (_tick != 0 && !_reported) {
//noinspection ConstantConditions
if (!_ignoreDebugger && (Debug.isDebuggerConnected() || Debug.waitingForDebugger())) {
Log.w("ANRWatchdog", "An ANR was detected but ignored because the debugger is connected (you can prevent this with setIgnoreDebugger(true))");
_reported = true;
continue ;
}
interval = _anrInterceptor.intercept(_tick);
if (interval > 0) {
continue;
}
final ANRError error;
if (_namePrefix != null) {
error = ANRError.New(_tick, _namePrefix, _logThreadsWithoutStackTrace);
} else {
error = ANRError.NewMainOnly(_tick);
}
_anrListener.onAppNotResponding(error);
interval = _timeoutInterval;
_reported = true;
}
}
}
複製代碼
使用ANRWatchDog的緣由就是它是非侵入式的,而且能夠彌補高版本權限問題。兩者結合使用。
以上就是對應用卡頓檢測的方法。那麼具體如何規避卡頓,這就要求咱們在平時的開發中養成良好的代碼習慣。書寫高質量代碼。具體請看下面往期回顧中的佈局優化實戰。