您的位置:  首頁 > 技術 > java語言 > 正文

【架構筆記】Android 內存泄漏知識點匯總

2021-10-16 08:00 管理員 次閱讀 條評論

檢測內存是否泄漏非常簡單,只要在任意位置調用 Debug.dumpHprofData(file) 即可,通過拿到 hprof 文件進行分析就可以知道哪里產生了泄漏,但 dump 的過程會 suspend 所有的 java 線程,導致用戶界面無響應,所以又不能隨意 dump。為了能找到合理的 dump 時機,leakCanary 就采用預判的方式,在 onDestroy 中先檢測一下當前 Activity 是否存在泄漏的風險,如果有這種情況,就開始 dump。需要注意的是,在 onDestroy 做檢測僅僅只是預判,一種時機,并不能斷定真的發生了泄漏,真正的泄漏需要通過分析 hprof 文件才能知曉。 ?

hprof 是由 JVM TI Agent HPROF 生成的一種二進制文件,文件格式可以查看 Binary Dump Format

一、如何預判內存泄漏

  • 主動檢測法
  • 閾值檢測法

1、主動檢測法

  • Activity 的檢測預判
  • Service 的檢測預判
  • Bitmap 大圖的檢測預判

1、Activity 的檢測預判 LeakCanary 中對 Activity 的預判是在 onDestroy 生命周期中通過弱引用隊列來持有當前 Activity 引用,如果在主動觸發 gc 之后,泄漏對象集合中仍然能找到該引用實例,則說明發生了內存泄漏,就開始 dump ?

2、Service 的檢測預判 LeakCanary 對 Service 的內存泄漏檢測時機,是 hook 監聽 ActivityThread 的 stopService,然后記錄這個 binder 到弱引用集合中,然后代理 AMS 的 serviceDoneExecuting 方法,通過 binder 在弱引用集合中去移除,移除成功的話,說明發生了內存泄漏,就開始 dump ?

3、Bitmap 大圖檢測預判 Bitmap 不像 Activity、Service 這種,能夠通過生命周期主動監測當前是否有內存泄漏的可能,他一般是在 Activity、Service 發生泄漏 dump 的時候,順便檢測一下 Bitmap 。在 Koom 中,Bitmap 大圖檢測是分析 hprof 中是否有超過 Bitmap 設置的閾值 size (width * height) ?

2、閾值檢測法

閾值檢測法的代表框架是 Koom,他拋棄了 LeakCanary 的實時檢測性,采用定時輪訓檢測當前內存是否在不斷累加,增長達到一定次數(可自己配置)時會進行 dump hprof,這種方式會犧牲一定的時效性,但對于應用到線上的 Koom 的框架,他完全不需要這么高的時效性 ?

二、如何分析內存泄漏

分析工具代表:

  • MAT
  • Android Studio
  • HaHa
    • Matrix
    • LeakCanary 1.x
  • shark
    • Liko
    • Koom
    • LeakCanary 2.x

1、MAT

MAT 工具下載可點擊鏈接 ,Android 生成的 dump 需要做一下轉換才能被 MAT 識別,轉換指令:

hprof-conv <hprof 文件> <新生成的文件>

eg:

hprof-conv android.hprof mat.hprof

hprof-conv 跟 adb 在同一個文件夾下,配置了 adb 命令的可以直接用這個命令執行。 ?

MAT 查內存泄漏會有點費勁,畢竟是個 java 通用工具,并不會指明告訴你是哪個 Activity 發生了泄漏,但可以分析個大概。 ?

一般泄漏的都是比較大的實例:

在這里插入圖片描述

點擊類名進入查看: ?

在這里插入圖片描述

ActivityLeakMaker 占用了近 190944 byte 的內存空間,并且引用鏈里面有 Activity 相關的內容,切回代碼來看問題,原來是靜態變量持有了 Activity 實例導致:

在這里插入圖片描述

2、Android Studio

Android Studio 的 Profiler 工具支持 hprof 的解析,并且很智能的提示當前 leak 了哪些對象,打開方式很簡單,將 hprof 文件拖拽至 as,然后雙擊 hprof 文件即可:

在這里插入圖片描述

我們可以很直觀的看到,當前 LeakedActivity 和 ReportFragment 發生了泄漏。 ?

如果我們的需求僅僅只是在開發階段進行內存泄漏檢測的話,并且又不想接入 LeakCanary(因為有時候想調試下自己模塊的代碼,其他模塊經常報內存泄漏,凍結當前線程,很影響調試),那么我們可以在應用里面埋個彩蛋,比如單擊 5 次版本號,然后調用 Debug.dumpHprofData ,然后將 hprof 文件導出到 as 進行分析,這就將原本可能會進行數次 dump 的過程,改成了自己需要去檢測的時候再去 dump。 ?

3、HaHa

在 LeakCanary 的第一版的時候,是采用的 Haha 庫來分析泄漏引用鏈,但由于后面新出的 Shark,比 HaHa 快 8 倍,并且內存占用還要少 10 倍,但查找泄漏路徑的大致步驟與 Shark 無異,故此文就不分析 HaHa 了。 ?

4、Shark

Shark 是 square 團隊開發的一款全新的分析 hprof 文件的工具,其官方宣布比 Android Studio 用于 memory profiler 的核心庫 perflib 要快 8 倍并且內存占用少 10 倍,更加適合手機端的分析工具。其目的就是提供快速解析hprof文件和分析快照的能力,并找出真正的泄漏對象以及對象到GcRoot 的最短引用路徑鏈,以便幫助開發者更加直觀的找出泄漏的真正原因。 – 引用自《LeakCanary2.0解析

看了下 Koom 分析引用鏈的過程,大致可以分為以下幾個步驟:

  • 分析 hprof 文件,獲取鏡像所有的 instance 實例
  • 遍歷所有的實例,判斷這個實例與各個 Detectors 是否有存在泄漏,如果有,則記錄 objectId 到集合
  • 根據 objectId 集合獲取各個泄漏實例引用鏈,分析出 gcRoot,并遍歷 gcRoot 下的引用路徑

這個地方重點在于如何找到泄漏的 objectId,因為找到 objectId,即可找到泄漏引用鏈。在分析 hprof 的時候我們可以拿到 dump 時的內存實例,那么,我們可以根據這個實例來判斷是否泄漏,例如:

  • Activity : 判斷實例是否是 android.app.Activity 的子類,并且 mFinished 或 mDestroyed 是否為 true (Activity 關閉時該值會為 true),因為 Activity 不泄露的話肯定是會被釋放,所以,不可能存在于 dump 的實例中,有就是發生了泄漏
  • Bitmap : 獲取實例的類名稱是否為 android.graphics.Bitmap,如果是的話,則獲取實例的 mWidth 和 mHeight 實例變量,計算兩者的乘積是否超過閾值,是的話,也判定為泄漏
  • … (更多判斷可以看 analysis 目錄的各個 Detector)

Shark 根據 objectId 分析出的引用鏈路徑:

   ┬───
   │ GC Root: Local variable in native code
   │
   ├─ android.os.HandlerThread instance
   │    Leaking: UNKNOWN
   │    ↓ HandlerThread.contextClassLoader
   │                    ~~~~~~~~~~~~~~~~~~
   ├─ dalvik.system.PathClassLoader instance
   │    Leaking: UNKNOWN
   │    ↓ PathClassLoader.runtimeInternalObjects
   │                      ~~~~~~~~~~~~~~~~~~~~~~
   ├─ java.lang.Object[] array
   │    Leaking: UNKNOWN
   │    ↓ Object[].[197]
   │               ~~~~~
   ├─ com.kwai.koom.demo.leaked.ActivityLeakMaker$LeakedActivity class
   │    Leaking: UNKNOWN
   │    ↓ static ActivityLeakMaker$LeakedActivity.uselessObjectList
   │                                              ~~~~~~~~~~~~~~~~~
   ├─ java.util.ArrayList instance
   │    Leaking: UNKNOWN
   │    ↓ ArrayList.elementData
   │                ~~~~~~~~~~~
   ├─ java.lang.Object[] array
   │    Leaking: UNKNOWN
   │    ↓ Object[].[0]
   │               ~~~
   ╰→ com.kwai.koom.demo.leaked.ActivityLeakMaker$LeakedActivity instance
  ?     Leaking: YES (This is the leaking object), Signature: 39f4102649e5d3a5be12db591c2e5f68a1c0d2e9

三、如何應用于線上

1、解決 dump 凍結問題

由于 dump hprof 會暫停所有 java 線程問題,致使 LeakCanary 只能應用于線下檢測。但 Koom 和 Liko 另辟蹊徑,采用 linux 的 copy-on-write 機制,從當前的主線程 fork 出一個子進程,然后在子進程進行 dump 分析,對于用戶所在的進程不會有任何感知。 ?

這個地方會有個坑,就是在 fork 子進程的時候 dump hprof。由于 dump 前會先 suspend 所有的 java 線程,等所有線程都掛起來了,才會進行真正的 dump。由于 copy-on-write 機制,子進程也會將父進程中的 threadList 也拷貝過來,但由于 threadList 中的 java 線程活動在父進程,子進程是無法掛起父進程中的線程的,然后就會一直處于等待中。 ?

為了解決這個問題,Koom 和 Liko 采用欺騙的方式,在 fork 子進程之前,先將父進程中的 threadList 全部設置為 suspend 狀態,然后 fork 子進程,子進程在 dump 的時候發現 threadList 都為掛起狀態了,就立馬開始 dump hprof,然后父進程在 fork 操作之后,立馬 resume 恢復回 threadList 的狀態 ?

2、解決混淆問題

Shark 支持混淆反解析,思路也很簡單,解析 mapping.txt 文件,每次讀取一行,只解析類和字段:

  • 類特征 :行尾為 : 冒號結尾,然后根據 -> 作為 index 分割,左邊的為原類名,右邊的為混淆類名
  • 字段特征:行尾不為 : 冒號結尾,并且不包含 (括號(帶括號的為方法),即為字段特征,根據 -> 作為 index 分割,左邊為原字段名,右邊的為混淆字段名

將混淆類名、字段名作為 key,原類名、原字段名作為 value 存入 map 集合,在分析出內存泄漏的引用路徑類時,將類名和字段名都通過這個 map 集合去拿到原始類名和字段名即可,即完成混淆后的反解析 ?

leakCanary 內部是寫死的 mapping 文件為 leakCanaryObfuscationMapping.txt,如果打開該文件失敗,則不做引用鏈反解析:
在這里插入圖片描述

也即意味著,如果想 LeakCanary 支持混淆反解析,只需要將自己的 mapping 文件重命名為 leakCanaryObfuscationMapping.txt,然后放入 asset 目錄即可

對于 Koom 的混淆反解析,Koom 并沒有做,但我們可以自己去加這塊代碼:

private boolean buildIndex() {
    ...
    try {
      // 新增 ---------- start
      InputStream is =  KGlobalConfig.getApplication().getResources().getAssets().open("mapping.txt");
      ProguardMapping mapping = new ProguardMappingReader(is).readProguardMapping();
     //  新增 ---------- end

      heapGraph = HprofHeapGraph.Companion.indexHprof(hprof, mapping,
              kotlin.collections.SetsKt.setOf(gcRoots));
    } catch (Exception e) {
      e.printStackTrace();
    }
    return true;
 }         

將 mapping.txt 文件放到 asset 目錄即可,如下是混淆與混淆反解析的引用鏈的對比:

在這里插入圖片描述

3、泄漏兜底

在預判內存泄漏發生時,我們可以將 Activity 中引用到的 Bitmap、DrawingCache 等進行主動釋放,以此來降低泄漏的影響面。做法是,在 Activity onDestory 時候從 view 的 rootview 開始,遞歸釋放所有子 view 涉及的圖片、背景、DrawingCache、監聽器等等資源,讓 Activity 成為一個不占資源的空殼,泄露了也不會導致圖片資源被持有,eg:

...
    Drawable d = iv.getDrawable();
if (d != null) {
    d.setCallback(null);
}        
iv.setImageDrawable(null);
...
...

但這一點對于閾值檢測法的 Koom 來說,沒辦法做到,因為他拿不到 onDestroy 時的 Activity 實例,但也不要緊,我們可以將兜底操作做成通用操作,不管他泄漏與不泄露,都做 view 相關引用的卸載。

四、總結:

整體下來,分析個內存泄漏其實并不難,難就難在我們平時并沒有養成好的習慣,對于引用的傳遞考慮的不周全,但我們可以加強自身的編碼習慣,盡量減少項目中的泄漏問題

  • 0
    感動
  • 0
    路過
  • 0
    高興
  • 0
    難過
  • 0
    搞笑
  • 0
    無聊
  • 0
    憤怒
  • 0
    同情
熱度排行
友情鏈接
18禁高潮出水呻吟娇喘mp3,日本熟妇乱人伦A片免费高清,成人午夜精品无码区,狠狠色噜噜色狠狠狠综合久久,麻豆一区二区99久久久久,年轻的妈妈4,少妇被又大又粗又爽毛片,护士张开腿让我爽了一夜,男男互攻互受h啪肉np文,你好神枪手电视剧免费观看啊,97人妻一区二区精品免费,久久久婷婷五月亚洲97号色,freegaysexvideos男男中国,国产精品国产三级国av麻豆,国产精品又黄又爽又色无遮挡网站,亚洲av无码一区二区三区网站,亚洲国产精品久久久久蜜桃,国产真人无码作爱视频免费,国产成人精品亚洲一区二区三区,亚洲欧洲日产最新,老司机带带我精彩免费,国产成人久久精品激情,日本最新av免费一区二区三区,边摸边吃奶又黄又激烈视频
<蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <蜘蛛词>| <文本链> <文本链> <文本链> <文本链> <文本链> <文本链>