Android App 優化之 ANR 詳解

爲了便於閱讀, 應邀將Android App性能優化系列, 轉移到掘金原創上來.
掘金的新出的"收藏集"功能能夠用來作系列文集了.html

今天先來聊聊ANR.java

1, 你碰到ANR了嗎

在App使用過程當中, 你可能遇到過這樣的狀況:android

恭喜你, 這就是傳說中的ANR.git

1.1 何爲ANR

ANR全名Application Not Responding, 也就是"應用無響應". 當操做在一段時間內系統沒法處理時, 系統層面會彈出上圖那樣的ANR對話框.github

1.2 爲何會產生ANR

在Android裏, App的響應能力是由Activity Manager和Window Manager系統服務來監控的. 一般在以下兩種狀況下會彈出ANR對話框:數據庫

  • 5s內沒法響應用戶輸入事件(例如鍵盤輸入, 觸摸屏幕等).
  • BroadcastReceiver在10s內沒法結束.

形成以上兩種狀況的首要緣由就是在主線程(UI線程)裏面作了太多的阻塞耗時操做, 例如文件讀寫, 數據庫讀寫, 網絡查詢等等.編程

1.3 如何避免ANR

知道了ANR產生的緣由, 那麼想要避免ANR, 也就很簡單了, 就一條規則:性能優化

不要在主線程(UI線程)裏面作繁重的操做.網絡

這裏面實際上涉及到兩個問題:多線程

  1. 哪些地方是運行在主線程的?
  2. 不在主線程作, 在哪兒作?

稍後解答.

2, ANR分析

2.1 獲取ANR產生的trace文件

ANR產生時, 系統會生成一個traces.txt的文件放在/data/anr/下. 能夠經過adb命令將其導出到本地:

$adb pull data/anr/traces.txt .複製代碼

2.2 分析traces.txt

2.2.1 普通阻塞致使的ANR

獲取到的tracs.txt文件通常以下:

以下以GithubApp代碼爲例, 強行sleep thread產生的一個ANR.

----- 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
  
  
  

 
  
  ) 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信息中的添加的中文註釋已基本說明了trace文件該怎麼分析:

  1. 文件最上的即爲最新產生的ANR的trace信息.
  2. 前面兩行代表ANR發生的進程pid, 時間, 以及進程名字(包名).
  3. 尋找咱們的代碼點, 而後往前推, 看方法調用棧, 追溯到問題產生的根源.

以上的ANR trace是屬於相對簡單, 還有可能你並無在主線程中作過於耗時的操做, 然而仍是ANR了. 這就有多是以下兩種狀況了:

2.2.2 CPU滿負荷

這個時候你看到的trace信息可能會包含這樣的信息:

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操做佔用了.

此時分析方法調用棧, 通常來講會發現是方法中有頻繁的文件讀寫或是數據庫讀寫操做放在主線程來作了.

2.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的異常...

2.2 ANR的處理

針對三種不一樣的狀況, 通常的處理狀況以下

  1. 主線程阻塞的
    開闢單獨的子線程來處理耗時阻塞事務.

  2. CPU滿負荷, I/O阻塞的
    I/O阻塞通常來講就是文件讀寫或數據庫操做執行在主線程了, 也能夠經過開闢子線程的方式異步執行.

  3. 內存不夠用的
    增大VM內存, 使用largeHeap屬性, 排查內存泄露(這個在內存優化那篇細說吧)等.

3, 深刻一點

沒有人願意在出問題以後去解決問題.
高手和新手的區別是, 高手知道怎麼在一開始就避免問題的發生. 那麼針對ANR這個問題, 咱們須要作哪些層次的工做來避免其發生呢?

3.1 哪些地方是執行在主線程的

  1. Activity的全部生命週期回調都是執行在主線程的.
  2. Service默認是執行在主線程的.
  3. BroadcastReceiver的onReceive回調是執行在主線程的.
  4. 沒有使用子線程的looper的Handler的handleMessage, post(Runnable)是執行在主線程的.
  5. AsyncTask的回調中除了doInBackground, 其餘都是執行在主線程的.
  6. View的post(Runnable)是執行在主線程的.

3.2 使用子線程的方式有哪些

上面咱們幾乎一直在說, 避免ANR的方法就是在子線程中執行耗時阻塞操做. 那麼在Android中有哪些方式可讓咱們實現這一點呢.

3.2.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();複製代碼

3.2.2 使用AsyncTask

這個是Android特有的方式, AsyncTask顧名思義, 就是異步任務的意思.

private class DownloadFilesTask extends AsyncTask
  
  
  

  

 {
    // 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);複製代碼

3.2.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這個子線程中了.
    }
}複製代碼

3.2.4 IntentService

Service是運行在主線程的, 然而IntentService是運行在子線程的.
實際上IntentService就是實現了一個HandlerThread + ServiceHandler的模式.

以上HandlerThread的使用代碼示例也就來自於IntentService源碼.

3.2.5 Loader

Android 3.0引入的數據加載器, 能夠在Activity/Fragment中使用. 支持異步加載數據, 並可監控數據源在數據發生變化時傳遞新結果. 經常使用的有CursorLoader, 用來加載數據庫數據.

// Prepare the loader.  Either re-connect with an existing one,
// or start a new one.
// 使用LoaderManager來初始化Loader
getLoaderManager().initLoader(0, null, this);

//若是 ID 指定的加載器已存在,則將重複使用上次建立的加載器。
//若是 ID 指定的加載器不存在,則 initLoader() 將觸發 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您能夠實現代碼以實例化並返回新加載器

// 建立一個Loader
public Loader
  
  
  

 
  
  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
  
  
  

 
  
  loader, Cursor data) 

 {
    // Swap the new cursor in.  (The framework will take care of closing the
    // old cursor once we return.)
    mAdapter.swapCursor(data);
}複製代碼

具體請參看官網Loader介紹.

3.2.6 特別注意

使用Thread和HandlerThread時, 爲了使效果更好, 建議設置Thread的優先級偏低一點:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);複製代碼

由於若是沒有作任何優先級設置的話, 你建立的Thread默認和UI Thread是具備一樣的優先級的, 你懂的. 一樣的優先級的Thread, CPU調度上仍是可能會阻塞掉你的UI Thread, 致使ANR的.

結語

對於ANR問題, 我的認爲仍是預防爲主, 認清代碼中的阻塞點, 善用線程. 同時造成良好的編程習慣, 要有MainThread和Worker Thread的概念的...(實際上人的工做狀態也是這樣的~~哈哈)

相關文章
相關標籤/搜索