1. 程式人生 > >Android App優化之ANR詳解

Android App優化之ANR詳解

nic orm rim manage dal private rom syn UNC

引言

  1. 背景:Android App優化, 要怎麽做?
  2. Android App優化之性能分析工具
  3. Android App優化之提升你的App啟動速度之理論基礎
  4. Android App優化之提升你的App啟動速度之實例挑戰
  5. Android App優化之Layout怎麽擺
  6. Android App優化之ANR詳解
  7. Android App優化之消除卡頓
  8. Android App優化之內存優化
  9. Android App優化之持久電量
  10. Android App優化之如何高效網絡請求

App優化系列已近中期, 前面分享了一些工具, 理論, 也結合了案例談了下啟動優化, 布局分析等.

原計劃將本文作為這個系列的一個承上啟下點, 對前面幾篇作一個小總結, 聊聊App流暢度和快速響應的話題.

粗一縷, 發現內容還是很多, 暫且拆成幾篇來慢慢寫吧, 勿怪~

今天先來聊聊ANR.

1, 你碰到ANR了嗎

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

技術分享圖片 ANR

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

1.1 何為ANR

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

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<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信息中的添加的中文註釋已基本說明了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<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);

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

具體請參看官網Loader介紹.

3.2.6 特別註意

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

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

因為如果沒有做任何優先級設置的話, 你創建的Thread默認和UI Thread是具有同樣的優先級的, 你懂的. 同樣的優先級的Thread, CPU調度上還是可能會阻塞掉你的UI Thread, 導致ANR的.

結語

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

Android App優化之ANR詳解