源碼茶舍之由一次簡單的ANR分析深刻了解Context

ANR是Android的老大難了,關於這方面的基礎知識和深刻好文都很是多,你們不妨谷歌一下。 最近搭載驍龍855的小米9也發佈了,移動平臺的設備性能愈來愈強,許多App大多時候其實都吃不完那麼多計算資源。 說得可能很差聽一點,不少爛代碼要是在不少年前的手機上,本該致使卡頓(甚至是ANR)的,但因爲現在強大的計算性能,卡頓概率大大減少了。從某方面來講增大了程序的容錯,同時也掩蓋了程序自己的缺陷。java

今天的題目關鍵詞是「簡單分析」和「深刻了解」,哈哈,可能對於大佬們來講這些內容並不深刻,因此我措辭爲「瞭解」,望輕噴。android

分析traces文件

前段時間,業務質量平臺報上來不少ANR,我是一看就頭疼呀!每次內心都犯嘀咕,我怎麼就歷來沒遇到ANR呢?大家究竟是怎麼使用的。 吐槽歸吐槽,問題仍是要解決的,Android的系統日誌打包上來通常都會有traces.txt文件(還有event log等等,這裏給你們硬廣一下我另外一篇使用可視化的ChkBugreport分析log文件),也是咱們分析這類問題的入口,裏面記錄了各個應用進程和系統進程的函數堆棧信息。因而乎,抓一份來瞧瞧:數據庫

"main" prio=5 tid=1 Blocked
group="main" sCount=1 dsCount=0 obj=0x75afba88 self=0x7fb0e96a00
...
at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:483)
- waiting to lock <0x0cfeaaf2> (a java.lang.Object) held by thread 24
at android.app.ContextImpl.getSharedPreferencesPath(ContextImpl.java:665)
at android.app.ContextImpl.getSharedPreferences(ContextImpl.java:364)
- locked <0x09b0b543> (a java.lang.Class<android.app.ContextImpl>)
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:174)
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:174)
...
at com.xxx.receiver.xxx.onReceive(xxx.java:36)
...
複製代碼

這裏簡單解釋一下,ANR無非就是UI線程Block了,因此咱們找到形如 "main" prio=5 tid=1 Blocked 這樣的片斷,main表示主線程,prio即priority,線程優先級(這裏不是重點),tid就是thread的id,即線程id,最後標記了Blocked,表示線程阻塞了。 接着的信息就是告訴你線程被哪一個鬼lock了,關注這行: waiting to lock <0x0cfeaaf2> (a java.lang.Object) held by thread 24 說明主線程的getPreferencesDir方法等着要去鎖一個id爲0x0cfeaaf2的Object類型的對象,可是被該死的tid=24的線程搶佔了!讓我來看看是誰,因而咱們能夠直接在traces文件裏全局搜索0x0cfeaaf2或者tid=24這些字符串,鎖定到以下日誌:安全

"PackageProcessor" daemon prio=5 tid=24 Native
group="main" sCount=1 dsCount=0 obj=0x32c06af0 self=0x7fb0f36400
...
native: #06 pc 0000000000862c18 /system/framework/arm64/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+196)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:620)
at android.os.storage.IMountService$Stub$Proxy.mkdirs(IMountService.java:870)
at android.app.ContextImpl.ensureExternalDirsExistOrFilter(ContextImpl.java:2228)
at android.app.ContextImpl.getExternalFilesDirs(ContextImpl.java:586)
- locked <0x0cfeaaf2> (a java.lang.Object)
at android.app.ContextImpl.getExternalFilesDir(ContextImpl.java:569)
at android.content.ContextWrapper.getExternalFilesDir(ContextWrapper.java:243)
at com.xxx.push.log.xxx.writeLog2File(xxx.java:100)
...
複製代碼

這裏很明顯就看到了 locked <0x0cfeaaf2> (a java.lang.Object) ,某個和推送服務相關的writeLog2File方法調用了getExternalFilesDirs,而後此方法進一步鎖住了 0x0cfeaaf2 對象,沒錯,這個對象和剛纔主線程等待要鎖的對象是同一個。 因此主線程被tid=24的線程阻塞了,由於兩個線程須要同一把對象鎖,tid=24線程一直佔着茅坑,致使死鎖,ANR就這麼爆出來了。bash

瞭解Context

Context是一個抽象類,ContextImpl是Context的實現類(具體一些繼承關係可參考Context都沒弄明白,還怎麼作Android開發?,某大佬寫的,比較全面)。 那麼,上面的ANR咱們重點關注的對象0x0cfeaaf2究竟是誰呢?根據這一行: at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:483) 咱們直接Read the fucking code,看看ContextImpl中這個方法在幹啥:app

private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
複製代碼

可見,這裏涉及到shared_prefs文件的IO操做,系統考慮到線程安全,搞了個同步鎖,mSync對象被鎖住。這個mSync就是咱們剛纔反覆提到的id爲0x0cfeaaf2的Object對象,去看看它的實例化就知曉了:異步

private final Object mSync = new Object();
複製代碼

private final,兩個關鍵字合體了,說明這個成員是不可變的,並且是私有的,不許繼承,即在Context的生命週期內全局只實例化一次,這樣才能在加鎖的時候保證惟一性。 接下來又看剛纔tid=24給對象加鎖的方法,源碼天然也在ContextImpl中:ide

@Override
    public File[] getExternalFilesDirs(String type) {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
            if (type != null) {
                dirs = Environment.buildPaths(dirs, type);
            }
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }
複製代碼

OK,它也有給mSync加鎖的操做, 因此tid=24線程的getExternalFilesDirs方法先加鎖,形成主線程的getPreferencesDir方法搶不到這把鎖,這真是喧賓奪主啊! 你區區一個子線程和主線程做對,分析到此咱們基本清楚了此次ANR是怎麼來的了。 這裏咱們進一步看看上面return的ensureExternalDirsExistOrFilter方法:函數

/** * Ensure that given directories exist, trying to create them if missing. If * unable to create, they are filtered by replacing with {@code null}. */
    private File[] ensureExternalDirsExistOrFilter(File[] dirs) {
        final StorageManager sm = getSystemService(StorageManager.class);
        final File[] result = new File[dirs.length];
        for (int i = 0; i < dirs.length; i++) {
            File dir = dirs[i];
            if (!dir.exists()) {
                if (!dir.mkdirs()) {
                    // recheck existence in case of cross-process race
                    if (!dir.exists()) {
                        // Failing to mkdir() may be okay, since we might not have
                        // enough permissions; ask vold to create on our behalf.
                        try {
                            sm.mkdirs(dir);
                        } catch (Exception e) {
                            Log.w(TAG, "Failed to ensure " + dir + ": " + e);
                            dir = null;
                        }
                    }
                }
            }
            result[i] = dir;
        }
        return result;
    }
複製代碼

個人天鴨,你看看,這操做多重啊,又是循環又是建立文件的,還有getSystemService這些系統服務對端調用,加在一塊兒就是灰常耗時的操做,尤爲是在文件目錄極其散亂繁雜並且磁盤讀寫性能還很差的時候,此方法將進一步延長阻塞時間。性能

我又一想,什麼SP啊,DB啊,外部存儲啊這些咱們平時常常訪問啊,也並非那麼容易就ANR的。也就是說雖然上面的系統方法操做很繁雜,但應該不是致使最終問題的核心因素。

通過我反覆分析traces文件,發現除了main線程在wait to lock這把鎖,還有幾個其它的子線程也在等待鎖(有一些是訪問App本地數據庫的,最終調用也在ContextImpl中,和上面分析的兩個方法相似)。說明當前這短暫的時間內,須要經過某個Context進行的IO操做太多了,各個線程都排着隊要鎖mSync,因此耗時操做不可怕,可怕的是一窩蜂全上來。天然就增大了ANR的風險。若是你反覆遇到這種ANR,就應該考慮優化了。

最終,追溯到方法調用的源頭,是在Application初始化時,各類SDK加載,以及一些業務邏輯觸發。很顯然,它們都是經過getApplicationContext來拿到的同一個Context引用,請求鎖的也是同一個mSync對象。

結論與建議

  • 調用Context相關的IO操做,不是啓個子線程就高枕無憂了,由上面分析,mSync對象鎖就這麼一把,該阻塞仍是阻塞,和是否是主線程無關。
  • 儘可能不要在Application的初始化時刻進行太多的方法調用,尤爲是針對ApplicationContext的IO操做。
  • 在主Activity中延後初始化,用IntentService進行異步操做(由於實例化一個Service就是另外一個Context對象了)等都是比較好的優化方案。
  • 因此爲何有大佬說不要濫用SharedPreference,它的性能並非很好,從本文分析也可知它直接可能阻塞UI線程,試圖尋找其它替代品吧。
  • 廣播接收onReceive裏面能夠用goAsync異步處理,見:goAsync幫你在onReceive中簡便地進行異步操做
  • ...想到再說,也歡迎你們補充。
相關文章
相關標籤/搜索