看完這篇 Android ANR 分析,就可以和麵試官裝逼了!
ANR概述
首先,ANR(Application Not responding)是指應用程式未響應,Android系統對於一些事件需要在一定的時間範圍內完成,如果超過預定時間能未能得到有效響應或者響應時間過長,都會造成ANR。ANR由訊息處理機制保證,Android在系統層實現了一套精密的機制來發現ANR,核心原理是訊息排程和超時處理。
其次,ANR機制主體實現在系統層。所有與ANR相關的訊息,都會經過系統程式(system_server)排程,然後派發到應用程式完成對訊息的實際處理,同時,系統程式設計了不同的超時限制來跟蹤訊息的處理。 一旦應用程式處理訊息不當,超時限制就起作用了,它收集一些系統狀態,譬如CPU/IO使用情況、程式函式呼叫棧,並且報告使用者有程式無響應了(ANR對話方塊)。
然後,ANR問題本質是一個效能問題。ANR機制實際上對應用程式主執行緒的限制,要求主執行緒在限定的時間內處理完一些最常見的操作(啟動服務、處理廣播、處理輸入), 如果處理超時,則認為主執行緒已經失去了響應其他操作的能力。主執行緒中的耗時操作,譬如密集CPU運算、大量IO、複雜介面佈局等,都會降低應用程式的響應能力。
哪些場景會造成ANR?
1. 發生ANR時會呼叫AppNotRespondingDialog.show()方法彈出對話方塊提示使用者,該對話方塊的依次呼叫關係如下圖所示:
2. AppErrors.appNotResponding(),該方法是最終彈出ANR對話方塊的唯一入口,呼叫該方法的場景才會有ANR提示,也可以認為在主執行緒中執行無論再耗時的任務,只要最終不呼叫該方法,都不會有ANR提示,也不會有ANR相關日誌及報告;通過呼叫關係可以看出哪些場景會導致ANR,有以下四種場景:
(1)Service Timeout:Service在特定的時間內無法處理完成
(2)BroadcastQueue Timeout:BroadcastReceiver在特定時間內無法處理完成
(3)ContentProvider Timeout:內容提供者執行超時
(4)inputDispatching Timeout: 按鍵或觸控事件在特定時間內無響應。
ANR機制
ANR機制可以分為兩部分:
ANR監測機制:Android對於不同的ANR型別(Broadcast, Service, InputEvent)都有一套監測機制。
ANR報告機制:在監測到ANR以後,需要顯示ANR對話方塊、輸出日誌(發生ANR時的程式函式呼叫棧、CPU使用情況等)。
整個ANR機制的程式碼也是橫跨了Android的幾個層:
App層:應用主執行緒的處理邏輯;
Framework層:ANR機制的核心,主要有AMS、BroadcastQueue、ActiveServices、InputmanagerService、InputMonitor、InputChannel、ProcessCpuTracker等;
Native層:InputDispatcher.cpp;
Provider超時機制遇到的比較少,暫不做分析;Broadcast目前主要想說兩個知識點:
第一:無論是普通廣播還是有序廣播,最終廣播接受者的onreceive都是序列執行的,可以通過Demo進行驗證;
第二:通過Demo以及框架新增相關日誌,都驗證了普通廣播也會有ANR監測機制,ANR機制以及問題分析文章認為只有序列廣播才有ANR監測機制,後續再會專門講解Broadcast傳送及接收流程,同時也會補充Broadcast ANR監測機制;本文主要以Servie處理超時、輸入事件分發超時為例探討ANR監測機制。
Service超時監測機制
Service執行在應用程式的主執行緒,如果Service的執行時間超過20秒,則會引發ANR。
當發生Service ANR時,一般可以先排查一下在Service的生命週期函式中(onCreate(), onStartCommand()等)有沒有做耗時的操作,譬如複雜的運算、IO操作等。 如果應用程式的程式碼邏輯查不出問題,就需要深入檢查當前系統的狀態:CPU的使用情況、系統服務的狀態等,判斷當時發生ANR程式是否受到系統執行異常的影響。
如何檢測Service超時呢?Android是通過設定定時訊息實現的。定時訊息是由AMS的訊息佇列處理的(system_server的ActivityManager執行緒)。 AMS有Service執行的上下文資訊,所以在AMS中設定一套超時檢測機制也是合情合理的。
我們先丟擲兩個問題
問題一:Service啟動流程?
問題一:如何監測Service超時?
主要通過以上兩個問題來說明Service監測機制,在知道Service啟動流程之後,通過Service啟動流程可以更容易分析Service超時監測機制。
1. Service啟動流程如下圖所示:
(1)ActiveServices.realStartServiceLocked()在通過app.thread的scheduleCreateService()來建立Service物件並呼叫Service.onCreate()後,接著又呼叫sendServiceArgsLocked()方法來呼叫Service的其他方法,如onStartCommand。以上兩步均是程式間通訊,應用與AMS之間跨程式通訊可以參考應用程式與系統程式通訊
(2)以上只是列出Service啟動流程的關鍵步驟,具體每個方法主要做哪些工作還需要檢視具體的程式碼,暫時先忽略這些,感興趣的可以參考Android開發藝術探索等其他相關資料
2. Service超時監測機制
Service超時監測機制可以從Service啟動流程中找到。
(1)ActiveServices.realStartServiceLocked()主要工作有
private final void realStartServiceLocked(ServiceRecord r,
ProcessRecord app, boolean execInFg) throws RemoteException {
...
// 主要是為了設定ANR超時,可以看出在正式啟動Service之前開始ANR監測;
bumpServiceExecutingLocked(r, execInFg, "create");
// 啟動過程呼叫scheduleCreateService方法,最終會呼叫Service.onCreate方法;
app.thread.scheduleCreateService(r, r.serviceInfo,
// 繫結過程中,這個方法中會呼叫app.thread.scheduleBindService方法
requestServiceBindingsLocked(r, execInFg);
// 調動Service的其他方法,如onStartCommand,也是IPC通訊
sendServiceArgsLocked(r, execInFg, true);
}
(2)bumpServiceExecutingLocked()會呼叫scheduleServiceTimeoutLocked()方法
void scheduleServiceTimeoutLocked(ProcessRecord proc) {
if (proc.executingServices.size() == 0 || proc.thread == null) {
return;
}
Message msg = mAm.mHandler.obtainMessage(
ActivityManagerService.SERVICE_TIMEOUT_MSG);
msg.obj = proc;
// 在serviceDoneExecutingLocked中會remove該SERVICE_TIMEOUT_MSG訊息,
// 當超時後仍沒有remove SERVICE_TIMEOUT_MSG訊息,則執行ActiveServices. serviceTimeout()方法;
mAm.mHandler.sendMessageDelayed(msg,
proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
// 前臺程式中執行Service,SERVICE_TIMEOUT=20s;後臺程式中執行Service,SERVICE_BACKGROUND_TIMEOUT=200s
}
(3)如果在指定的時間內還沒有serviceDoneExecutingLocked()方法將訊息remove掉,就會呼叫ActiveServices. serviceTimeout()方法
void serviceTimeout(ProcessRecord proc) {
...
final long maxTime = now -
(proc.execServicesFg ? SERVICE_TIMEOUT : SERVICE_BACKGROUND_TIMEOUT);
...
// 尋找執行超時的Service
for (int i=proc.executingServices.size()-1; i>=0; i--) {
ServiceRecord sr = proc.executingServices.valueAt(i);
if (sr.executingStart < maxTime) {
timeout = sr;
break;
}
...
}
...
// 判斷執行Service超時的程式是否在最近執行程式列表,如果不在,則忽略這個ANR
if (timeout != null && mAm.mLruProcesses.contains(proc)) {
anrMessage = "executing service " + timeout.shortName;
}
...
if (anrMessage != null) {
// 當存在timeout的service,則執行appNotResponding,報告ANR
mAm.appNotResponding(proc, null, null, false, anrMessage);
}
}
(4)Service onCreate超時監測整體流程如下圖
在onCreate生命週期開始執行前,啟動超時監測,如果在指定的時間onCreate沒有執行完畢(該該方法中執行耗時任務),就會呼叫ActiveServices.serviceTimeout()方法報告ANR;如果在指定的時間內onCreate執行完畢,那麼就會呼叫ActivityManagerService.serviceDoneExecutingLocked()方法移除SERVICE_TIMEOUT_MSG訊息,說明Service.onCreate方法沒有發生ANR,Service是由AMS排程,利用Handler和Looper,設計了一個TIMEOUT訊息交由AMS執行緒來處理,整個超時機制的實現都是在Java層;以上就是Service超時監測的整體流程。
輸入事件超時監測
應用程式可以接收輸入事件(按鍵、觸屏、軌跡球等),當5秒內沒有處理完畢時,則會引發ANR。
這裡先把問題丟擲來了:
輸入事件經歷了一些什麼工序才能被派發到應用的介面?
如何檢測到輸入時間處理超時?
1. Android輸入系統簡介
Android輸入系統總體流程與參與者如下圖所示。
簡單來說,核心將原始事件寫入到裝置節點中,InputReader在其執行緒迴圈中不斷地從EventHub中抽取原始輸入事件,進行加工處理後將加工所得的事件放入InputDispatcher的派發發佇列中。InputDispatcher則在其執行緒迴圈中將派發佇列中的事件取出,查詢合適的視窗,將事件寫入到視窗的事件接收管道中。視窗事件接收執行緒的Looper從管道中將事件取出,交由視窗事件處理函式進行事件響應。關鍵流程有:原始輸入事件的讀取與加工;輸入事件的派發;輸入事件的傳送、接收與反饋。其中輸入事件派發是指InputDispatcher不斷的從派發佇列取出事件、尋找合適的視窗進行傳送的過程,輸入事件的傳送是InputDispatcher通過Connection物件將事件傳送給視窗的過程。
InputDispatcher與視窗之間的跨程式通訊主要通過InputChannel來完成。在InputDispatcher與視窗通過InputChannel建立連線之後,就可以進行事件的傳送、接收與反饋;輸入事件的傳送和接收主要流程如圖所示:
其中,將輸入事件注入派發佇列後,會喚醒派發執行緒,派發執行緒迴圈由InputDispatcher.dispatchOnce函式完成;InputDispatcher將事件以InputMessage寫入InputChannel之後,視窗端的looper被喚醒,進而執行NativeInputReceiver::handleEvent()開始輸入事件的接收,從InputEventReceiver開始輸入事件被派發到使用者介面;以上只是輸入事件的大致流程,更詳細的流程可以參考相關資料;在瞭解輸入系統的大致流程之後,我們來分析輸入事件的超時監測機制。
2. 輸入事件超時監測
按鍵事件超時監測整體流程如下圖所示
(1)InputDispatcher::dispatchOnceInnerLocked():
根據事件型別選擇不同事件的處理方法:InputDispatcher::dispatchKeyLocked()或者InputDispatcher::dispatchMotionLocked(),我們以按鍵事件超時監測為例進行說明;
(2)findFocusedWindowTargetsLocked()方法會呼叫checkWindowReadyForMoreInputLocked();該方法檢查視窗是否有能力再接收新的輸入事件;可能會有一系列的場景阻礙事件的繼續派發,相關場景有:
場景1: 視窗處於paused狀態,不能處理輸入事件
“Waiting because the [targetType] window is paused.”
場景2: 視窗還未向InputDispatcher註冊,無法將事件派發到視窗
“Waiting because the [targetType] window’s input channel is not registered with the input dispatcher. The window may be in the process of being removed.”
場景3: 視窗和InputDispatcher的連線已經中斷,即InputChannel不能正常工作
“Waiting because the [targetType] window’s input connection is [status]. The window may be in the process of being removed.”
場景4: InputChannel已經飽和,不能再處理新的事件
“Waiting because the [targetType] window’s input channel is full. Outbound queue length: %d. Wait queue length: %d.”
場景5: 對於按鍵型別(KeyEvent)的輸入事件,需要等待上一個事件處理完畢
“Waiting to send key event because the [targetType] window has not finished processing all of the input events that were previously delivered to it. Outbound queue length: %d. Wait queue length: %d.”
場景6: 對於觸控型別(TouchEvent)的輸入事件,可以立即派發到當前的視窗,因為TouchEvent都是發生在使用者當前可見的視窗。但有一種情況, 如果當前應用由於佇列有太多的輸入事件等待派發,導致發生了ANR,那TouchEvent事件就需要排隊等待派發。
“Waiting to send non-key event because the %s window has not finished processing certain input events that were delivered to it over %0.1fms ago. Wait queue length: %d. Wait queue head age: %0.1fms.”
以上這些場景就是我們常在日誌中看到的ANR原因的列印。
(3)其中事件分發5s限制定義在InputDispatcher.cpp;InputDispatcher::handleTargetsNotReadyLocked()方法中如果事件5s之內還沒有分發完畢,則呼叫InputDispatcher::onANRLocked()提示使用者應用發生ANR;
//預設分發超時間為5s
const nsecs_t DEFAULT_INPUT_DISPATCHING_TIMEOUT = 5000 * 1000000LL;
int32_t InputDispatcher::handleTargetsNotReadyLocked(nsecs_t currentTime,
const EventEntry* entry,
const sp<InputApplicationHandle>& applicationHandle,
const sp<InputWindowHandle>& windowHandle,
nsecs_t* nextWakeupTime, const char* reason) {
// 1.如果當前沒有聚焦視窗,也沒有聚焦的應用
if (applicationHandle == NULL && windowHandle == NULL) {
...
} else {
// 2.有聚焦視窗或者有聚焦的應用
if (mInputTargetWaitCause != INPUT_TARGET_WAIT_CAUSE_APPLICATION_NOT_READY) {
// 獲取等待的時間值
if (windowHandle != NULL) {
// 存在聚焦視窗,DEFAULT_INPUT_DISPATCHING_TIMEOUT事件為5s
timeout = windowHandle->getDispatchingTimeout(DEFAULT_INPUT_DISPATCHING_TIMEOUT);
} else if (applicationHandle != NULL) {
// 存在聚焦應用,則獲取聚焦應用的分發超時時間
timeout = applicationHandle->getDispatchingTimeout(
DEFAULT_INPUT_DISPATCHING_TIMEOUT);
} else {
// 預設的分發超時時間為5s
timeout = DEFAULT_INPUT_DISPATCHING_TIMEOUT;
}
}
}
// 如果當前時間大於輸入目標等待超時時間,即當超時5s時進入ANR處理流程
// currentTime 就是系統的當前時間,mInputTargetWaitTimeoutTime 是一個全域性變數,
if (currentTime >= mInputTargetWaitTimeoutTime) {
// 呼叫ANR處理流程
onANRLocked(currentTime, applicationHandle, windowHandle,
entry->eventTime, mInputTargetWaitStartTime, reason);
// 返回需要等待處理
return INPUT_EVENT_INJECTION_PENDING;
}
}
(4)當應用主執行緒被卡住的事件,再點選該應用其它元件也是無響應,因為事件派發是序列的,上一個事件不處理完畢,不會處理下一個事件。
(5)Activity.onCreate執行耗時操作,不管使用者如何操作都不會發生ANR,因為輸入事件相關監聽機制還沒有建立起來;InputChannel通道還沒有建立
這時是不會響應輸入事件,InputDispatcher還不能事件傳送到應用視窗,ANR監聽機制也還沒有建立,所以此時是不會報告ANR的。
(6)輸入事件由InputDispatcher排程,待處理的輸入事件都會進入佇列中等待,設計了一個等待超時的判斷,超時機制的實現在Native層。
以上就是輸入事件ANR監測機制;具體邏輯請參考相關原始碼;
ANR報告機制
無論哪種型別的ANR發生以後,最終都會呼叫 AppErrors.appNotResponding() 方法,所謂“殊途同歸”。這個方法的職能就是向使用者或開發者報告ANR發生了。 最終的表現形式是:彈出一個對話方塊,告訴使用者當前某個程式無響應;輸入一大堆與ANR相關的日誌,便於開發者解決問題。
final void appNotResponding(ProcessRecord app, ActivityRecord activity,
ActivityRecord parent, boolean aboveSystem, final String annotation) {
...
if (ActivityManagerService.MONITOR_CPU_USAGE) {
// 1. 更新CPU使用資訊。ANR的第一次CPU資訊取樣,取樣資料會儲存在mProcessStats這個變數中
mService.updateCpuStatsNow();
}
// 記錄ANR到EventLog中
EventLog.writeEvent(EventLogTags.AM_ANR, app.userId, app.pid,
app.processName, app.info.flags, annotation);
// 輸出ANR到main log.
StringBuilder info = new StringBuilder();
info.setLength(0);
info.append("ANR in ").append(app.processName);
if (activity != null && activity.shortComponentName != null) {
info.append(" (").append(activity.shortComponentName).append(")");
}
info.append("\n");
info.append("PID: ").append(app.pid).append("\n");
if (annotation != null) {
info.append("Reason: ").append(annotation).append("\n");
}
if (parent != null && parent != activity) {
info.append("Parent: ").append(parent.shortComponentName).append("\n");
}
// 3. 列印呼叫棧。具體實現由dumpStackTraces()函式完成
File tracesFile = ActivityManagerService.dumpStackTraces(
true, firstPids,
(isSilentANR) ? null : processCpuTracker,
(isSilentANR) ? null : lastPids,
nativePids);
String cpuInfo = null;
// MONITOR_CPU_USAGE預設為true
if (ActivityManagerService.MONITOR_CPU_USAGE) {
// 4. 更新CPU使用資訊。ANR的第二次CPU使用資訊取樣。兩次取樣的資料分別對應ANR發生前後的CPU使用情況
mService.updateCpuStatsNow();
synchronized (mService.mProcessCpuTracker) {
// 輸出ANR發生前一段時間內各個程式的CPU使用情況
cpuInfo = mService.mProcessCpuTracker.printCurrentState(anrTime);
}
// 輸出CPU負載
info.append(processCpuTracker.printCurrentLoad());
info.append(cpuInfo);
}
// 輸出ANR發生後一段時間內各個程式的CPU使用率
info.append(processCpuTracker.printCurrentState(anrTime));
//會列印發生ANR的原因,如輸入事件導致ANR的不同場景
Slog.e(TAG, info.toString());
if (tracesFile == null) {
// There is no trace file, so dump (only) the alleged culprit's threads to the log
// 傳送signal 3(SIGNAL_QUIT)來dump棧資訊
Process.sendSignal(app.pid, Process.SIGNAL_QUIT);
}
// 將anr資訊同時輸出到DropBox
mService.addErrorToDropBox("anr", app, app.processName, activity, parent, annotation,
cpuInfo, tracesFile, null);
// Bring up the infamous App Not Responding dialog
// 5. 顯示ANR對話方塊。丟擲SHOW_NOT_RESPONDING_MSG訊息,
// AMS.MainHandler會處理這條訊息,顯示AppNotRespondingDialog對話方塊提示使用者發生ANR
Message msg = Message.obtain();
HashMap<String, Object> map = new HashMap<String, Object>();
msg.what = ActivityManagerService.SHOW_NOT_RESPONDING_UI_MSG;
msg.obj = map;
msg.arg1 = aboveSystem ? 1 : 0;
map.put("app", app);
if (activity != null) {
map.put("activity", activity);
}
mService.mUiHandler.sendMessage(msg);
}
}
除了主體邏輯,發生ANR時還會輸出各種類別的日誌:
event log:通過檢索”am_anr”關鍵字,可以找到發生ANR的應用
main log:通過檢索”ANR in “關鍵字,可以找到ANR的資訊,日誌的上下文會包含CPU的使用情況
dropbox:通過檢索”anr”型別,可以找到ANR的資訊
traces:發生ANR時,各程式的函式呼叫棧資訊
至此ANR相關報告已經完成,後續需要分析ANR問題,分析ANR往往是從main log中的CPU使用情況和traces中的函式呼叫棧開始。所以,更新CPU的使用資訊updateCpuStatsNow()方法和列印函式棧dumpStackTraces()方法,是系統報告ANR問題關鍵所在,具體分析ANR問題請參考相關資料。
總結
1. ANR的監測機制:首先分析Service和輸入事件大致工作流程,然後從Service,InputEvent兩種不同的ANR監測機制的原始碼實現開始,分析了Android如何發現各類ANR。在啟動服務、輸入事件分發時,植入超時檢測,用於發現ANR。
2. ANR的報告機制:分析Android如何輸出ANR日誌。當ANR被發現後,兩個很重要的日誌輸出是:CPU使用情況和程式的函式呼叫棧,這兩類日誌是我們解決ANR問題的利器。
3. 監測ANR的核心原理是訊息排程和超時處理。
4. 只有被ANR監測的場景才會有ANR報告以及ANR提示框。
附錄
Android高階技術大綱,以及系統進階視訊;
Android高階技術大綱
Android高階進階視訊資料
獲取方式;
加Android進階群;701740775。即可前往免費領取。免費備註一下csdn
相關文章
- 看完這篇 HashMap,和麵試官扯皮就沒問題了HashMap
- 看完這篇 final、finally 和 finalize 和麵試官扯皮就沒問題了
- 看完這篇 Session、Cookie、Token,和麵試官扯皮就沒問題了SessionCookie
- 看完這篇快取穿透的文章,又能和麵試官互扯了~快取穿透
- 看完這篇作業系統,和麵試官扯皮就沒問題了作業系統
- 和麵試官這樣吹MongoDB 複製集!MongoDB
- 看完這篇 HashSet,跟面試官扯皮沒問題了面試
- 看完這篇HTTP,跟面試官扯皮就沒問題了HTTP面試
- Android ANR日誌分析指南Android
- 教你如何 分析 Android ANR 問題Android
- 看完這一篇,再也不怕面試官問到IntentService的原理面試Intent
- 看完這篇,再也不怕面試官問我執行緒池了面試執行緒
- 看完這篇,Promise面試就搞定了...Promise面試
- 原來ReadWriteLock也能開發高效能快取,看完我也能和麵試官好好聊聊了!快取
- Android-ANR總結及日誌分析Android
- 瞭解 Android ANRAndroid
- ANR的分析
- 征服面試官:OkHttp 原理篇 掌握這篇面試題彙總,吊打面試官!HTTP面試題
- 想轉行資料分析,看完這篇再做決定
- Mysql探索之索引詳解,又能和麵試官互扯了~MySql索引
- 深度分析:面試90%被問到的 Session、Cookie、Token,看完這篇你就掌握了!面試SessionCookie
- 測試用例如何評審,看完這篇就會了
- Android學習筆記·ANRAndroid筆記
- Android中的ANR簡述Android
- flutter筆記7:flutter頁面佈局基礎,看完這篇就可以用flutter寫APP了Flutter筆記APP
- 看完這篇文章你就可以告訴領導你精通Zookeeper了
- 搞懂鉤子方法和模板方法,看完這篇就夠了
- HTML教程(看完這篇就夠了)HTML
- Android高階進階之路【三】看完這篇再不會Android動畫框架,我跪搓衣板Android動畫框架
- 一個static和麵試官扯了一個小時,舌戰加強版
- 面試官這樣問你:為什麼MySQL新增索引後就可以提高查詢速度面試MySql索引
- 看完這篇不在擔心刪庫跑路
- 看完這篇關於MVVM的文章,面試通過率提升了80%MVVM面試
- NET近期面試總結和麵試題面試
- MySQL優化篇(一),我可以和麵試官多聊幾句嗎?——SQL優化流程與優化資料庫物件MySql優化資料庫物件
- Java中筆試和麵試---P9 級面試官是如何 360° 無死角考察候選人的Java筆試面試
- 看完這篇Linux基本的操作就會了Linux
- Sql Or NoSql,看完這一篇你就懂了SQL