Android App優(yōu)化之ANR詳解

引言

  1. 背景:Android App優(yōu)化, 要怎么做?
  2. Android App優(yōu)化之性能分析工具
  3. Android App優(yōu)化之提升你的App啟動速度之理論基礎(chǔ)
  4. Android App優(yōu)化之提升你的App啟動速度之實例挑戰(zhàn)
  5. Android App優(yōu)化之Layout怎么擺
  6. Android App優(yōu)化之ANR詳解
  7. Android App優(yōu)化之消除卡頓
  8. Android App優(yōu)化之內(nèi)存優(yōu)化
  9. Android App優(yōu)化之持久電量
  10. Android App優(yōu)化之如何高效網(wǎng)絡(luò)請求

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

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

粗一縷, 發(fā)現(xiàn)內(nèi)容還是很多, 暫且拆成幾篇來慢慢寫吧, 勿怪~

今天先來聊聊ANR.

1, 你碰到ANR了嗎

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

ANR

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

1.1 何為ANR

ANR全名Application Not Responding, 也就是"應(yīng)用無響應(yīng)". 當(dāng)操作在一段時間內(nèi)系統(tǒng)無法處理時, 系統(tǒng)層面會彈出上圖那樣的ANR對話框.

1.2 為什么會產(chǎn)生ANR

在Android里, App的響應(yīng)能力是由Activity Manager和Window Manager系統(tǒng)服務(wù)來監(jiān)控的. 通常在如下兩種情況下會彈出ANR對話框:

  • 5s內(nèi)無法響應(yīng)用戶輸入事件(例如鍵盤輸入, 觸摸屏幕等).
  • BroadcastReceiver在10s內(nèi)無法結(jié)束.

造成以上兩種情況的首要原因就是在主線程(UI線程)里面做了太多的阻塞耗時操作, 例如文件讀寫, 數(shù)據(jù)庫讀寫, 網(wǎng)絡(luò)查詢等等.

1.3 如何避免ANR

知道了ANR產(chǎn)生的原因, 那么想要避免ANR, 也就很簡單了, 就一條規(guī)則:

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

這里面實際上涉及到兩個問題:

  1. 哪些地方是運(yùn)行在主線程的?
  2. 不在主線程做, 在哪兒做?

稍后解答.

2, ANR分析

2.1 獲取ANR產(chǎn)生的trace文件

ANR產(chǎn)生時, 系統(tǒng)會生成一個traces.txt的文件放在/data/anr/下. 可以通過adb命令將其導(dǎo)出到本地:

$adb pull data/anr/traces.txt .

2.2 分析traces.txt

2.2.1 普通阻塞導(dǎo)致的ANR

獲取到的tracs.txt文件一般如下:

如下以GithubApp代碼為例, 強(qiáng)行sleep thread產(chǎn)生的一個ANR.

----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp  // 最新的ANR發(fā)生的進(jìn)程(包名)

...

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過長時間, 阻塞導(dǎo)致無響應(yīng).
  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)  // 產(chǎn)生ANR的那個函數(shù)調(diào)用
  - 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. 文件最上的即為最新產(chǎn)生的ANR的trace信息.
  2. 前面兩行表明ANR發(fā)生的進(jìn)程pid, 時間, 以及進(jìn)程名字(包名).
  3. 尋找我們的代碼點, 然后往前推, 看方法調(diào)用棧, 追溯到問題產(chǎn)生的根源.

以上的ANR trace是屬于相對簡單, 還有可能你并沒有在主線程中做過于耗時的操作, 然而還是ANR了. 這就有可能是如下兩種情況了:

2.2.2 CPU滿負(fù)荷

這個時候你看到的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. 當(dāng)是CPU占用100%, 滿負(fù)荷了.
  2. 其中絕大數(shù)是被iowait即I/O操作占用了.

此時分析方法調(diào)用棧, 一般來說會發(fā)現(xiàn)是方法中有頻繁的文件讀寫或是數(shù)據(jù)庫讀寫操作放在主線程來做了.

2.2.3 內(nèi)存原因

其實內(nèi)存原因有可能會導(dǎo)致ANR, 例如如果由于內(nèi)存泄露, App可使用內(nèi)存所剩無幾, 我們點擊按鈕啟動一個大圖片作為背景的activity, 就可能會產(chǎn)生ANR, 這時trace信息可能是這樣的:

// 以下trace信息來自網(wǎng)絡(luò), 用來做個示例
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的內(nèi)存已所剩無幾.

當(dāng)然這種情況可能更多的是會產(chǎn)生OOM的異常...

2.2 ANR的處理

針對三種不同的情況, 一般的處理情況如下

  1. 主線程阻塞的
    開辟單獨(dú)的子線程來處理耗時阻塞事務(wù).

  2. CPU滿負(fù)荷, I/O阻塞的
    I/O阻塞一般來說就是文件讀寫或數(shù)據(jù)庫操作執(zhí)行在主線程了, 也可以通過開辟子線程的方式異步執(zhí)行.

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

3, 深入一點

沒有人愿意在出問題之后去解決問題.
高手和新手的區(qū)別是, 高手知道怎么在一開始就避免問題的發(fā)生. 那么針對ANR這個問題, 我們需要做哪些層次的工作來避免其發(fā)生呢?

3.1 哪些地方是執(zhí)行在主線程的

  1. Activity的所有生命周期回調(diào)都是執(zhí)行在主線程的.
  2. Service默認(rèn)是執(zhí)行在主線程的.
  3. BroadcastReceiver的onReceive回調(diào)是執(zhí)行在主線程的.
  4. 沒有使用子線程的looper的Handler的handleMessage, post(Runnable)是執(zhí)行在主線程的.
  5. AsyncTask的回調(diào)中除了doInBackground, 其他都是執(zhí)行在主線程的.
  6. View的post(Runnable)是執(zhí)行在主線程的.

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

上面我們幾乎一直在說, 避免ANR的方法就是在子線程中執(zhí)行耗時阻塞操作. 那么在Android中有哪些方式可以讓我們實現(xiàn)這一點呢.

3.2.1 啟Thread方式

這個其實也是Java實現(xiàn)多線程的方式. 有兩種實現(xiàn)方法, 繼承Thread 或 實現(xiàn)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();

實現(xiàn)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顧名思義, 就是異步任務(wù)的意思.

private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
    // Do the long-running work in here
    // 執(zhí)行在子線程
    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()
    // 執(zhí)行在主線程
    protected void onProgressUpdate(Integer... progress) {
        setProgressPercent(progress[0]);
    }

    // This is called when doInBackground() is finished
    // 執(zhí)行在主線程
    protected void onPostExecute(Long result) {
        showNotification("Downloaded " + result + " bytes");
    }
}

// 啟動方式
new DownloadFilesTask().execute(url1, url2, url3);

3.2.3 HandlerThread

Android中結(jié)合Handler和Thread的一種方式. 前面有云, 默認(rèn)情況下Handler的handleMessage是執(zhí)行在主線程的, 但是如果我給這個Handler傳入了子線程的looper, handleMessage就會執(zhí)行在這個子線程中的. HandlerThread正是這樣的一個結(jié)合體:

// 啟動一個名為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是運(yùn)行在new_thread這個子線程中了.
    }
}

3.2.4 IntentService

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

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

3.2.5 Loader

Android 3.0引入的數(shù)據(jù)加載器, 可以在Activity/Fragment中使用. 支持異步加載數(shù)據(jù), 并可監(jiān)控數(shù)據(jù)源在數(shù)據(jù)發(fā)生變化時傳遞新結(jié)果. 常用的有CursorLoader, 用來加載數(shù)據(jù)庫數(shù)據(jù).

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

//如果 ID 指定的加載器已存在,則將重復(fù)使用上次創(chuàng)建的加載器。
//如果 ID 指定的加載器不存在,則 initLoader() 將觸發(fā) LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以實現(xiàn)代碼以實例化并返回新加載器

// 創(chuàng)建一個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);
}

具體請參看官網(wǎng)Loader介紹.

3.2.6 特別注意

使用Thread和HandlerThread時, 為了使效果更好, 建議設(shè)置Thread的優(yōu)先級偏低一點:

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

因為如果沒有做任何優(yōu)先級設(shè)置的話, 你創(chuàng)建的Thread默認(rèn)和UI Thread是具有同樣的優(yōu)先級的, 你懂的. 同樣的優(yōu)先級的Thread, CPU調(diào)度上還是可能會阻塞掉你的UI Thread, 導(dǎo)致ANR的.

結(jié)語

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

強(qiáng)行插入一波:
之前發(fā)的打造一款開源的Android平臺的Github客戶端的這個客戶端有了自己的名字了, 叫做CoderPub. 歡迎大家持續(xù)關(guān)注, 參與, 貢獻(xiàn)...
另外, 我也啟用了weibo賬戶anly-jun, 歡迎互粉.


轉(zhuǎn)載請注明出處, 歡迎大家分享到朋友圈, 微博~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容