Android ANR問題總結

在實際情況中,當Android項目的用戶量特別大時候,一些細小的問題也會被放大,ANR問題就是一個典型的例子。
一些ANR問題只會發生在用戶實際使用的情景,當系統資源比較緊張等一些特殊情況下才會遇到,而這些ANR問題有很大一部分是因爲我們的代碼不合理導致,這就需要我們定位問題,修復問題,並且在以後的代碼設計中儘量避免這些不合理。
最近工作中集中分析了項目的大量的用戶自動上報的ANR問題日誌,雖然網上ANR相關的文章已經很多了,在這裏還是做一個總結。

提綱

一. 什麼情況下會出現ANR
二. ANR機制的原理
三. 如何分析ANR問題
四. 如何避免ANR問題

一.什麼情況下會出現ANR問題:

ANR(Application Not responding)。Android中,主線程(UI線程)如果在規定時內沒有處理完相應工作,就會出現ANR。
具體來說,ANR會在以下幾種情況中出現:

  1. 輸入事件(按鍵和觸摸事件)5s內沒被處理: Input event dispatching timed out
  2. BroadcastReceiver的事件(onRecieve方法)在規定時間內沒處理完(前臺廣播爲10s,後臺廣播爲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 前臺20s後臺200s未完成啓動 Timeout executing service
  4. ContentProvider的publish在10s內沒進行完:timeout publishing content providers
    在android文檔(https://developer.android.com/training/articles/perf-anr.html)中,只寫了第一種和第二種情況,而根據源碼和實際的實驗,我們能發現service的啓動和provider的publish同樣會造成anr問題。
    這裏需要注意的是,在後三種情況,以BroadcastReviever爲例,在onRecieve()方法執行10秒內沒發生第一種ANR(也就是在這個過程中沒有輸入事件或輸入事件還沒到5s)纔會發生Receiver timeout,否則將先發生事件無相應ANR,所以onRecieve()是有可能執行不到10s就發生ANR的,所以不要在onRecieve()方法裏面幹活,service的onCreate()和ContentProvider的onCreate()也一樣,他們都是主線程的,不要在這些方法裏幹活,這個會在本文最後再細說。

二.ANR機制的實現原理:

文章:http://gityuan.com/2016/07/02/android-anr/從源碼角度詳細的分析了ANR機制實現的原理。對於上一章講到的1-4中情況,分別找到了其源碼中是如何實現的,對於每一種大概原理如下:1.在進行相關操作調用hander.sendMessageAtTime()發送一個ANR的消息,延時時間爲ANR發生的時間(如前臺Service是當前時間20s之後)。2.進行相關的操作3.操作結束後向remove掉該條message。如果相關的操作在規定時間沒有執行完成,該條message將被handler取出並執行,就發生了ANR。
下面以BroadcastReceiver爲例詳細介紹:
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.發送延時消息
                            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.取消延時消息
                        cancelBroadcastTimeoutLocked();
                        ...
                    }
                } while (r == null) ;
                ...
            }
        }
    }

1.發送延時消息: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;
            }
        }

他調用了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)的時間xxx + mTimeoutPeriod,mTimeoutPeriod就是onRecieve()可以執行的時間,在BroadcastQueue初始化時候被賦值,前臺隊列爲10s後臺隊列爲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()爲廣播的實際處理,就不展開了
  2. cancelBroadcastTimeoutLocked() :
    該方法的主要工作是當service啓動完成,則移除服務超時消息SERVICE_TIMEOUT_MSG。
    final void cancelBroadcastTimeoutLocked() {
        if (mPendingBroadcastTimeoutMessage) {
            mHandler.removeMessages(BROADCAST_TIMEOUT_MSG, this);
            mPendingBroadcastTimeoutMessage = false;
        }
    }

三.如何分析ANR問題:

從前文可以明確,ANR問題是由於主線程的任務在規定時間內沒處理完任務,而造成這種情況的原因大致會有一下幾點:

  1. 主線程在做一些耗時的工作
  2. 主線程被其他線程鎖
  3. cpu被其他進程佔用,該進程沒被分配到足夠的cpu資源。

判斷一個ANR屬於哪種情況便是分析ANR問題的關鍵。那麼拿到一個anr的日誌,應該如何分析呢?
在發生ANR的時候,系統會收集ANR相關的信息提供給開發者:首先在Log中有ANR相關的信息,其次會收集ANR時的CPU使用情況,還會收集trace信息,也就是當時各個線程的執行情況。trace文件保存到了/data/anr/traces.txt中,此外,ANR前後該進程打印出的log也有一定價值。一般來說可以按一下思路來分析:

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

  2. 在該條log之後會有CPU usage的信息,表明了CPU在ANR前後的用量(log會表明截取ANR的時間),從各種CPU Usage信息中大概可以分析如下幾點:
    (1). 如果某些進程的CPU佔用百分比較高,幾乎佔用了所有CPU資源,而發生ANR的進程CPU佔用爲0%或非常低,則認爲CPU資源被佔用,進程沒有被分配足夠的資源,從而發生了ANR。這種情況多數可以認爲是系統狀態的問題,並不是由本應用造成的。
    (2). 如果發生ANR的進程CPU佔用較高,如到了80%或90%以上,則可以懷疑應用內一些代碼不合理消耗掉了CPU資源,如出現了死循環或者後臺有許多線程執行任務等等原因,這就要結合trace和ANR前後的log進一步分析了。
    (3). 如果CPU總用量不高,該進程和其他進程的佔用過高,這有一定概率是由於某些主線程的操作就是耗時過長,或者是由於主進程被鎖造成的。

  3. 除了上述的情況(1)以外,分析CPU usage之後,確定問題需要我們進一步分析trace文件。trace文件記錄了發生ANR前後該進程的各個線程的stack。對我們分析ANR問題最有價值的就是其中主線程的stack,一般主線程的trace可能有如下幾種情況:
    (1). 主線程是running或者native而對應的棧對應了我們應用中的函數,則很有可能就是執行該函數時候發生了超時。
    (2). 主線程被block:非常明顯的線程被鎖,這時候可以看是被哪個線程鎖了,可以考慮優化代碼。如果是死鎖問題,就更需要及時解決了。
    (3). 由於抓trace的時刻很有可能耗時操作已經執行完了(ANR -> 耗時操作執行完畢 ->系統抓trace),這時候的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@[email protected] (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)

當然這種情況很有可能是由於該進程的其他線程消耗掉了CPU資源,這就需要分析其他線程的trace以及ANR前後該進程自己輸出的log了。

四.如何降低ANR的概率:

有一些操作是很危險的,非常容易發生ANR,在寫代碼時候一定要避免:

  1. 主線程讀取數據:在Android中主線程去讀取數據是非常不好的,Android是不允許主線程從網絡讀數據的,但系統允許主線程從數據庫或者其他地方獲取數據,但這種操作ANR風險很大,也會造成掉幀等,影響用戶體驗。
    (1). 避免在主線程query provider,首先這會比較耗時,另外這個操作provider那一方的進程如果掛掉了或者正在啓動,我們應用的query就會很長時間不會返回,我們應該在其他線程中執行數據庫query、provider的query等獲取數據的操作。
    (2). sharePreference的調用:針對sharePreference的優化點有很多,文章http://weishu.me/2016/10/13/sharedpreference-advices/ 詳細介紹了幾點sharepreference使用時候的注意事項。首先sharePreference的commit()方法是同步的,apply()方法一般是異步執行的。所以在主線程不要用其commit(),用apply()替換。其次sharePreference的寫是全量寫而非增量寫,所以儘量都修改完同一apply,避免改一點apply一次(apply()方法在Activity stop的時候主線程會等待寫入完成,提交多次就很容易卡)。並且存儲文本也不宜過大,這樣會很慢。另外,如果寫入的是json或者xml,由於需要加和刪轉義符號,速度會比較慢。
  2. 不要在broadcastReciever的onRecieve()方法中幹活,這一點很容易被忽略,尤其應用在後臺的時候。爲避免這種情況,一種解決方案是直接開的異步線程執行,但此時應用可能在後臺,系統優先級較低,進程很容易被系統殺死,所以可以選擇開個IntentService去執行相應操作,即使是後臺Service也會提高進程優先級,降低被殺可能性。
  3. 各個組件的生命週期函數都不應該有太耗時的操作,即使對於後臺Service或者ContentProvider來講,應用在後臺運行時候其onCreate()時候不會有用戶輸入引起事件無響應ANR,但其執行時間過長也會引起Service的ANR和ContentProvider的ANR。
  4. 儘量避免主線程的被鎖的情況,在一些同步的操作主線程有可能被鎖,需要等待其他線程釋放相應鎖才能繼續執行,這樣會有一定的ANR風險,對於這種情況有時也可以用異步線程來執行相應的邏輯。另外, 我們要避免死鎖的發生(主線程被死鎖基本就等於要發生ANR了)。



作者:qintong000
鏈接:https://www.jianshu.com/p/fa962a5fd939
來源:簡書
簡書著作權歸作者所有,任何形式的轉載都請聯繫作者獲得授權並註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章