Android 避免出現(xiàn)程序無響應(yīng)ANR

2018-08-02 18:28 更新

編寫:kesenhoo - 原文:http://developer.android.com/training/articles/perf-anr.html

可能你寫的代碼在性能測試上表現(xiàn)良好,但是你的應(yīng)用仍然有時(shí)候會(huì)反應(yīng)遲緩(sluggish),停頓(hang)或者長時(shí)間卡死(frezze),或者是應(yīng)用處理輸入的數(shù)據(jù)花費(fèi)時(shí)間過長。對于你的應(yīng)用來說最槽糕的事情是出現(xiàn)"程序無響應(yīng)(Application Not Responding)" (ANR)的警示框。

在Android中,系統(tǒng)通過顯示ANR警示框來保護(hù)程序的長時(shí)間無響應(yīng)。對話框如下:

anr

此時(shí),你的應(yīng)用已經(jīng)經(jīng)歷過一段時(shí)間的無法響應(yīng)了,因此系統(tǒng)提供用戶可以退出應(yīng)用的選擇。為你的程序提供良好的響應(yīng)性是至關(guān)重要的,這樣才能夠避免系統(tǒng)為用戶顯示ANR的警示框。

這節(jié)課描述了Android系統(tǒng)是如何判斷一個(gè)應(yīng)用不可響應(yīng)的。這節(jié)課還會(huì)提供程序編寫的指導(dǎo)原則,確保你的程序保持響應(yīng)性。

是什么導(dǎo)致了ANR?(What Triggers ANR?)

通常來說,系統(tǒng)會(huì)在程序無法響應(yīng)用戶的輸入事件時(shí)顯示ANR。例如,如果一個(gè)程序在UI線程執(zhí)行I/O操作(通常是網(wǎng)絡(luò)請求或者是文件讀寫),這樣系統(tǒng)就無法處理用戶的輸入事件?;蛘呤菓?yīng)用在UI線程花費(fèi)了太多的時(shí)間用來建立一個(gè)復(fù)雜的在內(nèi)存中的數(shù)據(jù)結(jié)構(gòu),又或者是在一個(gè)游戲程序的UI線程中執(zhí)行了一個(gè)復(fù)雜耗時(shí)的計(jì)算移動(dòng)的操作。確保那些計(jì)算操作高效是很重要的,不過即使是最高效的代碼也是需要花時(shí)間執(zhí)行的。

對于你的應(yīng)用中任何可能長時(shí)間執(zhí)行的操作,你都不應(yīng)該執(zhí)行在UI線程。你可以創(chuàng)建一個(gè)工作線程,把那些操作都執(zhí)行在工作線程中。這確保了UI線程(這個(gè)線程會(huì)負(fù)責(zé)處理UI事件) 能夠順利執(zhí)行,也預(yù)防了系統(tǒng)因代碼僵死而崩潰。因?yàn)閁I線程是和類級(jí)別相關(guān)聯(lián)的,你可以把相應(yīng)性作為一個(gè)類級(jí)別(class-level)的問題(相比來說,代碼性能則屬于方法級(jí)別(method-level)的問題)

在Android中,程序的響應(yīng)性是由Activity Manager與Window Manager系統(tǒng)服務(wù)來負(fù)責(zé)監(jiān)控的。當(dāng)系統(tǒng)監(jiān)測到下面的條件之一時(shí)會(huì)顯示ANR的對話框:

  • 對輸入事件(例如硬件點(diǎn)擊或者屏幕觸摸事件),5秒內(nèi)都無響應(yīng)。
  • BroadReceiver不能夠在10秒內(nèi)結(jié)束接收到任務(wù)。

如何避免ANRs(How to Avoid ANRs)

Android程序通常是執(zhí)行在默認(rèn)的UI線程(也就是main線程)中的。這意味著在UI線程中執(zhí)行的任何長時(shí)間的操作都可能觸發(fā)ANR,因?yàn)槌绦驔]有給自己處理輸入事件或者broadcast事件的機(jī)會(huì)。

因此,任何執(zhí)行在UI線程的方法都應(yīng)該盡可能的簡短快速。特別是,在activity的生命周期的關(guān)鍵方法onCreate()onResume()方法中應(yīng)該盡可能的做比較少的事情。類似網(wǎng)絡(luò)或者DB操作等可能長時(shí)間執(zhí)行的操作,或者是類似調(diào)整bitmap大小等需要長時(shí)間計(jì)算的操作,都應(yīng)該執(zhí)行在工作線程中。(在DB操作中,可以通過異步的網(wǎng)絡(luò)請求)。

為了執(zhí)行一個(gè)長時(shí)間的耗時(shí)操作而創(chuàng)建一個(gè)工作線程最方便高效的方式是使用AsyncTask。只需要繼承AsyncTask并實(shí)現(xiàn)doInBackground()方法來執(zhí)行任務(wù)即可。為了把任務(wù)執(zhí)行的進(jìn)度呈現(xiàn)給用戶,你可以執(zhí)行publishProgress()方法,這個(gè)方法會(huì)觸發(fā)onProgressUpdate()的回調(diào)方法。在onProgressUpdate()的回調(diào)方法中(它執(zhí)行在UI線程),你可以執(zhí)行通知用戶進(jìn)度的操作,例如:

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");
    }
}

為了能夠執(zhí)行這個(gè)工作線程,只需要?jiǎng)?chuàng)建一個(gè)實(shí)例并執(zhí)行execute():

new DownloadFilesTask().execute(url1, url2, url3);

相比起AsycnTask來說,創(chuàng)建自己的線程或者HandlerThread稍微復(fù)雜一點(diǎn)。如果你想這樣做,你應(yīng)該通過Process.setThreadPriority()并傳遞THREAD_PRIORITY_BACKGROUND來設(shè)置線程的優(yōu)先級(jí)為"background"。如果你不通過這個(gè)方式來給線程設(shè)置一個(gè)低的優(yōu)先級(jí),那么這個(gè)線程仍然會(huì)使得你的應(yīng)用顯得卡頓,因?yàn)檫@個(gè)線程默認(rèn)與UI線程有著同樣的優(yōu)先級(jí)。

如果你實(shí)現(xiàn)了Thread或者HandlerThread,請確保你的UI線程不會(huì)因?yàn)榈却ぷ骶€程的某個(gè)任務(wù)而去執(zhí)行Thread.wait()或者Thread.sleep()。UI線程不應(yīng)該去等待工作線程完成某個(gè)任務(wù),你的UI線程應(yīng)該提供一個(gè)Handler給其他工作線程,這樣工作線程能夠通過這個(gè)Handler在任務(wù)結(jié)束的時(shí)候通知UI線程。使用這樣的方式來設(shè)計(jì)你的應(yīng)用程序可以使得你的程序UI線程保持響應(yīng)性,以此來避免ANR。

BroadcastReceiver有特定執(zhí)行時(shí)間的限制說明了broadcast receivers應(yīng)該做的是:簡短快速的任務(wù),避免執(zhí)行費(fèi)時(shí)的操作,例如保存數(shù)據(jù)或者注冊一個(gè)Notification。正如在UI線程中執(zhí)行的方法一樣,程序應(yīng)該避免在broadcast receiver中執(zhí)行費(fèi)時(shí)的長任務(wù)。但不是采用通過工作線程來執(zhí)行復(fù)雜的任務(wù)的方式,你的程序應(yīng)該啟動(dòng)一個(gè)IntentService來響應(yīng)intent broadcast的長時(shí)間任務(wù)。

Tip: 你可以使用StrictMode來幫助尋找因?yàn)椴恍⌒募尤氲経I線程的潛在的長時(shí)間執(zhí)行的操作,例如網(wǎng)絡(luò)或者DB相關(guān)的任務(wù)。

增加響應(yīng)性(Reinforce Responsiveness)

通常來說,100ms - 200ms是用戶能夠察覺到卡頓的上限。這樣的話,下面有一些避免ANR的技巧:

  • 如果你的程序需要響應(yīng)正在后臺(tái)加載的任務(wù),在你的UI中可以顯示ProgressBar來顯示進(jìn)度。
  • 對游戲程序,在工作線程執(zhí)行計(jì)算的任務(wù)。
  • 如果你的程序在啟動(dòng)階段有一個(gè)耗時(shí)的初始化操作,可以考慮顯示一個(gè)閃屏,要么盡快的顯示主界面,然后馬上顯示一個(gè)加載的對話框,異步加載數(shù)據(jù)。無論哪種情況,你都應(yīng)該顯示一個(gè)進(jìn)度信息,以免用戶感覺程序有卡頓的情況。
  • 使用性能測試工具,例如Systrace與Traceview來判斷程序中影響響應(yīng)性的瓶頸。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)