ANR可能是monkey測試中最容易出現的問題了。Android程式員面試的時候,也極有可能會談到這個方面。下面的文章主要是對Google Developers的http://developer.android.com/training/articles/perf-anr.html結合自己了解的純人腦翻譯。
Here we go:
你寫的代碼可能成功通過了所有的性能測試,但程式仍可能在特定的情況下遲鈍、挂起或者當機,又或者花了很久去處理輸入。最糟糕的情況莫過于,程式出現了 "Application Not Responding" (ANR) 應用程式無響應的對話框。
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiI0NXYFhGd192UvwVe0lmdhJ3ZvwFM38CXlZHbvN3cpR2Lc1TPB10QGtWUCpEMJ9CXsxWam9CXwADNvwVZ6l2c052bm9CXUJDT1wkNhVzLcRnbvZ2LcFDbuJWMw1mYoZFSaZXUYpVd1kmYr50MZV3YyI2cKJDT29GRjBjUIF2LcRHelR3LcJzLctmch1mclRXY39DOygzMxkTNwEzNycDMzEDMy8CX0Vmbu4GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.jpg)
在Android中,系統檢測到你的程式在一定時間沒有響應就會出現如上的對話框。這時,系統會在一段沒有響應的時間之後,讓使用者可以選擇停止程式。在設計程式的時候,避免程式出現這種情況,是非常關鍵的。這篇文章介紹了“Android如何判定程式是否沒有響應 和 如何保證程式保持響應”。
一、什麼導緻了ANR?
通常,當程式不能響應使用者的輸入時,系統就會顯示ANR。比如,如果程式阻塞在UI線程中處理I/O(通常是通路網絡),而不能處理使用者輸入事件。又比如, app花了太多時間用于建構記憶體結構或者在UI線程中計算遊戲的下一步動作。讓計算高效的執行很重要,但有時即使再高效的代碼也需要一定時間去運作。是以,你的app在任何需要一個長時間操作的時候,你不應該在UI線程中去執行任務,而應該建立一個工作線程,讓這個線程去執行大部分的工作。 這就保持UI線程(驅動使用者界面的事件循環)保持運作,并且防止系統判定你的程式停止運作。因為這樣的線程(UI線程)處理通常是在類級别完成的,你可以認為“響應”是最進階别的問題(與之相比的基本代碼的運作,是方法級别的)。
在Android中,程式的響應情況由Activity Manager和Window Manager system services監控。當出現如下的任一情況就會顯示ANR對話框:
- 輸入事件 (比如按鍵和觸屏事件) 5秒鐘沒有響應。
-
一個
10秒鐘沒有結束執行BroadcastReceiver
二、如果避免ANR?
Android程式通常運作在一個線程之内(預設是“UI線程”或者“主線程”)。 這意味着在UI線程中需要長時間才能結束運作的任務會觸發ANR,因為程式無暇處理輸入事件或intent broadcasts。是以,任何在UI線程中跑的方法應當盡可能少地在相應線程處理任務。特别地,activities 應該盡可能少在關鍵生命周期方法如
onCreate()
和
onResume()
。潛在需要長時間運作的操作如網絡、資料庫操作,或者耗費大量計算資源的如重新計算位圖等應該在工作線程中執行。 (或者比如在資料庫中, 通過異步請求)。
更有效的方法是用
AsyncTask
類為長時間的操作建立一個工作線程。 簡單地繼承(extend)
AsyncTask
和實作(implement)
doInBackground()
方法來執行工作。要通知進展變化給使用者時,你可以調用
publishProgress()
, 它可以援引
onProgressUpdate()
回調方法。通過方法
onProgressUpdate()
(在UI線程中執行),你可以通知使用者,比如:
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");
}
}
要執行這個工作線程,建立一個執行個體并調用
execute()
:
new DownloadFilesTask().execute(url1, url2, url3);
盡管比
AsyncTask
要複雜,你可能不想用異步處理,轉而建立自己的
Thread
或
HandlerThread
類。如果你這麼做,需要把線程優先級設定為 "background" 優先級, 通過調用
Process.setThreadPriority()
并傳參
THREAD_PRIORITY_BACKGROUND
。如果你不這樣降低線程的優先級,那麼這個線程很可能還是會拖慢你的app,因為它和UI線程的優先級預設是一樣的。
如果你使用
Thread
或
HandlerThread
, 要確定你的UI線程不會在等待工作線程完成時阻塞——不要調用
Thread.wait()
or
Thread.sleep()
。為了不用阻塞等帶工作線程完成,你的主線程應該提供一個
Handler
讓其他線程在執行完後通知(post back)主線程。這樣設計你的程式,你會讓你的UI線程保持對輸入的響應,因而避免了ANR的對話框(由輸入事件的5秒逾時産生)。
在
BroadcastReceiver
執行時間上的特定限制,強調了broadcast receivers應該做的是:在背景,小而離散的工作量,比如儲存一個設定或者注冊一個
Notification
。其他在UI線程中被調用的方法也是一樣,程式應該避免潛在的長時間操作或者broadcast receiver中的長時間計算。把這些強度大的工作交給工作線程,你的程式應該起一個
IntentService
如果需要長時間的操作來回應 intent broadcast。
提示 :你可以使用
StrictMode
來幫助發現潛在的長時間運作操作如網絡、資料庫操作,而這些操作可能不是你故意在主線程中去做的。
三、加強響應
通常超過100到200毫秒的門檻值,使用者會察覺程式的緩慢。是以,這裡有更多的一些建議讓你不僅僅是避免ANR,而且讓你的程式對使用者來說是響應及時的:
- 如果你的程式在背景操作,需要對使用者的輸入響應,那麼在界面要去展示執行的進度 (比如在你的UI線程中用一個
)。ProgressBar
- 特别是對遊戲而言,在工作線程中計算運動(moves)。
- 如果你的程式有一個耗時的初始按照階段,可以考慮用一個啟動畫面或者盡可能快的渲染主界面,表明正在加載并且異步地填充資訊,在這兩種情況下,你應該以某種方式表示正在取得進展,以免使用者覺得應用程式當機。
- 使用性能工具,如 Systrace 和 Traceview 來檢查你程式響應的瓶頸。