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

引言

  1. 背景:Android App優(yōu)化, 要怎么做?
  2. Android App優(yōu)化之性能分析工具
  3. Android App優(yōu)化之提升你的App啟動(dòng)速度之理論基礎(chǔ)
  4. Android App優(yōu)化之提升你的App啟動(dòng)速度之實(shí)例挑戰(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ò)請(qǐng)求

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

原計(jì)劃將本文作為這個(gè)系列的一個(gè)承上啟下點(diǎn), 對(duì)前面幾篇作一個(gè)小總結(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)操作在一段時(shí)間內(nèi)系統(tǒng)無法處理時(shí), 系統(tǒng)層面會(huì)彈出上圖那樣的ANR對(duì)話框.

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

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

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

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

1.3 如何避免ANR

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

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

這里面實(shí)際上涉及到兩個(gè)問題:

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

稍后解答.

2, ANR分析

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

ANR產(chǎn)生時(shí), 系統(tǒng)會(huì)生成一個(gè)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)生的一個(gè)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過長時(shí)間, 阻塞導(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的那個(gè)函數(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的起點(diǎn)
  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, 時(shí)間, 以及進(jìn)程名字(包名).
  3. 尋找我們的代碼點(diǎn), 然后往前推, 看方法調(diào)用棧, 追溯到問題產(chǎn)生的根源.

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

2.2.2 CPU滿負(fù)荷

這個(gè)時(shí)候你看到的trace信息可能會(huì)包含這樣的信息:

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操作占用了.

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

2.2.3 內(nèi)存原因

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

// 以下trace信息來自網(wǎng)絡(luò), 用來做個(gè)示例
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)然這種情況可能更多的是會(huì)產(chǎn)生OOM的異常...

2.2 ANR的處理

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

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

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

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

3, 深入一點(diǎn)

沒有人愿意在出問題之后去解決問題.
高手和新手的區(qū)別是, 高手知道怎么在一開始就避免問題的發(fā)生. 那么針對(duì)ANR這個(gè)問題, 我們需要做哪些層次的工作來避免其發(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í)行耗時(shí)阻塞操作. 那么在Android中有哪些方式可以讓我們實(shí)現(xiàn)這一點(diǎn)呢.

3.2.1 啟Thread方式

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

實(shí)現(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

這個(gè)是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");
    }
}

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

3.2.3 HandlerThread

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

// 啟動(dòng)一個(gè)名為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) {
      // 此時(shí)handleMessage是運(yùn)行在new_thread這個(gè)子線程中了.
    }
}

3.2.4 IntentService

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

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

3.2.5 Loader

Android 3.0引入的數(shù)據(jù)加載器, 可以在Activity/Fragment中使用. 支持異步加載數(shù)據(jù), 并可監(jiān)控?cái)?shù)據(jù)源在數(shù)據(jù)發(fā)生變化時(shí)傳遞新結(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()桩盲。在此方法中排抬,您可以實(shí)現(xiàn)代碼以實(shí)例化并返回新加載器

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

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

3.2.6 特別注意

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

Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);

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

結(jié)語

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

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


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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市黔州,隨后出現(xiàn)的幾起案子碘橘,更是在濱河造成了極大的恐慌,老刑警劉巖碉钠,帶你破解...
    沈念sama閱讀 206,311評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件纲缓,死亡現(xiàn)場(chǎng)離奇詭異卷拘,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)祝高,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門栗弟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人工闺,你說我怎么就攤上這事乍赫。” “怎么了陆蟆?”我有些...
    開封第一講書人閱讀 152,671評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵雷厂,是天一觀的道長。 經(jīng)常有香客問我叠殷,道長改鲫,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,252評(píng)論 1 279
  • 正文 為了忘掉前任林束,我火速辦了婚禮像棘,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘诊县。我一直安慰自己讲弄,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,253評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般引镊。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瓶摆,一...
    開封第一講書人閱讀 49,031評(píng)論 1 285
  • 那天,我揣著相機(jī)與錄音性宏,去河邊找鬼群井。 笑死,一個(gè)胖子當(dāng)著我的面吹牛毫胜,可吹牛的內(nèi)容都是我干的书斜。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼酵使,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼荐吉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起口渔,我...
    開封第一講書人閱讀 36,973評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤样屠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體痪欲,經(jīng)...
    沈念sama閱讀 43,466評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡悦穿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,937評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了业踢。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栗柒。...
    茶點(diǎn)故事閱讀 38,039評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖陨亡,靈堂內(nèi)的尸體忽然破棺而出傍衡,到底是詐尸還是另有隱情,我是刑警寧澤负蠕,帶...
    沈念sama閱讀 33,701評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站倦畅,受9級(jí)特大地震影響遮糖,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叠赐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,254評(píng)論 3 307
  • 文/蒙蒙 一欲账、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧芭概,春花似錦赛不、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至惹苗,卻和暖如春殿较,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背桩蓉。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來泰國打工淋纲, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人院究。 一個(gè)月前我還...
    沈念sama閱讀 45,497評(píng)論 2 354
  • 正文 我出身青樓洽瞬,卻偏偏與公主長得像,于是被迫代替她去往敵國和親业汰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子伙窃,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,786評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢(shì)1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 171,504評(píng)論 25 707
  • 被文同時(shí)發(fā)布在CSDN上,歡迎查看蔬胯。 APP內(nèi)存的使用对供,是評(píng)價(jià)一款應(yīng)用性能高低的一個(gè)重要指標(biāo)。雖然現(xiàn)在智能手機(jī)的內(nèi)...
    大圣代閱讀 4,803評(píng)論 2 54
  • 看動(dòng)漫看到現(xiàn)在也有那么多年了呢,一直都有點(diǎn)想法产场,現(xiàn)在就來說說鹅髓,反正閑著沒事干 說句實(shí)話,我看動(dòng)漫都是看日本的京景,很少...
    BINGBIGN閱讀 585評(píng)論 3 6
  • 加入21天持續(xù)寫作營的收獲 2016-12-10鑠心難得一世生命 不知不覺間20天就這樣過去窿冯,今天又是群主何以寬要...
    鑠心閱讀 268評(píng)論 0 0