Android ANR問題總結(jié)

在實(shí)際情況中,當(dāng)Android項(xiàng)目的用戶量特別大時(shí)候,一些細(xì)小的問題也會(huì)被放大,ANR問題就是一個(gè)典型的例子。
一些ANR問題只會(huì)發(fā)生在用戶實(shí)際使用的情景,當(dāng)系統(tǒng)資源比較緊張等一些特殊情況下才會(huì)遇到,而這些ANR問題有很大一部分是因?yàn)槲覀兊拇a不合理導(dǎo)致,這就需要我們定位問題,修復(fù)問題,并且在以后的代碼設(shè)計(jì)中盡量避免這些不合理。
最近工作中集中分析了項(xiàng)目的大量的用戶自動(dòng)上報(bào)的ANR問題日志,雖然網(wǎng)上ANR相關(guān)的文章已經(jīng)很多了,在這里還是做一個(gè)總結(jié)。

提綱

一. 什么情況下會(huì)出現(xiàn)ANR
二. ANR機(jī)制的原理
三. 如何分析ANR問題
四. 如何避免ANR問題

一.什么情況下會(huì)出現(xiàn)ANR問題:

ANR(Application Not responding)。Android中,主線程(UI線程)如果在規(guī)定時(shí)內(nèi)沒有處理完相應(yīng)工作,就會(huì)出現(xiàn)ANR。
具體來說,ANR會(huì)在以下幾種情況中出現(xiàn):

  1. 輸入事件(按鍵和觸摸事件)5s內(nèi)沒被處理: Input event dispatching timed out
  2. BroadcastReceiver的事件(onRecieve方法)在規(guī)定時(shí)間內(nèi)沒處理完(前臺(tái)廣播為10s,后臺(tái)廣播為60s):Timeout of broadcast BroadcastRecord
    07-27 19:18:47.448 1707 1766 W BroadcastQueue: Receiver during timeout: ResolveInfo{ccd831e com.example.qintong.myapplication/.MyBroadCastReciever m=0x108000}
    07-27 19:18:47.502 3513 3728 I WtEventController: ANR com.example.qintong.myapplication 7573
  3. service 前臺(tái)20s后臺(tái)200s未完成啟動(dòng) Timeout executing service
  4. ContentProvider的publish在10s內(nèi)沒進(jìn)行完:timeout publishing content providers
    在android文檔(https://developer.android.com/training/articles/perf-anr.html)中,只寫了第一種和第二種情況,而根據(jù)源碼和實(shí)際的實(shí)驗(yàn),我們能發(fā)現(xiàn)service的啟動(dòng)和provider的publish同樣會(huì)造成anr問題。
    這里需要注意的是,在后三種情況,以BroadcastReviever為例,在onRecieve()方法執(zhí)行10秒內(nèi)沒發(fā)生第一種ANR(也就是在這個(gè)過程中沒有輸入事件或輸入事件還沒到5s)才會(huì)發(fā)生Receiver timeout,否則將先發(fā)生事件無相應(yīng)ANR,所以onRecieve()是有可能執(zhí)行不到10s就發(fā)生ANR的,所以不要在onRecieve()方法里面干活,service的onCreate()和ContentProvider的onCreate()也一樣,他們都是主線程的,不要在這些方法里干活,這個(gè)會(huì)在本文最后再細(xì)說。

二.ANR機(jī)制的實(shí)現(xiàn)原理:

文章:http://gityuan.com/2016/07/02/android-anr/從源碼角度詳細(xì)的分析了ANR機(jī)制實(shí)現(xiàn)的原理。對(duì)于上一章講到的1-4中情況,分別找到了其源碼中是如何實(shí)現(xiàn)的,對(duì)于每一種大概原理如下:1.在進(jìn)行相關(guān)操作調(diào)用hander.sendMessageAtTime()發(fā)送一個(gè)ANR的消息,延時(shí)時(shí)間為ANR發(fā)生的時(shí)間(如前臺(tái)Service是當(dāng)前時(shí)間20s之后)。2.進(jìn)行相關(guān)的操作3.操作結(jié)束后向remove掉該條message。如果相關(guān)的操作在規(guī)定時(shí)間沒有執(zhí)行完成,該條message將被handler取出并執(zhí)行,就發(fā)生了ANR。
下面以BroadcastReceiver為例詳細(xì)介紹:
BroadcastQueue.processNextBroadcast()

     final void processNextBroadcast(boolean fromMsg) {
        ...
        synchronized (mService) {
            ...
            do {
                if (mOrderedBroadcasts.size() == 0) {
                    ...
                    if (mService.mProcessesReady && r.dispatchTime > 0) {
                        long now = SystemClock.uptimeMillis();
                        if ((numReceivers > 0) &&
                                (now > r.dispatchTime + (2 * mTimeoutPeriod * numReceivers))) {
                            //1.發(fā)送延時(shí)消息
                            broadcastTimeoutLocked(false); // forcibly finish this broadcast
                            forceReceive = true;
                            r.state = BroadcastRecord.IDLE;
                        }
                    }

                    if (r.state != BroadcastRecord.IDLE) {
                        if (DEBUG_BROADCAST) Slog.d(TAG,
                                "processNextBroadcast("
                                        + mQueueName + ") called when not idle (state="
                                        + r.state + ")");
                        return;
                    }

                    if (r.receivers == null || r.nextReceiver >= numReceivers
                            || r.resultAbort || forceReceive) {
                        // No more receivers for this broadcast!  Send the final
                        // result if requested...
                        if (r.resultTo != null) {
                            try {
                                //2. 處理廣播消息
                                performReceiveLocked(r.callerApp, r.resultTo,
                                        new Intent(r.intent), r.resultCode,
                                        r.resultData, r.resultExtras, false, false, r.userId);
                                // Set this to null so that the reference
                                // (local and remote) isn't kept in the mBroadcastHistory.
                                r.resultTo = null;
                            } catch (RemoteException e) {
                                ...
                            }
                        }
                        //3.取消延時(shí)消息
                        cancelBroadcastTimeoutLocked();
                        ...
                    }
                } while (r == null) ;
                ...
            }
        }
    }

1.發(fā)送延時(shí)消息:broadcastTimeoutLocked(false):

    final void broadcastTimeoutLocked(boolean fromMsg) {
    ...
        long now = SystemClock.uptimeMillis();
        if (fromMsg) {
            if (mService.mDidDexOpt) {
                // Delay timeouts until dexopt finishes.
                mService.mDidDexOpt = false;
                long timeoutTime = SystemClock.uptimeMillis() + mTimeoutPeriod;
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
            if (!mService.mProcessesReady) {
                return;
            }

            long timeoutTime = r.receiverTime + mTimeoutPeriod;
            if (timeoutTime > now) {
                setBroadcastTimeoutLocked(timeoutTime);
                return;
            }
        }

他調(diào)用了setBroadcastTimeoutLocked(long timeoutTime):

    final void setBroadcastTimeoutLocked(long timeoutTime) {
        if (! mPendingBroadcastTimeoutMessage) {
            Message msg = mHandler.obtainMessage(BROADCAST_TIMEOUT_MSG, this);
            mHandler.sendMessageAtTime(msg, timeoutTime);
            mPendingBroadcastTimeoutMessage = true;
        }
    }

傳入setBroadcastTimeoutLocked(long timeoutTime)的時(shí)間xxx + mTimeoutPeriod,mTimeoutPeriod就是onRecieve()可以執(zhí)行的時(shí)間,在BroadcastQueue初始化時(shí)候被賦值,前臺(tái)隊(duì)列為10s后臺(tái)隊(duì)列為60s:
ActivityManagerService.java:

    public ActivityManagerService(Context systemContext) {
        ...
        static final int BROADCAST_FG_TIMEOUT = 10 * 1000;
        static final int BROADCAST_BG_TIMEOUT = 60 * 1000;
        ...
        mFgBroadcastQueue = new BroadcastQueue(this, mHandler,
                "foreground", BROADCAST_FG_TIMEOUT, false);
        mBgBroadcastQueue = new BroadcastQueue(this, mHandler,
                "background", BROADCAST_BG_TIMEOUT, true);
        ...
    }
  1. performReceiveLocked()為廣播的實(shí)際處理,就不展開了
  2. cancelBroadcastTimeoutLocked() :
    該方法的主要工作是當(dāng)service啟動(dòng)完成,則移除服務(wù)超時(shí)消息SERVICE_TIMEOUT_MSG。
    final void cancelBroadcastTimeoutLocked() {
        if (mPendingBroadcastTimeoutMessage) {
            mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
            mPendingBroadcastTimeoutMessage = false;
        }
    }

三.如何分析ANR問題:

從前文可以明確,ANR問題是由于主線程的任務(wù)在規(guī)定時(shí)間內(nèi)沒處理完任務(wù),而造成這種情況的原因大致會(huì)有一下幾點(diǎn):

  1. 主線程在做一些耗時(shí)的工作
  2. 主線程被其他線程鎖
  3. cpu被其他進(jìn)程占用,該進(jìn)程沒被分配到足夠的cpu資源。

判斷一個(gè)ANR屬于哪種情況便是分析ANR問題的關(guān)鍵。那么拿到一個(gè)anr的日志,應(yīng)該如何分析呢?
在發(fā)生ANR的時(shí)候,系統(tǒng)會(huì)收集ANR相關(guān)的信息提供給開發(fā)者:首先在Log中有ANR相關(guān)的信息,其次會(huì)收集ANR時(shí)的CPU使用情況,還會(huì)收集trace信息,也就是當(dāng)時(shí)各個(gè)線程的執(zhí)行情況。trace文件保存到了/data/anr/traces.txt中,此外,ANR前后該進(jìn)程打印出的log也有一定價(jià)值。一般來說可以按一下思路來分析:

  1. 從log中找到ANR反生的信息:可以從log中搜索“ANR in”或“am_anr”,會(huì)找到ANR發(fā)生的log,該行會(huì)包含了ANR的時(shí)間、進(jìn)程、是何種ANR等信息,如果是BroadcastReceiver的ANR可以懷疑BroadCastReceiver.onRecieve()的問題,如果的Service或Provider就懷疑是否其onCreate()的問題。

  2. 在該條log之后會(huì)有CPU usage的信息,表明了CPU在ANR前后的用量(log會(huì)表明截取ANR的時(shí)間),從各種CPU Usage信息中大概可以分析如下幾點(diǎn):
    (1). 如果某些進(jìn)程的CPU占用百分比較高,幾乎占用了所有CPU資源,而發(fā)生ANR的進(jìn)程CPU占用為0%或非常低,則認(rèn)為CPU資源被占用,進(jìn)程沒有被分配足夠的資源,從而發(fā)生了ANR。這種情況多數(shù)可以認(rèn)為是系統(tǒng)狀態(tài)的問題,并不是由本應(yīng)用造成的。
    (2). 如果發(fā)生ANR的進(jìn)程CPU占用較高,如到了80%或90%以上,則可以懷疑應(yīng)用內(nèi)一些代碼不合理消耗掉了CPU資源,如出現(xiàn)了死循環(huán)或者后臺(tái)有許多線程執(zhí)行任務(wù)等等原因,這就要結(jié)合trace和ANR前后的log進(jìn)一步分析了。
    (3). 如果CPU總用量不高,該進(jìn)程和其他進(jìn)程的占用過高,這有一定概率是由于某些主線程的操作就是耗時(shí)過長,或者是由于主進(jìn)程被鎖造成的。

  3. 除了上述的情況(1)以外,分析CPU usage之后,確定問題需要我們進(jìn)一步分析trace文件。trace文件記錄了發(fā)生ANR前后該進(jìn)程的各個(gè)線程的stack。對(duì)我們分析ANR問題最有價(jià)值的就是其中主線程的stack,一般主線程的trace可能有如下幾種情況:
    (1). 主線程是running或者native而對(duì)應(yīng)的棧對(duì)應(yīng)了我們應(yīng)用中的函數(shù),則很有可能就是執(zhí)行該函數(shù)時(shí)候發(fā)生了超時(shí)。
    (2). 主線程被block:非常明顯的線程被鎖,這時(shí)候可以看是被哪個(gè)線程鎖了,可以考慮優(yōu)化代碼。如果是死鎖問題,就更需要及時(shí)解決了。
    (3). 由于抓trace的時(shí)刻很有可能耗時(shí)操作已經(jīng)執(zhí)行完了(ANR -> 耗時(shí)操作執(zhí)行完畢 ->系統(tǒng)抓trace),這時(shí)候的trace就沒有什么用了,主線程的stack就是這樣的:

"main" prio=5 tid=1 Native
  | group="main" sCount=1 dsCount=0 obj=0x757855c8 self=0xb4d76500
  | sysTid=3276 nice=0 cgrp=default sched=0/0 handle=0xb6ff5b34
  | state=S schedstat=( 50540218363 186568972172 209049 ) utm=3290 stm=1764 core=3 HZ=100
  | stack=0xbe307000-0xbe309000 stackSize=8MB
  | held mutexes=
  kernel: (couldn't read /proc/self/task/3276/stack)
  native: #00 pc 0004099c  /system/lib/libc.so (__epoll_pwait+20)
  native: #01 pc 00019f63  /system/lib/libc.so (epoll_pwait+26)
  native: #02 pc 00019f71  /system/lib/libc.so (epoll_wait+6)
  native: #03 pc 00012ce7  /system/lib/libutils.so (_ZN7android6Looper9pollInnerEi+102)
  native: #04 pc 00012f63  /system/lib/libutils.so (_ZN7android6Looper8pollOnceEiPiS1_PPv+130)
  native: #05 pc 00086abd  /system/lib/libandroid_runtime.so (_ZN7android18NativeMessageQueue8pollOnceEP7_JNIEnvP8_jobjecti+22)
  native: #06 pc 0000055d  /data/dalvik-cache/arm/system@framework@boot.oat (Java_android_os_MessageQueue_nativePollOnce__JI+96)
  at android.os.MessageQueue.nativePollOnce(Native method)
  at android.os.MessageQueue.next(MessageQueue.java:323)
  at android.os.Looper.loop(Looper.java:138)
  at android.app.ActivityThread.main(ActivityThread.java:5528)
  at java.lang.reflect.Method.invoke!(Native method)
  at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:740)
  at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:630)

當(dāng)然這種情況很有可能是由于該進(jìn)程的其他線程消耗掉了CPU資源,這就需要分析其他線程的trace以及ANR前后該進(jìn)程自己輸出的log了。

四.如何降低ANR的概率:

有一些操作是很危險(xiǎn)的,非常容易發(fā)生ANR,在寫代碼時(shí)候一定要避免:

  1. 主線程讀取數(shù)據(jù):在Android中主線程去讀取數(shù)據(jù)是非常不好的,Android是不允許主線程從網(wǎng)絡(luò)讀數(shù)據(jù)的,但系統(tǒng)允許主線程從數(shù)據(jù)庫或者其他地方獲取數(shù)據(jù),但這種操作ANR風(fēng)險(xiǎn)很大,也會(huì)造成掉幀等,影響用戶體驗(yàn)。
    (1). 避免在主線程query provider,首先這會(huì)比較耗時(shí),另外這個(gè)操作provider那一方的進(jìn)程如果掛掉了或者正在啟動(dòng),我們應(yīng)用的query就會(huì)很長時(shí)間不會(huì)返回,我們應(yīng)該在其他線程中執(zhí)行數(shù)據(jù)庫query、provider的query等獲取數(shù)據(jù)的操作。
    (2). sharePreference的調(diào)用:針對(duì)sharePreference的優(yōu)化點(diǎn)有很多,文章http://weishu.me/2016/10/13/sharedpreference-advices/ 詳細(xì)介紹了幾點(diǎn)sharepreference使用時(shí)候的注意事項(xiàng)。首先sharePreference的commit()方法是同步的,apply()方法一般是異步執(zhí)行的。所以在主線程不要用其commit(),用apply()替換。其次sharePreference的寫是全量寫而非增量寫,所以盡量都修改完同一apply,避免改一點(diǎn)apply一次(apply()方法在Activity stop的時(shí)候主線程會(huì)等待寫入完成,提交多次就很容易卡)。并且存儲(chǔ)文本也不宜過大,這樣會(huì)很慢。另外,如果寫入的是json或者xml,由于需要加和刪轉(zhuǎn)義符號(hào),速度會(huì)比較慢。
  2. 不要在broadcastReciever的onRecieve()方法中干活,這一點(diǎn)很容易被忽略,尤其應(yīng)用在后臺(tái)的時(shí)候。為避免這種情況,一種解決方案是直接開的異步線程執(zhí)行,但此時(shí)應(yīng)用可能在后臺(tái),系統(tǒng)優(yōu)先級(jí)較低,進(jìn)程很容易被系統(tǒng)殺死,所以可以選擇開個(gè)IntentService去執(zhí)行相應(yīng)操作,即使是后臺(tái)Service也會(huì)提高進(jìn)程優(yōu)先級(jí),降低被殺可能性。
  3. 各個(gè)組件的生命周期函數(shù)都不應(yīng)該有太耗時(shí)的操作,即使對(duì)于后臺(tái)Service或者ContentProvider來講,應(yīng)用在后臺(tái)運(yùn)行時(shí)候其onCreate()時(shí)候不會(huì)有用戶輸入引起事件無響應(yīng)ANR,但其執(zhí)行時(shí)間過長也會(huì)引起Service的ANR和ContentProvider的ANR。
  4. 盡量避免主線程的被鎖的情況,在一些同步的操作主線程有可能被鎖,需要等待其他線程釋放相應(yīng)鎖才能繼續(xù)執(zhí)行,這樣會(huì)有一定的ANR風(fēng)險(xiǎn),對(duì)于這種情況有時(shí)也可以用異步線程來執(zhí)行相應(yīng)的邏輯。另外, 我們要避免死鎖的發(fā)生(主線程被死鎖基本就等于要發(fā)生ANR了)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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