淺談 Android 內存監控(上)

文章最後更新時間:2020.03.08html

前言

性能優化是一個永不過期的話題,對於移動端來講也是如此。即便如今的終端設備更新速度突飛猛進,但用戶老是但願你的應用能越快越好,秒啓動,流暢無違和的操做,及時的操做響應等等。java

移動端性能優化是一個很泛的話題,以筆者目前的認知水平來講,大概可分如下幾個方面:android

  • 內存優化
  • UI 渲染優化
  • IO 優化
  • 弱網優化
  • 電量優化

對於通常的應用來講,UI 渲染優化內存優化 是兩個必需要作的事情,由於這兩個方面是平常開發比較常見的,同時對於用戶來講,也是更直接的影響。好比,若是內存出現問題,經常會致使長時間內存佔用太高,繼而致使內存抖動,更極端的狀況會出現 OOM 異常,致使應用奔潰。若是 UI 繪製渲染出現問題,經常會出現丟幀的狀況,對於用戶來講,就是卡頓,操做不流暢。git

關於本文

關於 內存優化 這個話題,網上有不少相關的文章,會告訴你怎麼去減小內存佔用等等,因此這篇文章不會涉及這些知識,正像標題所說的同樣,偏向於怎麼去作內存監控。文章會分紅上下兩篇,這是第一篇,主要是調研現有的方案,包括微信開源的 Martix 中的 Matrix-Android-ResourceCanary,美團的 Probe:Android線上OOM問題定位組件,還有開源的 LeakCanary。第二篇,會嘗試去開發一個內存監控組件。github

技術難點

咱們先把相關技術難點先提出來,再帶着問題去看看上面列舉的這些方案的解決思路。shell

  1. hprof 文件裁剪數組

    hprof 是堆內存快照文件,能夠方便的定位內存問題,但有一點不可忽視的問題,就是文件過大。hprof 文件動則 70M,80M 多,有的應用可能會超過 100M,對於測試環境來講,這個可能不是問題,但若是在生產環境的話,對於用戶設備來講,這會是個不小的負擔(不論是流量或者電量來講),並且若是涉及日誌回撈,那上傳成功率可能會大打折扣。再者,當咱們在設備上進行分析操做時,內存的佔用也會處於更高的水平線,這可能會加重應用當前的內存問題,甚至可能會致使 OOM。性能優化

  2. 實時監控微信

    咱們須要監控哪些對象,好比 Activity,Fragment 或者是其餘的對象,檢查頻率又該是怎麼樣的,好比一分鐘檢查一次,或者是一天檢查一次等等。對於測試環境來講,咱們能夠採用更高的監控等級,監控更多的對象,使用更高的檢查頻率,生產環境則須要更爲保守的方案。markdown

  3. 優化 dump 操做

    Android 設備上,要 dump 當前內存快照,通常會調用 Debug.dumpHprofData() 方法,接入過 LeakCanary 的同窗會知道,當執行 dump 操做時,經常會致使應用停頓幾秒,在生產環境執行 dump 操做的話,若是時間過長,可能會給用戶帶來糟糕的體驗,甚至可能會觸發 ANR。

可能還有其餘問題,但暫時還沒考慮到,後續再補充。

調研方案

網上能蒐集到方案可能不少,這裏咱們選取幾個比較有參考價值的:

  • LeakCanary
  • 微信的 Matrix
  • 美團的 Probe

技術更新很是快,後續有其餘更好的方案,再繼續更新。

LeakCanary

LeakCanary 多是 Android 同窗很是熟悉的開源項目了,它有不少優勢,如今已經開發了 2.0 了,使用新的 hrpof 分析工具 shark 代替原來的 haha,對於上面提出的難點,LeakCanary 主要作了 hprof 文件裁剪實時監控

hprof 文件裁剪

LeakCanary 用 shark 組件來實現裁剪 hprof 文件功能,在 shark-cli 工具中,咱們能夠經過添加 strip-hprof 選項來裁剪 hprof 文件,它的實現思路是:經過將全部基本類型數組替換爲空數組(大小不變)。

執行如下命令:

shark-cli strip-hprofHPROF_FILE_PATH
複製代碼

相關源碼:

when (record) {                                                  
  is BooleanArrayDump -> BooleanArrayDump(                       
      record.id, record.stackTraceSerialNumber,                  
      BooleanArray(record.array.size)                            
  )                                                              
  is CharArrayDump -> CharArrayDump(                             
      record.id, record.stackTraceSerialNumber,                  
      CharArray(record.array.size) {                              
        '?'                                                      
      }                                                          
  )                                                              
  is FloatArrayDump -> FloatArrayDump(                           
      record.id, record.stackTraceSerialNumber,                  
      FloatArray(record.array.size)                              
  )                                                              
  is DoubleArrayDump -> DoubleArrayDump(                         
      record.id, record.stackTraceSerialNumber,                  
      DoubleArray(record.array.size)                             
  )                                                              
  is ByteArrayDump -> ByteArrayDump(                             
      record.id, record.stackTraceSerialNumber,                  
      ByteArray(record.array.size)                               
  )                                                              
  is ShortArrayDump -> ShortArrayDump(                           
      record.id, record.stackTraceSerialNumber,                  
      ShortArray(record.array.size)                              
  )                                                              
  is IntArrayDump -> IntArrayDump(                               
      record.id, record.stackTraceSerialNumber,                  
      IntArray(record.array.size)                                
  )                                                              
  is LongArrayDump -> LongArrayDump(                             
      record.id, record.stackTraceSerialNumber,                  
      LongArray(record.array.size)                               
  )                                                              
  else -> {                                                      
    record                                                       
  }                                                              
}                                                                
複製代碼

實驗下來,這個裁剪的收益很小,72M 的文件大概只裁剪了 62KB,可能這個功能不是爲了裁剪文件大小,而是其餘目的,有知道的同窗能夠告知下。

實時監控

LeakCanary 主要是爲測試環境開發,它會在 Activity 或者 Fragment 的 destory 生命週期後,能夠檢測 Activity 和 Fragment 是否被回收,來判斷它們是否存在泄露的狀況。更多相關知識,能夠參考筆者以前的文章:LeakCanary2 源碼分析

微信的 Matrix

微信開源的 Matrix 組件是個很是不錯的項目,其中關於內存監控部分是 ResourceCanary,和 LeakCanary 同樣, Matrix 主要也是在 hprof 文件裁剪實時監控 這兩方面作了一些優化。

hprof 文件裁剪

Matrix 的裁剪思路主要是將除了部分字符串和 Bitmap 之外實例對象中的 buffer 數組。之因此保留 Bitmap 是由於 Matirx 有個檢測重複 Bitmap 的功能,會對 Bitmap 的 buffer 數組作一次 MD5 操做來判斷是否重複。

@Override                                                                                                              
public void visitHeapDumpPrimitiveArray(int tag, ID id, int stackId, int numElements, int typeId, byte[] elements) {   
    final ID deduplicatedID = mBmpBufferIdToDeduplicatedIdMap.get(id);                                                 
    // Discard non-bitmap or duplicated bitmap buffer but keep reference key. 
    if (deduplicatedID == null || !id.equals(deduplicatedID)) {                                                        
        if (!mStringValueIds.contains(id)) {                                                                           
            skipData += elements.length;                                                                               
            return;                                                                                                    
        }                                                                                                              
    }                                                                                                                  
    super.visitHeapDumpPrimitiveArray(tag, id, stackId, numElements, typeId, elements);                                
}                                                                                                                      
複製代碼

Martix 的裁剪收益仍是比較可觀的,原文件 92M 裁剪後只有 18M。

實時監控

Matrix 是基於 LeakCanary 上進行二次開發,因此監控原理基本是一致的,主要增長了一些誤報的優化,好比:

  • 屢次檢測到相同的可疑對象,才認定爲泄露對象,參數可配置。

    if (destroyedActivityInfo.mDetectedCount < mMaxRedetectTimes                                
            || !mResourcePlugin.getConfig().getDetectDebugger()) {                              
        // Although the sentinel tell us the activity should have been recycled, 
        // system may still ignore it, so try again until we reach max retry times. 
        MatrixLog.i(TAG, "activity with key [%s] should be recycled but actually still \n"      
                        + "exists in %s times, wait for next detection to confirm.",            
                destroyedActivityInfo.mKey, destroyedActivityInfo.mDetectedCount);              
        continue;                                                                               
    }                                                                                           
    複製代碼
  • 增長一個哨兵對象,用於判斷是否有 GC 操做,由於調用 Runtime.getRuntime().gc() 只是建議虛擬機進行 GC 操做,並不必定會進行。

    if (sentinelRef.get() != null) {                                                       
        // System ignored our gc request, we will retry later. 
        MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");        
        return Status.RETRY;                                                               
    }                                                                                      
    複製代碼
  • 避免重複檢測相同的對象

    if (sentinelRef.get() != null) {                                                       
        // System ignored our gc request, we will retry later. 
        MatrixLog.d(TAG, "system ignore our gc request, wait for next detection.");        
        return Status.RETRY;                                                               
    }                                                                                      
    複製代碼

Matrix 雖然是基於 LeakCnary,但額外增長了一些配置選項,能夠用於生產環境,好比 dump 模式,支持手動觸發 dump,自動 dump,和不進行 dump,能夠根據不一樣的環境,使用不一樣的模式。

public enum DumpMode {                            
    NO_DUMP, AUTO_DUMP, MANUAL_DUMP, SILENCE_DUMP 
}                                                 
複製代碼

除此以前,還有檢測時間間隔等等。

還有一點就是,Matrix 是將分析 hprof 操做獨立出來的,可使用 resource-canary-analyzer 去執行分析操做。

美團的 Probe

Probe 不是開源的,因此只能經過相關的文章 Probe:Android線上OOM問題定位組件 和一些其餘手段去了解。

Probe 一樣是在 hprof 文件裁剪實時監控 這兩方面作了優化。

注意事項⚠️:對 Probe 組件的研究純粹只是出於學習目的,請不要用作其餘用途。

hprof 文件裁剪

與 Matrix 不一樣的是,Probe 支持在設備上進行分析 hprof 操做,因此它不只考慮了 hprof 文件上傳成功率,還考慮加載 hprof 帶來的內存佔用高問題。不論是 LeakCanary 仍是 Matrix,在裁剪的處理上,都是先將原始的 hprof 文件 dump 出來之後,過濾掉某些數據後,再從新寫入到新的 hprof 文件。而 Probe 的處理則更加有創意,它經過 native hook 了虛擬機寫入 hprof 文件的操做,在這裏先過濾了某些數據,最終生成的 hprof 文件就是裁剪後的了。在不考慮可能會有的兼容性問題,這個方案要比其餘的方案要高效,由於它解決了 hprof 加載的內存問題。

美團在測試中發現,分析 hprof 文件的佔用內存和 hprof 中記錄對象實例數量是成正比的,

測試時遇到的最大問題就是分析進程自身常常會發生OOM,致使分析失敗。爲了弄清楚分析進程爲何會佔用這麼大內存,咱們作了兩個對比實驗:

  • 在一個最大可用內存256MB的手機上,讓一個成員變量申請特別大的一塊內存200多MB,人造OOM,Dump內存,分析,內存快照文件達到250多MB,分析進程佔用內存並不大,爲70MB左右。
  • 在一個最大可用內存256MB的手機上,添加200萬個小對象(72字節),人造OOM,Dump內存,分析,內存快照文件達到250多MB,分析進程佔用內存增加很快,在解析時就發生OOM了。

實驗說明,分析進程佔用內存與HPROF文件中的Instance數量是正相關的,在將HPROF文件映射到內存中解析時,若是Instance的數量太大,就會致使OOM。

計數壓縮邏輯:若是存在重複的相同實例,則增長它的計數,不保存它的實例。Probe 定義超過實例超過 8000,則不會再繼續保持它的實例對象。

hprof 文件分析

除了文件裁剪,Probe 還優化分析泄露對象的鏈路,由於 Probe 相對於其餘方案,理論上它是支持全部對象的內存泄露檢測的(排除了原始類型等),而 LeakCanary 和 Matrix 只支持 Activity 和 Fragment 等對象,這個優化是基於 RetainSize 越大的對象對內存的影響也越大,是最有可能形成OOM的「元兇」 這一原則。

首先,在 dump 出全部對象後,建立一個 TOP N 的小根堆,根據 RetainSize 排序,初始 N 默認是 5,這裏有個小細節,就是 Probe 會處理 ByteArray 類型的實例:

if (isByteArray(var6)) {
    var6.parent.addRetainedSize(var1.getHeapIndex(var4), var6.getTotalRetainedSize());
    var15.add(var6.parent);
}
複製代碼

將 ByteArray 的 RetainSize 加到它的父節點,同時將它的父節點加到堆中。

在初始化小根堆後,繼續遍歷作動態調整,將大於文件大小 5% 的對象直接加到堆中,同時增大 TOP N 的值,不然,跟堆頂元素作比較。在遍歷對象的同時,若是是相同類的不一樣實例,則只會保存一份實例,同時將它們的 RetainSize 和 Num 計算進去:

if (var3.containsKey(var9)) {                            
    InstanceExtra var17 = (InstanceExtra)var3.get(var9); 
    var17.retainSize += var6.getTotalRetainedSize();     
    ++var17.num;
}
複製代碼

還會將在 計數壓縮邏輯 中 RetainSize 補回來:

if (var10 != null) {                                                                                  
    var16.retainSize += var10.getSize();                                                              
    long var11 = var16.num;                                                                           
    var16.num = (long)var10.getCount() + var11;                                                       
    ClassCountInfo var18 = (ClassCountInfo)AbandonedInstanceManager.getInstance().countMap.get(var9); 
    if (var18 != null && var18.classId > 0L) {                                                        
        var16.id = var18.classId;                                                                     
    }                                                                                                 
}                                                                                                     
複製代碼

實時監控

Probe 對監控 Activity 對象的處理方法和 Matrix 的 DumpMode 相似,都是分級處理,Probe 線上監控到 Activity 泄露後,不會進行 dump 操做,只是對 Activity 類名等進行上報處理(這些都是能夠配置的)。

除了 Activity 覺得,Probe 還會經過開啓一個內存監控器,每隔 1s 查看當前應用可用內存,當剩餘內存低於 10% (觸底)時,會進行內存分析。

當發生 OOM 時,也會進行內存分析。

以前的配置都是經過接口下發的,這也能保證最大程度的靈活性。

小結

從上面三種方案中,咱們能夠得出一些結論:LeakCanary 雖然只會在測試環境使用,且只提供 Activity 和 Fragment 等對象的監控,但它提供監控的思路和 hprof 的分析,Matrix 和 Probe 或多或少都是基於它進行二次開發和優化。Matrix 主要是優化泄露對象的誤判和 hprof 文件的裁剪,Probe 則是優化了在設備上進行 hprof 文件分析的內存佔用,同時支持檢測內存中全部對象(RetainSize 大的對象)。

惋惜的是,上面幾種方案都沒有涉及如何優化 heap dump 操做,這個留着後續研究。

關於 OOM

Out of memory (OOM) 當系統沒法知足申請的內存大小時,就會拋出 OOM 錯誤。致使 OOM 的緣由,除了咱們上面說講的內存泄露之外,可能還會是線程建立超過限制,能夠經過 /proc/sys/kernel/threads-max 獲取:

cat /proc/sys/kernel/threads-max
26418
複製代碼

還有一種多是 FD(File descriptor)文件描述符超過限制,能夠經過 /proc/pid/limits 獲取,其中 Max open files 就是可建立的文件描述符數量:

Limit                     Soft Limit           Hard Limit           Units
Max open files            4096                 4096                 files
複製代碼

參考文章

Probe:Android線上OOM問題定位組件

Matrix ResourceCanary

相關文章
相關標籤/搜索