ANR全稱是Application Not Responding,意思是應用程序無響應。相信從事Android開發的肯定遇到過。ANR的直觀體驗是用戶在操作App的過程中,感覺界面卡頓,當界面卡頓超過一定時間(一般5秒),就會出現ANR對話框。ANR對於一個應用來說是不能承受之痛,其影響並不比應用發生Crash小。
ANR產生的原因
只有當應用程序的UI線程響應超時纔會引起ANR,超時產生原因一般有兩種。
- 當前的事件沒有機會得到處理,例如UI線程正在響應另一個事件,當前事件由於某種原因被阻塞了。
- 當前的事件正在處理,但是由於耗時太長沒能及時完成。
根據ANR產生的原因不同,超時事件也不盡相同,從本質上將,產生ANR的原因有三種,大致可以對應到Android中四大組件中的三個(Activity/View,BroadcastReceiver和Service)。
KeyDispatchTimeout
最常見的一種類型,原因就是View的點擊事件或者觸摸事件在特定的時間(5s)內無法得到響應。
BroadcastTimeout
原因是BroadcastReceiver的onReceive()函數運行在主線程中,在特定的時間(10s)內無法完成處理。
ServiceTimeout
比較少出現的一種類型,原因是Service的各個生命週期函數在特定時間(20s)內無法完成處理。
典型的ANR問題場景
- 應用程序UI線程存在耗時操作。例如在UI線程中進行聯網請求,數據庫操作或者文件操作等。
- 應用程序的UI線程等待子線程釋放某個鎖,從而無法處理用戶的輸入。
- 耗時的動畫需要大量的計算工作,可能導致CPU負載過重。
ANR的定位和分析
當發生ANR時,可以通過結合Logcat日誌和生成的位於手機內部存儲的/data/anr/traces.tex文件進行分析和定位。
Cmd line: com.anly.githubapp // 最新的ANR發生的進程(包名)
...
DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
| sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
| state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
| stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:985) // 主線程中sleep過長時間, 阻塞導致無響應.
at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
- locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166) // 產生ANR的那個函數調用
- locked <@addr=0x12d1e840> (a java.lang.Class<com.tencent.bugly.crashreport.CrashReport>)
at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起點
at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5254)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
現在找到了ANR產生的原因和位置了,就可以對產生ANR的代碼進行修復。這麼一個一個的找肯定不現實,那我們就來說說如何避免和檢測ANR。
ANR的避免和檢測
爲了避免在開發中引入可能導致應用發生ANR的問題,除了切記不要在主線程中作耗時操作,我們也可以藉助於一些工具來進行檢測,從而更有效的避免ANR的引入。
StrictMode
嚴格模式StrictMode是Android SDK提供的一個用來檢測代碼中是否存在違規操作的工具類,StrictMode主要檢測兩大類問題。
- 線程策略 ThreadPolicy
- detectCustomSlowCalls:檢測自定義耗時操作
- detectDiskReads:檢測是否存在磁盤讀取操作
- detectDiskWrites:檢測是否存在磁盤寫入操作
- detectNetWork:檢測是否存在網絡操作
- 虛擬機策略VmPolicy
- detectActivityLeaks:檢測是否存在Activity泄露
- detectLeakedClosableObjects:檢測是否存在未關閉的Closeable對象泄露
- detectLeakedSqlLiteObjects:檢測是否存在Sqlite對象泄露
- setClassInstanceLimit:檢測類實例個數是否超過限制
可以看到,ThreadPolicy可以用來檢測可能催在的主線程耗時操作,需要注意的是我們只能在Debug版本中使用它,發佈到市場上的版本要關閉掉。StrictMode的使用很簡單,我們只需要在應用初始化的地方例如Application或者MainActivity類的onCreate方法中執行如下代碼:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 開啓線程模式
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()
.penaltyLog()
.penaltyDialog() ////打印logcat,當然也可以定位到dropbox,通過文件保存相應的log
.build());
// 開啓虛擬機模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()
.penaltyLog()
.build());
}
}
上面的初始化代碼調用penaltyLog表示在Logcat中打印日誌,調用detectAll方法表示啓動所有的檢測策略,我們也可以根據應用的具體要求只開啓某些策略,語句如下:
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectActivityLeaks()
.penaltyLog()
.build());
BlockCanary
BlockCanary是一個非侵入式式的性能監控函數庫,它的用法和leakCanary類似,只不過後者監控應用的內存泄露,而BlockCanary主要用來監控應用主線程的卡頓。它的基本原理是利用主線程的消息隊列處理機制,通過對比消息分發開始和結束的時間點來判斷是否超過設定的時間,如果是,則判斷爲主線程卡頓。它的集成很簡單,首先在build.gradle中添加依賴
一般選取以下其中一個 case 引入即可
dependencies {
compile 'com.github.markzhai:blockcanary-android:1.5.0'
// 僅在debug包啓用BlockCanary進行卡頓監控和提示的話,可以這麼用
debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'
releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'
}
然後在Application類中進行配置和初始化即可
public class AnrDemoApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 在主進程初始化調用哈
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
實現自己監控的上下文
public class AppBlockCanaryContext extends BlockCanaryContext {
// 實現各種上下文,包括應用標示符,用戶uid,網絡類型,卡慢判斷闕值,Log保存位置等
/**
* Implement in your project.
*
* @return Qualifier which can specify this installation, like version + flavor.
*/
public String provideQualifier() {
return "unknown";
}
/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}
/**
* Network type
*
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
public String provideNetworkType() {
return "unknown";
}
/**
* Config monitor duration, after this time BlockCanary will stop, use
* with {@code BlockCanary}'s isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
public int provideMonitorDuration() {
return -1;
}
/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
*
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 1000;
}
/**
* Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
* stack according to current sample cycle.
* <p>
* Because the implementation mechanism of Looper, real dump interval would be longer than
* the period specified here (especially when cpu is busier).
* </p>
*
* @return dump interval (in millis)
*/
public int provideDumpInterval() {
return provideBlockThreshold();
}
/**
* Path to save log, like "/blockcanary/", will save to sdcard if can.
*
* @return path of log files
*/
public String providePath() {
return "/blockcanary/";
}
/**
* If need notification to notice block.
*
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}
/**
* Implement in your project, bundle files into a zip file.
*
* @param src files before compress
* @param dest files compressed
* @return true if compression is successful
*/
public boolean zip(File[] src, File dest) {
return false;
}
/**
* Implement in your project, bundled log files.
*
* @param zippedFile zipped file
*/
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}
/**
* Packages that developer concern, by default it uses process name,
* put high priority one in pre-order.
*
* @return null if simply concern only package with process name.
*/
public List<String> concernPackages() {
return null;
}
/**
* Filter stack without any in concern package, used with @{code concernPackages}.
*
* @return true if filter, false it not.
*/
public boolean filterNonConcernStack() {
return false;
}
/**
* Provide white list, entry in white list will not be shown in ui list.
*
* @return return null if you don't need white-list filter.
*/
public List<String> provideWhiteList() {
LinkedList<String> whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}
/**
* Whether to delete files whose stack is in white list, used with white-list.
*
* @return true if delete, false it not.
*/
public boolean deleteFilesInWhiteList() {
return true;
}
/**
* Block interceptor, developer may provide their own actions.
*/
public void onBlock(Context context, BlockInfo blockInfo) {
}
}
在AndroidManifest.xml文件中聲明Application,一定不要忘記
現在就已經將BlockCanary集成到應用裏面了,接下來,編譯安裝到手機上,點擊測試按鈕,將產生一個ANR,效果如圖: