1 ANR簡介
- ANR,是“Application Not Responding”的縮寫,即“應用程序無響應”。
- 在Android中,ActivityManagerService(簡稱AMS)和WindowManagerService(簡稱WMS)會監測應用程序的響應時間,如果應用程序主線程(即UI線程)在超時時間內對輸入事件沒有處理完畢(或沒有處理),或者對特定操作沒有執行完畢,就會出現ANR。
- 對于輸入事件沒有處理完畢產生的ANR,Android會顯示一個對話框,提示用戶當前應用程序沒有響應,用戶可以選擇繼續等待或者關閉這個應用程序(也就是殺掉這個應用程序的進程)。
2 為什么會有ANR
ANR的產生需要同時滿足三個條件:
- 主線程:只有應用程序進程的主線程響應超時才會產生ANR;
- 超時時間:產生ANR的上下文不同,超時時間也會不同,但只要在這個時間上限內沒有響應就會ANR;
- 輸入事件/特定操作:輸入事件是指按鍵、觸屏等設備輸入事件,特定操作是指BroadcastReceiver和Service的生命周期中的各個函數,產生ANR的上下文不同,導致ANR的原因也會不同;
3 三種情況會觸發ANR
3.1 主線程對輸入事件在5秒內沒有處理完畢
- Android的事件系統從2.3開始做了完全不同的實現,原先2.2中是在Java層實現的,但在2.3中整體轉移到了C++層,本書基于2.3以后的版本進行說明。我們先簡單了解一下產生這種ANR的整個流程。
- 當應用程序的Window處于Active狀態并且能夠接收輸入事件(例如按鍵事件、觸摸事件等)時,系統底層上報的事件就會被InputDispatcher分發給這個應用程序,應用程序的主線程通過InputChannel讀取輸入事件并交給界面視圖處理,界面視圖是一個樹狀結構,DecorView是視圖樹的根,事件從樹根開始一層一層向端點(例如一個Button)傳遞。
- 我們通常會注冊一個監聽器來接收并處理事件,或者創建自定義的視圖控件來處理事件。
- InputDispatcher運行在系統進程(進程名為system_server)的一個單獨的線程中,應用程序的主線程在處理事件的過程中,InputDispatcher會不斷的檢測處理過程是否超時,一旦超時,會通過一系列的回調通知WMS的notifyANR函數,最終會調用到AMS中mHandler對象里的SHOW_NOT_RESPONDING_MSG這個case,此時界面上就顯示系統提示對話框了,同時使用logcat命令查看log(日志信息)也可以看到關于ANR的信息。
- Window:具體指的是PhoneWindow對象,表示一個能夠顯示的窗口,它能夠接收系統分發的各種輸入事件;
- InputDispatcher:將系統上報的輸入事件分發給當前活動的窗口;
- InputChannel:InputDispatcher和應用程序分別運行在兩個不同的進程中,InputDispatcher就是通過InputChannel將事件對象傳遞給應用進程的。
- 注意:產生這種ANR的前提是要有輸入事件,如果用戶沒有觸發任何輸入事件,即便是主線程阻塞了,也不會產生ANR,因為InputDispatcher沒有分發事件給應用程序,當然也不會檢測處理超時和報告ANR了。
3.2 主線程在執行BroadcastReceiver的onReceive函數時10秒內沒有執行完畢
- BroadcastReceiver(簡稱BR)的onReceive函數運行在主線程中,當這個函數超過10秒鐘沒有返回就會觸發ANR。不過對這種情況的ANR系統不會顯示對話框提示,僅是輸出log而已。
3.3 主線程在執行Service的各個生命周期函數時20秒內沒有執行完畢
- Service的各個生命周期函數也運行在主線程中,當這些函數超過20秒鐘沒有返回就會觸發ANR。同樣對對這種情況的ANR系統不會顯示對話框提示,僅是輸出log而已。
3.4 小結
- 1.三種ANR中只有第1種會顯示系統提示對話框,因為用戶正在做界面交互操作,如果長時間沒有任何響應,會讓用戶懷疑設備死機了,大多數人此時會開始亂按,甚至拔出電池重啟,給用戶的體驗肯定是非常糟糕的。
- 2.三種ANR發生時都會在log中輸出錯誤信息,你會發現各個應用進程和系統進程的函數堆棧信息都輸出到了一個/data/anr/traces.txt的文件中,這個文件是分析ANR原因的關鍵文件,同時在日志中還會看到當時的CPU使用率,這也是重要信息,可以利用它們分析ANR問題。
- 3.這三種ANR不是孤立的,有可能會相互影響。例如一個應用程序進程中同時有一個正在顯示的Activity和一個正在處理消息的BroadcastReceiver,它們都運行在這個進程的主線程中。如果BR的onReceive函數沒有返回,此時用戶點擊屏幕,而onReceive超過5秒仍然沒有返回,主線程無法處理用戶輸入事件,就會引起第1種ANR。如果繼續超過10秒沒有返回,又會引起第2種ANR。
4 traces.txt--ANR分析
4.1 獲取ANR產生的trace文件
ANR產生時, 系統會生成一個traces.txt的文件放在/data/anr/下. 可以通過adb命令將其導出到本地:
$adb pull data/anr/traces.txt .
4.2 分析traces.txt
4.2.1 普通阻塞導致的ANR
強行sleep thread產生的一個ANR
獲取到的tracs.txt文件一般如下:
----- pid 2976 at 2016-09-08 23:02:47 -----
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)
如上trace信息中的添加的中文注釋已基本說明了trace文件該怎么分析:
- 1.文件最上的即為最新產生的ANR的trace信息.
- 2.前面兩行表明ANR發生的進程pid, 時間, 以及進程名字(包名).
- 3.尋找我們的代碼點, 然后往前推, 看方法調用棧, 追溯到問題產生的根源.
4.2.2 CPU滿負荷
race信息可能會包含這樣的信息:
Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...
100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait
- 最后一句表明了:
- 1.當是CPU占用100%, 滿負荷了.
- 2.其中絕大數是被iowait即I/O操作占用了.
4.2.3 內存原因
- 其實內存原因有可能會導致ANR,
例如如果由于內存泄露, App可使用內存所剩無幾, 我們點擊按鈕啟動一個大圖片作為背景的activity, 就可能會產生ANR, 這時trace信息可能是這樣的:
// 以下trace信息來自網絡, 用來做個示例
Cmdline: android.process.acore
DALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)
...
MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732
可以看到free的內存已所剩無幾
當然這種情況可能更多的是會產生OOM的異常...
5 ANR的避免和檢測
5.1 切記不要在主線程中作耗時操作
針對三種不同的情況, 一般的處理情況如下
- 1.主線程阻塞的
開辟單獨的子線程來處理耗時阻塞事務.
- 2.CPU滿負荷, I/O阻塞的
I/O阻塞一般來說就是文件讀寫或數據庫操作執行在主線程了, 也可以通過開辟子線程的方式異步執行.
- 3.內存不夠用的
增大VM內存, 使用largeHeap屬性, 排查內存泄露。
5.2 借助于一些工具來進行檢測,從而更有效的避免ANR的引入。
5.2.1 StrictMode
嚴格模式StrictMode是Android SDK提供的一個用來檢測代碼中是否存在違規操作的工具類,StrictMode主要檢測兩大類問題。
- 線程策略 ThreadPolicy
1.detectCustomSlowCalls:檢測自定義耗時操作
2.detectDiskReads:檢測是否存在磁盤讀取操作
3.detectDiskWrites:檢測是否存在磁盤寫入操作
4.detectNetWork:檢測是否存在網絡操作
- 虛擬機策略VmPolicy
1.detectActivityLeaks:檢測是否存在Activity泄露
2.detectLeakedClosableObjects:檢測是否存在未關閉的Closeable對象泄露
3.detectLeakedSqlLiteObjects:檢測是否存在Sqlite對象泄露
4.setClassInstanceLimit:檢測類實例個數是否超過限制
Debug版本中使用
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()//調用detectAll方法表示啟動所有的檢測策略
.penaltyLog()
.penaltyDialog() ////打印logcat,當然也可以定位到dropbox,通過文件保存相應的log
.build());
// 開啟虛擬機模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()//調用detectAll方法表示啟動所有的檢測策略
.penaltyLog()//調用penaltyLog表示在Logcat中打印日志
.build());
}
也可以根據應用的具體要求只開啟某些策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectActivityLeaks()
.penaltyLog()
.build());
5.2.2 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
* 配置監視器持續時間,此時BlockCanary將停止,并使用{@Code BlockCanary}的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.
* 配置塊閾值(以毫為單位),在此持續時間內的調度被視為BLOCK。 您可以根據設備的性能進行設置。
* @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.
* 將文件捆綁到一個zip文件中。
* @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.
* 如果僅僅關注包含進程名稱的包,則返回null。
*/
public List<String> concernPackages() {
return null;
}
/**
* Filter stack without any in concern package, used with @{code concernPackages}.
* 無需關注包的過濾器堆棧,與@ {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.
* Block攔截器,開發者可以提供自己的操作。
*/
public void onBlock(Context context, BlockInfo blockInfo) {
}
}
6 使用子線程的方式
6.1 啟Thread方式
這個其實也是Java實現多線程的方式. 有兩種實現方法, 繼承Thread 或 實現Runnable接口:
繼承Thread
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeThread p = new PrimeThread(143);
p.start();
實現Runnable接口
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}
public void run() {
// compute primes larger than minPrime
. . .
}
}
PrimeRun p = new PrimeRun(143);
new Thread(p).start();
6.2 使用AsyncTask
這個是Android特有的方式, AsyncTask顧名思義, 就是異步任務的意思.
private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
// Do the long-running work in here
// 執行在子線程
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0; i < count; i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}
// This is called each time you call publishProgress()
// 執行在主線程
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}
// This is called when doInBackground() is finished
// 執行在主線程
protected void onPostExecute(Long result) {
showNotification("Downloaded " + result + " bytes");
}
}
// 啟動方式
new DownloadFilesTask().execute(url1, url2, url3);
6.3 HandlerThread
Android中結合Handler和Thread的一種方式. 前面有云, 默認情況下Handler的handleMessage是執行在主線程的, 但是如果我給這個Handler傳入了子線程的looper, handleMessage就會執行在這個子線程中的. HandlerThread正是這樣的一個結合體:
// 啟動一個名為new_thread的子線程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();
// 取new_thread賦值給ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
// 此時handleMessage是運行在new_thread這個子線程中了.
}
}
6.4 IntentService
Service是運行在主線程的, 然而IntentService是運行在子線程的.
實際上IntentService就是實現了一個HandlerThread + ServiceHandler的模式.
6.5 Loader
Android 3.0引入的數據加載器, 可以在Activity/Fragment中使用. 支持異步加載數據, 并可監控數據源在數據發生變化時傳遞新結果. 常用的有CursorLoader, 用來加載數據庫數據.
// 創建一個Loader
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ Contacts.DISPLAY_NAME + " != '' ))";
return new CursorLoader(getActivity(), baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}
// 加載完成
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
6.6 特別注意
使用Thread和HandlerThread時, 為了使效果更好, 建議設置Thread的優先級偏低一點:
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因為如果沒有做任何優先級設置的話, 你創建的Thread默認和UI Thread是具有同樣的優先級的, 你懂的. 同樣的優先級的Thread, CPU調度上還是可能會阻塞掉你的UI Thread, 導致ANR的.
參考
http://www.lxweimin.com/p/c8c15608c68e
http://www.lxweimin.com/p/6d855e984b99
https://blog.csdn.net/sinat_36668731/article/details/74924432