深入探索 Android 內存優化(煉獄級別)

前言

成爲一名優秀的Android開發,需要一份完備的知識體系,在這裏,讓我們一起成長爲自己所想的那樣~。

本篇是 Android 內存優化的進階篇,難度可以說達到了煉獄級別,建議對內存優化不是非常熟悉的仔細看看前篇文章: Android性能優化之內存優化,其中詳細分析了以下幾大模塊:

  • 1)、Android的內存管理機制
  • 2)、優化內存的意義
  • 3)、避免內存泄漏
  • 4)、優化內存空間
  • 5)、圖片管理模塊的設計與實現

如果你對以上基礎內容都比較瞭解了,那麼我們便開始 Android 內存優化的探索之旅吧。

本篇文章非常長,建議收藏後慢慢享用~

思維導圖大綱

目錄

  • 一、重識內存優化
    • 1、手機RAM
    • 2、內存優化的緯度
    • 3、內存問題
  • 二、常見工具選擇
    • 1、Memory Profiler
    • 2、Memory Analyzer
    • 3、LeakCanary
  • 三、Android內存管理機制回顧
    • 1、Java 內存分配
    • 2、Java 內存回收算法
    • 3、Android 內存管理機制
    • 4、小結
  • 四、內存抖動
    • 1、那麼,爲什麼內存抖動會導致 OOM?
    • 2、內存抖動解決實戰
    • 3、內存抖動常見案例
  • 五、內存優化體系化搭建
    • 1、MAT回顧
    • 2、搭建體系化的圖片優化 / 監控機制
    • 3、建立線上應用內存監控體系
    • 4、建立全局的線程監控組件
    • 5、GC 監控組件搭建
    • 6、建立線上 OOM 監控組件:Probe
    • 7、實現 單機版 的 Profile Memory 自動化內存分析
    • 8、搭建線下 Native 內存泄漏監控體系
    • 9、設置內存兜底策略
    • 10、更深入的內存優化策略
  • 六、內存優化演進
    • 1、自動化測試階段
    • 2、LeakCanary
    • 3、使用基於 LeakCannary 的改進版 ResourceCanary
  • 七、內存優化工具
    • 1、top
    • 2、dumpsys meminfo
    • 3、LeakInspector
    • 4、JHat
    • 5、ART GC Log
    • 6、Chrome Devtool
  • 八、內存問題總結
    • 1、內類是有危險的編碼方式
    • 2、普通 Hanlder 內部類的問題
    • 3、登錄界面的內存問題
    • 4、使用系統服務時產生的內存問題
    • 5、把 WebView 類型的泄漏裝進垃圾桶進程
    • 6、在適當的時候對組件進行註銷
    • 7、Handler / FrameLayout 的 postDelyed 方法觸發的內存問題
    • 8、圖片放錯資源目錄也會有內存問題
    • 9、列表 item 被回收時注意釋放圖片的引用
    • 10、使用 ViewStub 進行佔位
    • 11、注意定時清理 App 過時的埋點數據
    • 12、針對匿名內部類 Runnable 造成內存泄漏的處理
  • 九、內存優化常見問題
    • 1、你們內存優化項目的過程是怎麼做的?
    • 2、你做了內存優化最大的感受是什麼?
    • 3、如何檢測所有不合理的地方?
  • 十、總結
    • 1、優化大方向
    • 2、優化細節
    • 3、內存優化體系化建設總結

一、重識內存優化

Android給每個應用進程分配的內存都是非常有限的,那麼,爲什麼不能把圖片下載下來都放到磁盤中呢?那是因爲放在 內存 中,展示會更 “”,快的原因有兩點,如下所示:

  • 1)、硬件快:內存本身讀取、存入速度快。
  • 2)、複用快:解碼成果有效保存,複用時,直接使用解碼後對象,而不是再做一次圖像解碼。

這裏說一下解碼的概念。Android系統要在屏幕上展示圖片的時候只認 “像素緩衝”,而這也是大多數操作系統的特徵。而我們 常見的jpg,png等圖片格式,都是把 “像素緩衝” 使用不同的手段壓縮後的結果,所以這些格式的圖片,要在設備上 展示,就 必須經過一次解碼,它的 執行速度會受圖片壓縮比、尺寸等因素影響。(官方建議:把從內存中淘汰的圖片,降低壓縮比後存儲到本地,以備後用,這樣可以最大限度地降低以後複用時的解碼開銷。)

下面,我們來了解一下內存優化的一些重要概念。

1、手機RAM

手機不使用 PCDDR內存,採用的是 LPDDR RAM,即 ”低功耗雙倍數據速率內存“。其計算規則如下所示:

LPDDR系列的帶寬 = 時鐘頻率 ✖️內存總線位數 / 8
LPDDR4 = 1600MHZ ✖️64 / 8 ✖️雙倍速率 = 25.6GB/s。

那麼內存佔用是否越少越好?

當系統 內存充足 的時候,我們可以 多用 一些獲得 更好的性能。當系統 內存不足 的時候,我們希望可以做到 ”用時分配,及時釋放“。

2、內存優化的緯度

對於Android內存優化來說又可以細分爲如下兩個維度,如下所示:

  • 1)、RAM優化
  • 2)、ROM優化

1、RAM優化

主要是 降低運行時內存。它的 目的 有如下三個:

  • 1)、防止應用發生OOM
  • 2)、降低應用由於內存過大被LMK機制殺死的概率
  • 3)、避免不合理使用內存導致GC次數增多,從而導致應用發生卡頓

2、ROM優化

降低應用佔ROM的體積,進行APK瘦身。它的 目的 主要是爲了 降低應用佔用空間,避免因ROM空間不足導致程序無法安裝

3、內存問題

那麼,內存問題主要是有哪幾類呢?內存問題通常來說,可以細分爲如下 三類:

  • 1)、內存抖動
  • 2)、內存泄漏
  • 3)、內存溢出

下面,我們來了解下它們。

1、內存抖動

內存波動圖形呈 鋸齒張GC導致卡頓

這個問題在 Dalvik虛擬機 上會 更加明顯,而 ART虛擬機內存管理跟回收策略 上都做了 大量優化內存分配和GC效率相比提升了5~10倍,所以 出現內存抖動的概率會小很多

2、內存泄漏

Android系統虛擬機的垃圾回收是通過虛擬機GC機制來實現的。GC會選擇一些還存活的對象作爲內存遍歷的根節點GC Roots,通過對GC Roots的可達性來判斷是否需要回收。內存泄漏就是 在當前應用週期內不再使用的對象被GC Roots引用,導致不能回收,使實際可使用內存變小。簡言之,就是 對象被持有導致無法釋放或不能按照對象正常的生命週期進行釋放。一般來說,可用內存減少、頻繁GC,容易導致內存泄漏

3、內存溢出

即OOM,OOM時會導致程序異常。Android設備出廠以後,java虛擬機對單個應用的最大內存分配就確定下來了,超出這個值就會OOM。單個應用可用的最大內存對應於 /system/build.prop 文件中的 dalvik.vm.heapgrowthlimit

此外,除了因內存泄漏累積到一定程度導致OOM的情況以外,也有一次性申請很多內存,比如說 一次創建大的數組或者是載入大的文件如圖片的時候會導致OOM。而且,實際情況下 很多OOM就是因圖片處理不當 而產生的。

二、常見工具選擇

Android性能優化之內存優化 中我們已經介紹過了相關的優化工具,這裏再簡單回顧一下。

1、Memory Profiler

作用

  • 1)、實時圖表展示應用內存使用量
  • 2)、用於識別內存泄漏、抖動等
  • 3)、提供捕獲堆轉儲、強制GC以及根據內存分配的能力

優點

  • 1)、方便直觀
  • 2)、線下使用

2、Memory Analyzer

強大的 Java Heap 分析工具,查找 內存泄漏及內存佔用
生成 整體報告分析內存問題 等等。建議 線下深入使用

3、LeakCanary

自動化 內存泄漏檢測神器。建議僅用於線下集成

它的 缺點 比較明顯,具體有如下兩點:

  • 1)、雖然使用了 idleHandler與多進程,但是 dumphprof 的 SuspendAll Thread 的特性依然會導致應用卡頓
  • 2)、在三星等手機,系統會緩存最後一個Activity,此時應該採用更嚴格的檢測模式

三、Android內存管理機制回顧

ART 和 Dalvik 虛擬機使用 分頁和內存映射 來管理內存。下面我們先從Java的內存分配開始說起。

1、Java 內存分配

Java的 內存分配區域 分爲如下 五部分

  • 1)、方法區:主要存放靜態常量
  • 2)、虛擬機棧:Java變量引用
  • 3)、本地方法棧:native變量引用
  • 4)、堆:對象
  • 5)、程序計數器:計算當前線程的當前方法執行到多少行

2、Java 內存回收算法

1、標記-清除算法

流程可簡述爲 兩步

  • 1)、標記所有需要回收的對象
  • 2)、統一回收所有被標記的對象

優點

實現比較簡單。

缺點

  • 1)、標記、清除效率不高
  • 2)、產生大量內存碎片

2、複製算法

流程可簡述爲 三步

  • 1)、將內存劃分爲大小相等的兩塊
  • 2)、一塊內存用完之後複製存活對象到另一塊
  • 3)、清理另一塊內存

優點

實現簡單,運行高效,每次僅需遍歷標記一半的內存區域

缺點

浪費一半的空間,代價大。

3、標記-整理算法

流程可簡述爲 三步

  • 1)、標記過程與 標記-清除算法 一樣
  • 2)、存活對象往一端進行移動
  • 3)、清理其餘內存

優點

  • 1)、避免 標記-清除 導致的內存碎片
  • 2)、避免複製算法的空間浪費

4、分代收集算法

現在 主流的虛擬機 一般用的比較多的還是分代收集算法,它具有如下 特點

  • 1)、結合多種算法優勢
  • 2)、新生代對象存活率低,使用 複製算法
  • 3)、老年代對象存活率高,使用 標記-整理算法

3、Android 內存管理機制

Android 中的內存是 彈性分配 的,分配值 與 最大值 受具體設備影響

對於 OOM場景 其實可以細分爲如下兩種:

  • 1)、內存真正不足
  • 2)、可用(被分配的)內存不足

我們需要着重注意一下這兩種的區分。

4、小結

以Android中虛擬機的角度來說,我們要清楚 Dalvik 與 ART 區別Dalvik 僅固定一種回收算法,而 ART 回收算法可在 運行期按需選擇,並且,ART 具備 內存整理 能力,減少內存空洞

最後,LMK(Low Memory killer) 機制保證了進程資源的合理利用,它的實現原理主要是 根據進程分類和回收收益來綜合決定的一套算法集

四、內存抖動

內存頻繁分配和回收 導致內存 不穩定,就會出現內存抖動,它通常表現爲 頻繁GC、內存曲線呈鋸齒狀

並且,它的危害也很嚴重,通常會導致 頁面卡頓,甚至造成 OOM

1、那麼,爲什麼內存抖動會導致 OOM?

主要原因有如下兩點:

  • 1)、頻繁創建對象,導致內存不足及碎片(不連續)
  • 2)、不連續的內存片無法被分配,導致OOM

2、內存抖動解決實戰

這裏我們假設有這樣一個場景:點擊按鈕使用 handler 發送一個空消息,handler 的 handleMessage 接收到消息後創建內存抖動,即在 for 循環創建 100個容量爲10萬 的 strings 數組並在 30ms 後繼續發送空消息。

一般使用 Memory Profiler (表現爲 頻繁GC、內存曲線呈鋸齒狀)結合代碼排查即可找到內存抖動出現的地方。

通常的技巧就是着重查看 循環或頻繁被調用 的地方。

3、內存抖動常見案例

下面列舉一些導致內存抖動的常見案例,如下所示:

1、字符串使用加號拼接

  • 1)、使用StringBuilder替代
  • 2)、初始化時設置容量,減少StringBuilder的擴容

2、資源複用

  • 1)、使用 全局緩存池,以 重用頻繁申請和釋放的對象
  • 2)、注意 結束 使用後,需要 手動釋放對象池中的對象

3、減少不合理的對象創建

  • 1)、ondraw、getView 中創建的對象儘量進行復用
  • 2)、避免在循環中不斷創建局部變量

4、使用合理的數據結構

使用 SparseArray類族、ArrayMap 來替代 HashMap

五、內存優化體系化搭建

在開始我們今天正式的主題之前,我們先來回歸一下內存泄漏的概念與解決技巧。

所謂的內存泄漏就是 內存中存在已經沒有用的對象。它的 表現 一般爲 內存抖動、可用內存逐漸減少
它的 危害 即會導致 內存不足、GC頻繁、OOM

而對於 內存泄漏的分析 一般可簡述爲如下 兩步

  • 1)、使用 Memory Profiler 初步觀察
  • 2)、通過 Memory Analyzer 結合代碼確認

1、MAT回顧

MAT查找內存泄漏

對於MAT來說,其常規的查找內存泄漏的方式可以細分爲如下三步:

  • 1)、首先,找到當前 Activity,在 Histogram 中選擇其 List Objects 中的 with incoming reference(哪些引用引向了我)
  • 2)、然後,選擇當前的一個 Path to GC Roots/Merge to GC Roots 的 exclude All 弱軟虛引用
  • 3)、最後,找到的泄漏對象在左下角下會有一個小圓圈

此外,在 Android性能優化之內存優化 還有幾種進階的使用方式,這裏就不一一贅述了,下面,我們來看看關於 MAT 使用時的一些關鍵細節。

MAT的關鍵使用細節

要全面掌握MAT的用法,必須要先了解 隱藏在 MAT 使用中的四大細節,如下所示:

  • 1)、善於使用 Regex 查找對應泄漏類
  • 2)、使用 group by package 查找對應包下的具體類
  • 3)、明白 with outgoing references 和 with incoming references 的區別
    • with outgoing references:它引用了哪些對象
    • with incoming references:哪些對象引用了它
  • 4)、瞭解 Shallow Heap 和 Retained Heap 的區別
    • Shallow Heap:表示對象自身佔用的內存
    • Retained Heap:對象自身佔用的內存 + 對象引用的對象所佔用的內存

MAT 關鍵組件回顧

除此之外,MAT 共有 5個關鍵組件 幫助我們去分析內存方面的問題,分別如下所示:

  • 1)、Dominator_tree
  • 2)、Histogram
  • 3)、thread_overview
  • 4)、Top Consumers
  • 5)、Leak Suspects

下面我們這裏再簡單地回顧一下它們。

1、Dominator(支配者):

如果從GC Root到達對象A的路徑上必須經過對象B,那麼B就是A的支配者。

2、Histogram和dominator_tree的區別:

  • 1)、Histogram 顯示 Shallow Heap、Retained Heap、Objects,而 dominator_tree 顯示的是 Shallow Heap、Retained Heap、Percentage
  • 2)、Histogram 基於 的角度,dominator_tree是基於 實例 的角度。Histogram 不會具體顯示每一個泄漏的對象,而dominator_tree會

3、thread_overview

查看 線程數量線程的 Shallow Heap、Retained Heap、Context Class Loader 與 is Daemon

4、Top Consumers

通過 圖形 的形式列出 佔用內存比較多的對象

在下方的 Biggest Objects 還可以查看其 相對比較詳細的信息,例如 Shallow Heap、Retained Heap

5、Leak Suspects

列出有內存泄漏的地方,點擊 Details 可以查看其產生內存泄漏的引用鏈

2、搭建體系化的圖片優化 / 監控機制

在介紹圖片監控體系的搭建之前,首先我們來回顧下 Android Bitmap 內存分配的變化

Android Bitmap 內存分配的變化

在Android 3.0之前

  • 1)、Bitmap 對象存放在 Java Heap,而像素數據是存放在 Native 內存中的
  • 2)、如果不手動調用 recycle,Bitmap Native 內存的回收完全依賴 finalize 函數回調,但是回調時機是不可控的

Android 3.0 ~ Android 7.0

Bitmap對象像素數據 統一放到 Java Heap 中,即使不調用 recycle,Bitmap 像素數據也會隨着對象一起被回收。

但是,Bitmap 全部放在 Java Heap 中的缺點很明顯,大致有如下兩點:

  • 1)、Bitmap是內存消耗的大戶,而 Max Java Heap 一般限制爲 256、512MB,Bitmap 過大過多容易導致 OOM
  • 2)、容易引起大量 GC,沒有充分利用系統的可用內存

Android 8.0及以後

  • 1)、使用了能夠輔助回收 Native 內存的 NativeAllocationRegistry,以實現將像素數據放到 Native 內存中,並且可以和 Bitmap 對象一起快速釋放,最後,在 GC 的時候還可以考慮到這些 Bitmap 內存以防止被濫用
  • 2)、Android 8.0 爲了 解決圖片內存佔用過多和圖像繪製效率過慢 的問題新增了 硬件位圖 Hardware Bitmap

那麼,我們如何將圖片內存存放在 Native 中呢?

將圖片內存存放在Native中的步驟有 四步,如下所示:

  • 1)、調用 libandroid_runtime.so 中的 Bitmap 構造函數,申請一張空的 Native Bitmap。對於不同 Android 版本而言,這裏的獲取過程都有一些差異需要適配
  • 2)、申請一張普通的 Java Bitmap
  • 3)、將 Java Bitmap 的內容繪製到 Native Bitmap 中
  • 4)、釋放 Java Bitmap 內存

我們都知道的是,當 系統內存不足 的時候,LMK 會根據 OOM_adj 開始殺進程,從 後臺、桌面、服務、前臺,直到手機重啓。並且,如果頻繁申請釋放 Java Bitmap 也很容易導致內存抖動。對於這種種問題,我們該 如何評估內存對應用性能的影響 呢?

對此,我們可以主要從以下 兩個方面 進行評估,如下所示:

  • 1)、崩潰中異常退出和 OOM 的比例
  • 2)、低內存設備更容易出現內存不足和卡頓,需要查看應用中用戶的手機內存在 2GB 以下所佔的比例

對於具體的優化策略與手段,我們可以從以下 七個方面 來搭建一套 成體系化的圖片優化 / 監控機制

1、統一圖片庫

在項目中,我們需要 收攏圖片的調用,避免使用 Bitmap.createBitmap、BitmapFactory 相關的接口創建 Bitmap,而應該使用自己的圖片框架

2、設備分級優化策略

內存優化首先需要根據 設備環境 來綜合考慮,讓高端設備使用更多的內存,做到 針對設備性能的好壞使用不同的內存分配和回收策略

因此,我們可以使用類似 device-year-class 的策略對設備進行分級,對於低端機用戶可以關閉複雜的動畫或”重功能“,使用565格式的圖片或更小的緩存內存 等等。

業務開發人員需要 考慮功能是否對低端機開啓,在系統資源不夠時主動去做降級處理

3、建立統一的緩存管理組件

建立統一的緩存管理組件,合理使用 OnTrimMemory / LowMemory 回調,根據系統不同的狀態去釋放相應的緩存與內存

在實現過程中,需要 解決使用 static LRUCache 來緩存大尺寸 Bitmap 的問題

並且,在通過實際的測試後,發現 onTrimMemory 的 ComponetnCallbacks2.TRIM_MEMORY_COMPLETE 並不等價於 onLowMemory,因此建議仍然要去監聽 onLowMemory 回調

4、低端機避免使用多進程

一個 空進程 也會佔用 10MB 內存,低端機應該儘可能減少使用多進程。

針對低端機用戶可以推出** 4MB 的輕量級版本**,如今日頭條極速版、Facebook Lite。

5、線下大圖片檢測

在開發過程中,如果檢測到不合規的圖片使用(如圖片寬度超過View的寬度甚至屏幕寬度),應該立刻提示圖片所在的Activity和堆棧,讓開發人員更快發現並解決問題。在灰度和線上環境,可以將異常信息上報到後臺,還可以計算超寬率(圖片超過屏幕大小所佔圖片總數的比例)

下面,我們介紹下如何實現對大圖片的檢測。

常規實現

繼承 ImageView,重寫實現計算圖片大小。但是侵入性強,並且不通用。

因此,這裏我們介紹一種更好的方案:ARTHook。

ARTHook優雅檢測大圖

ARTHook,即 掛鉤,用額外的代碼勾住原有的方法,以修改執行邏輯,主要可以用於以下四個方面:

  • 1)、AOP編程
  • 2)、運行時插樁
  • 3)、性能分析
  • 4)、安全審計

具體我們是使用 Epic 來進行 Hook,Epic 是 一個虛擬機層面,以 Java 方法爲粒度的運行時 Hook 框架。簡單來說,它就是 ART 上的 Dexposed,並且它目前 支持 Android 4.0~10.0

Epic github 地址

使用步驟

Epic通常的使用步驟爲如下三個步驟:

1、在項目 moudle 的 build.gradle 中添加

compile 'me.weishu:epic:0.6.0'

2、繼承 XC_MethodHook,實現 Hook 方法前後的邏輯。如 監控Java線程的創建和銷燬

class ThreadMethodHook extends XC_MethodHook{
    @Override
    protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
        super.beforeHookedMethod(param);
        Thread t = (Thread) param.thisObject;
        Log.i(TAG, "thread:" + t + ", started..");
    }

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        Thread t = (Thread) param.thisObject;
        Log.i(TAG, "thread:" + t + ", exit..");
    }
}

3、注入 Hook 好的方法:

DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());

知道了 Epic 的基本使用方法之後,我們便可以利用它來實現大圖片的監控報警了。

項目實戰

Awesome-WanAndroid 項目爲例,首先,在 WanAndroidApp 的 onCreate 方法中添加如下代碼:

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
        @Override
        protected void afterHookedMethod(MethodHookParam param) throws Throwable {
            super.afterHookedMethod(param);
        // 1
        DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
        }
    });

在註釋1處,我們 通過調用 DexposedBridge 的 findAndHookMethod 方法找到所有通過 ImageView 的 setImageBitmap 方法設置的切入點,其中最後一個參數 ImageHook 對象是繼承了 XC_MethodHook 類,其目的是爲了 重寫 afterHookedMethod 方法拿到相應的參數進行監控邏輯的判斷

接下來,我們來實現我們的 ImageHook 類,代碼如下所示:

public class ImageHook extends XC_MethodHook {

    @Override
    protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        // 1
        ImageView imageView = (ImageView) param.thisObject;
        checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
    }


    private static void checkBitmap(Object thiz, Drawable drawable) {
        if (drawable instanceof BitmapDrawable && thiz instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if (bitmap != null) {
                final View view = (View) thiz;
                int width = view.getWidth();
                int height = view.getHeight();
                if (width > 0 && height > 0) {
                    // 2、圖標寬高都大於view的2倍以上,則警告
                    if (bitmap.getWidth() >= (width << 1)
                        &&  bitmap.getHeight() >= (height << 1)) {
                    warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
                }
                } else {
                    // 3、當寬高度等於0時,說明ImageView還沒有進行繪製,使用ViewTreeObserver進行大圖檢測的處理。
                    final Throwable stackTrace = new RuntimeException();
                    view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                        @Override
                        public boolean onPreDraw() {
                            int w = view.getWidth();
                            int h = view.getHeight();
                            if (w > 0 && h > 0) {
                                if (bitmap.getWidth() >= (w << 1)
                                    && bitmap.getHeight() >= (h << 1)) {
                                    warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
                                }
                                view.getViewTreeObserver().removeOnPreDrawListener(this);
                            }
                            return true;
                        }
                    });
                }
            }
        }
    }

    private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
        String warnInfo = "Bitmap size too large: " +
            "\n real size: (" + bitmapWidth + ',' + bitmapHeight + ')' +
            "\n desired size: (" + viewWidth + ',' + viewHeight + ')' +
            "\n call stack trace: \n" + Log.getStackTraceString(t) + '\n';

        LogHelper.i(warnInfo);
    }
}

首先,在註釋1處,我們重寫了 ImageHook 的 afterHookedMethod 方法,拿到了當前的 ImageView 和要設置的 Bitmap 對象。然後,在註釋2處,如果當前 ImageView 的寬高大於0,我們便進行大圖檢測的處理:ImageView 的寬高都大於 View 的2倍以上,則警告。接着,在註釋3處,如果當前 ImageView 的寬高等於0,則說明 ImageView 還沒有進行繪製,則使用 ImageView 的 ViewTreeObserver 獲取其寬高進行大圖檢測的處理。至此,我們的大圖檢測檢測組件就已經實現了。

ARTHook方案實現小結
  • 1)、無侵入性
  • 2)、通用性強
  • 3)、兼容性問題大,開源方案不能帶到線上環境

6、線下重複圖片檢測

已完全配置好的項目請參見這裏

首先我們來了解一下這裏的 重複圖片 所指的概念:
Bitmap 像素數據完全一致,但是有多個不同的對象存在

重複圖片檢測的原理其實就是 使用內存 Hprof 分析工具,自動將重複 Bitmap 的圖片和引用堆棧輸出

實現步驟

具體的實現可以細分爲如下三個步驟:

  • 1)、首先,獲取 android.graphics.Bitmap 實例對象的 mBuffer 作爲 ArrayInstance ,通過 getValues 獲取的數據爲 Object 類型。由於後面計算 md5 需要爲 byte[] 類型,所以通過反射的方式調用 ArrayInstance#asRawByteArray 直接返回 byte[] 數據
  • 2)、然後,根據 mBuffer 的數據生成 png 圖片文件,這裏直接參考了 https://github.com/JetBrains/adt-tools-base/blob/master/ddmlib/src/main/java/com/android/ddmlib/BitmapDecoder.java 的實現方式。
  • 3)、最後,獲取堆棧信息,直接 使用LeakCanary 獲取 stack 的方法,使用 leakcanary-analyzer-1.6.2.jar 和 leakcanary-watcher-1.6.2.jar 這兩個庫文件。並用 反射 的方式調用了 HeapAnalyzer#findLeakTrace 方法。

其中,獲取堆棧 的信息也可以直接使用 haha 庫來進行獲取。這裏簡單說一下 使用 haha 庫獲取堆棧的流程,其具體可以細分爲八個步驟,如下所示:

  • 1)、首先,預備一個已經存在重複 bitmap 的 hprof 文件
  • 2)、利用 haha 庫上的 MemoryMappedFileBuffer 讀取 hrpof 文件 [關鍵代碼 new MemoryMappedFileBuffer(heapDumpFile) ]
  • 3)、解析生成 snapshot,獲取 heap,這裏我只獲取了 app heap [關鍵代碼 snapshot.getHeaps(); heap.getName().equals(“app”) ]
  • 4)、從 snapshot 中根據指定 class 查找出所有的 Bitmap Classes [關鍵代碼snapshot.findClasses(Bitmap.class.getName()) ]
  • 5)、從 heap 中獲得所有的 Bitmap 實例 instance [關鍵代碼 clazz.getHeapInstances(heap.getId()) ]
  • 6)、根據 instance 中獲取所有的屬性信息 Field[],並從 Field[] 查找出我們需要的 “mWidth” “mHeight” “mBuffer” 信息
  • 7)、通過 “mBuffer” 屬性即可獲取到他們的 hashcode 來判斷是否是重複圖片
  • 8)、最後,通過 instance 中 mNextInstanceToGcRoot 獲取整個引用鏈信息並打印

在實現圖片內存監控的過程中,應注意 兩個關鍵點,如下所示:

  • 1)、在線上可以按照 不同的系統、屏幕分辨率 等緯度去 分析圖片內存的佔用情況
  • 2)、在 OOM 崩潰時,可以將 圖片總內存、Top N 圖片佔用內存 寫入 崩潰日誌

7、建立全局的線上 Bitmap 監控

爲了建立全局的 Bitmap 監控,我們必須 對 Bitmap 的分配和回收 進行追蹤。我們先來看看 Bitmap 有哪些特點:

  • 1)、創建場景比較單一:在 Java 層調用 Bitmap.create 或 BitmapFactory 等方法創建,可以封裝一層對 Bitmap 創建的接口,注意要包含調用第三方庫產生的 Bitmap,這裏我們具體可以使用 編譯插樁 + ASM 的方式來高效地實現。
  • 2)、創建頻率比較低
  • 3)、和 Java 對象的生命週期一樣服從 GC,可以使用 WeakReference 來追蹤 Bitmap 的銷燬

根據以上特點,我們可以建立一套 Bitmap 的高性價比監控組件

  • 1)、首先,在接口層將所有創建出來的 Bitmap 放入一個 WeakHashMap 中,並記錄創建 Bitmap 的數據、堆棧等信息。
  • 2)、然後,每隔一定時間查看 WeakHashMap 中有哪些 Bitmap 仍然存活來判斷是否出現 Bitmap 濫用或泄漏。
  • 3)、最後,如果發生了 Bitmap 濫用或泄露,則將相關的數據與堆棧等信息打印出來或上報至 APM 後臺。

這個方案的 性能消耗很低,可以在 正式環境 中進行。但是,需要注意的一點是,正式與測試環境需要採用不同程度的監控。

3、建立線上應用內存監控體系

要建立線上應用的內存監控體系,我們需要 先獲取 App 的 DalvikHeap 與 NativeHeap,它們的獲取方式可歸結爲如下四個步驟:

  • 1、首先,通過 ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 獲取內存信息數據
  • 2、然後,通過 hook Debug.MemoryInfo 的 getMemoryStat 方法(os v23 及以上)可以獲得 Memory Profiler 中的多項數據,進而獲得 細分內存的使用情況
  • 3、接着,通過 Runtime 獲取 DalvikHeap
  • 4、最後,通過 Debug.getNativeHeapAllocatedSize 獲取 NativeHeap

對於監控場景,我們需要將其劃分爲兩大類,如下所示:

  • 1)、常規內存監控
  • 2)、低內存監控

1、常規內存監控

根據 斐波那契數列 每隔一段時間(max:30min)獲取內存的使用情況。常規內存的監控方法有多種實現方式,下面,我們按照 項目早期 => 壯大期 => 成熟期 的常規內存監控方式進行 演進式 講解。

項目早期:針對場景進行線上 Dump 內存的方式

具體使用 Debug.dumpHprofData() 實現。

其實現的流程爲如下四個步驟:

  • 1)、超過最大內存的 80%
  • 2)、內存 Dump
  • 3)、回傳文件至服務器
  • 4)、MAT 手動分析

但是,這種方式有如下幾個缺點:

  • 1)、Dump文件太大,和對象數正相關,可以進行裁剪
  • 2)、上傳失敗率高,分析困難

壯大期:LeakCanary帶到線上的方式

在使用 LeakCanary 的時候我們需要 預設泄漏懷疑點,一旦發現泄漏進行回傳。但這種實現方式缺點比較明顯,如下所示:

  • 1)、不適合所有情況,需要預設懷疑點
  • 2)、分析比較耗時,容易導致 OOM

成熟期:定製 LeakCanary 方式

那麼,如何定製線上的LeakCanary?

定製 LeakCanary 其實就是對 haha組件 來進行 定製。haha庫是 square 出品的一款 自動分析Android堆棧的java庫。這是haha庫的 鏈接地址

對於haha庫,它的 基本用法 一般遵循爲如下四個步驟:

1、導出堆棧文件

File heapDumpFile = ...
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());

2、根據堆棧文件創建出內存映射文件緩衝區

DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);

3、根據文件緩存區創建出對應的快照

Snapshot snapshot = Snapshot.createSnapshot(buffer);

4、從快照中獲取指定的類

ClassObj someClass = snapshot.findClass("com.example.SomeClass");

我們在實現線上版的LeakCanary的時候主要要解決的問題有三個,如下所示:

  • 1)、解決 預設懷疑點 時不準確的問題 => 自動找懷疑點
  • 2)、解決掉將 hprof 文件映射到內存中的時候可能導致內存暴漲甚至發生 OOM 的問題 => 對象裁剪,不全部加載到內存。即對生成的 Hprof 內存快照文件做一些優化:裁剪大部分圖片對應的 byte 數據 以減少文件開銷,最後,使用 7zip 壓縮,一般可 節省 90% 大小
  • 3)、分析泄漏鏈路慢而導致分析時間過長 => 分析 Retain size 大的對象

成熟期:實現內存泄漏監控閉環

在實現了線上版的 LeakCanary 之後,就需要 將線上版的 LeakCanary 與服務器和前端頁面結合 起來。具體的 內存泄漏監控閉環流程 如下所示:

  • 1)、當在線上版 LeakCanary 上發現內存泄漏時,手機將上傳內存快照至服務器
  • 2)、此時服務器分析 Hprof,如果不是系統原因導致誤報則通過 git 得到該最近修改人
  • 3)、最後將內存泄漏 bug 單提交給負責人。該負責人通過前端實現的 bug 單系統即可看到自己新增的bug

2、低內存監控

對於低內存的監控,通常有兩種方式,分別如下所示:

  • 1、利用 onTrimMemory / onLowMemory 監聽系統回調的物理內存警告
  • 2、在後臺起一個服務定時監控系統的內存佔用,只要超過虛擬內存大小最大限制的 90% 則直接觸發內存警告

3、內存監控指標

爲了準確衡量內存性能,我們需要引入一系列的內存監控指標,如下所示:

1)、發生頻率

2)、發生時各項內存使用狀況

3)、發生時App的當前場景

4)、內存異常率

內存 UV 異常率 = PSS 超過 400MB 的 UV / 採集UV
PSS 獲取:調用 Debug.MemoryInfo 的 API 即可

如果出現 新的內存使用不當或內存泄漏 的場景,這個指標會有所 上漲

5)、觸頂率

內存 UV 觸頂率 = Java 堆佔用超過最大堆限制的 85% 的 UV / 採集UV

計算觸頂率的代碼如下所示:

long javaMax = Runtime.maxMemory();
long javaTotal = Runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
float proportion = (float) javaUsed / javaMax;

如果超過 85% 最大堆 的限制,GC 會變得更加 頻發,容易造成 OOM 和 卡頓

4、小結

在具體實現的時候,客戶端 儘量只負責 上報數據,而 指標值的計算 可以由 後臺 來計算。這樣便可以通過 版本對比監控是否有 新增內存問題。因此,建立線上內存監控的完整方案 至少需要包含以下四點

  • 1)、待機內存、重點模塊內存、OOM率
  • 2)、整體及重點模塊 GC 次數、GC 時間
  • 3)、增強的 LeakCanry 自動化內存泄漏分析
  • 4)、低內存監控模塊的設置

4、建立全局的線程監控組件

每個線程初始化都需要 mmap 一定的棧大小,在默認情況下初始化一個線程需要 mmap 1MB 左右的內存空間

32bit 的應用中有 4g 的 vmsize實際能使用的有 3g+,這樣一個進程 最大能創建的線程數 可以達到 3000個,但是,linux 對每個進程可創建的線程數也有一定的限制(/proc/pid/limits),並且,不同廠商也能修改這個限制,超過該限制就會 OOM。

因此,對線程數量的限制,在一定程度上可以 有效地避免 OOM 的發生。那麼,實現一套 全局的線程監控組件 便是 刻不容緩 的了。

全局線程監控組件的實現原理

在線下或灰度的環境下通過一個定時器每隔 10分鐘 dump 出應用所有的線程相關信息,當線程數超過當前閾值時,則將當前的線程信息上報並預警

5、GC 監控組件搭建

通過** Debug.startAllocCounting** 來監控 GC 情況,注意有一定 性能影響

Android 6.0 之前 可以拿到 內存分配次數和大小以及 GC 次數,其對應的代碼如下所示:

long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();

並且,在 Android 6.0 及之後 可以拿到 更精準GC 信息:

Debug.getRuntimeStat("art.gc.gc-count");
Debug.getRuntimeStat("art.gc.gc-time");
Debug.getRuntimeStat("art.gc.blocking-gc-count");
Debug.getRuntimeStat("art.gc.blocking-gc-time");

對於 GC 信息的排查,我們一般關注 阻塞式GC的次數和耗時,因爲它會 暫停線程,可能導致應用發生 卡頓。建議 僅對重度場景使用

6、建立線上 OOM 監控組件:Probe

美團的 Android 內存泄漏自動化鏈路分析組件 ProbeOOM 時會生成 Hprof 內存快照,然後,它會通過 單獨進程 對這個 文件 做進一步 分析

Probe 組件的缺陷及解決方案

它的缺點比較多,具體爲如下幾點:

  • 1、在崩潰的時候生成內存快照容易導致二次崩潰
  • 2、部分手機生成 Hprof 快照比較耗時
  • 3、部分 OOM 是由虛擬內存不足導致

在實現自動化鏈路分析組件 Probe 的過程中主要要解決兩個問題,如下所示:

1、鏈路分析時間過長

  • 1)、使用鏈路歸併:將具有 相同層級與結構 的鏈路進行 合併
  • 2)、使用 自適應擴容法通過不斷比較現有鏈路和新鏈路,結合擴容因子,逐漸完善爲完整的泄漏鏈路

2、分析進程佔用內存過大

分析進程佔用的內存內存快照文件的大小 不成正相關,而跟 內存快照文件的 Instance 數量正相關。所以在開發過程中我們應該 儘可能排除不需要的Instance實例

Prope 分析流程揭祕

Prope 的 總體架構圖 如下所示:

image

而它的整個分析流程具體可以細分爲八個步驟,如下所示:

1、hprof 映射到內存 => 解析成 Snapshot & 計數壓縮:

解析後的 Snapshot 中的 Heap 有四種類型,具體爲:

  • 1)、DefaultHeap
  • 2)、ImageHeap
  • 3)、App Heap:包括 ClassInstance、ClassObj、ArrayInstance、RootObj
  • 4)、System Heap

解析完 後使用了 計數壓縮策略,對 相同的 Instance 使用 計數,以 減少佔用內存。超過計數閾值的需要計入計數桶(計數桶記錄了 丟棄個數 和 每個 Instance 的大小)

2、生成 Dominator Tree

3、計算 RetainSize

4、生成 Reference 鏈 && 基礎數據類型增強:

如果對象是 基礎數據類型,會將 自身的 RetainSize 累加到父節點 上,將 懷疑對象 替換爲它的 父節點

5、鏈路歸併

6、計數桶補償 & 基礎數據類型和父節點融合

使用計數補償策略計算 RetainSize,主要是 判斷對象是否在計數桶中,如果在的話則將 丟棄的個數和大小補償到對象上,累積計算RetainSize,最後對 RetainSize 排序以查找可疑對象

7、排序擴容

8、查找泄露鏈路

7、實現 單機版 的 Profile Memory 自動化內存分析

項目地址請點擊此處

在配置的時候要注意兩個問題:

  • 1、liballoc-lib.so在構建後工程的 build => intermediates => cmake 目錄下。將對應的 cpu abi 目錄拷貝到新建的 libs 目錄下

  • 2、在 DumpPrinter Java 庫的 build.gradle 中的 jar 閉包中需要加入以下代碼以識別源碼路徑:

    sourceSets.main.java.srcDirs = [‘src’]

使用步驟

具體的使用步驟如下所示:

1、首先,點擊 ”開始記錄“ 按鈕可以看到觸發對象分配的記錄,說明對象已經開始記錄對象的分配,log如下所示:

12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====

2、然後,點擊多次 ”生成1000個對象“ 按鈕,當對象達到設置的最大數量的時候觸發內存dump,會得到保存數據路徑的日誌。如下所示:

12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
12-26 10:56:45.103 30450-30450/com.dodola.alloctrack I/AllocTracker: saveARTAllocationData write file to /storage/emulated/0/crashDump/1577329005

3、此時,可以看到數據保存在 sdk 下的 crashDump 目錄下。

4、接着,通過 gradle task :buildAlloctracker 任務編譯出存放在 tools/DumpPrinter-1.0.jar 的 dump 工具,然後採用如下命令來將數據解析 到dump_log.txt 文件中。

java -jar tools/DumpPrinter-1.0.jar dump文件路徑 > dump_log.txt

5、最後,就可以在 dump_log.txt 文件中看到解析出來的數據,如下所示:

Found 4949 records:
tid=1 byte[] (94208 bytes)
    dalvik.system.VMRuntime.newNonMovableArray (Native method)
    android.graphics.Bitmap.nativeCreate (Native method)
    android.graphics.Bitmap.createBitmap (Bitmap.java:975)
    android.graphics.Bitmap.createBitmap (Bitmap.java:946)
    android.graphics.Bitmap.createBitmap (Bitmap.java:913)
    android.graphics.drawable.RippleDrawable.updateMaskShaderIfNeeded (RippleDrawable.java:776)
    android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:860)
    android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:700)
    android.view.View.getDrawableRenderNode (View.java:17736)
    android.view.View.drawBackground (View.java:17660)
    android.view.View.draw (View.java:17467)
    android.view.View.updateDisplayListIfDirty (View.java:16469)
    android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
    android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:3885)
    android.view.View.updateDisplayListIfDirty (View.java:16429)
    android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)

8、搭建線下 Native 內存泄漏監控體系

Android 8.0 及之後,可以使用 Address Sanitizer、Malloc 調試和 Malloc 鉤子 進行 native 內存分析,參見 native_memory

對於線下 Native 內存泄漏監控的建立,主要針對 是否能重編 so 的情況 來記錄分配的內存信息。

針對無法重編so的情況

  • 1)、首先,使用 PLT Hook 攔截庫的內存分配函數,然後,重定向到我們自己的實現後去 記錄分配的 內存地址、大小、來源so庫路徑 等信息。
  • 2)、最後,定期 掃描分配與釋放 的配對內存塊,對於 不配對的分配 輸出上述記錄的信息

針對可重編的so情況

  • 1)、首先,通過 GCC”-finstrument-functions“ 參數給 所有函數插樁,然後,在樁中模擬調用棧的入棧與出棧操作
  • 2)、接着,通過 ld”–warp“ 參數 攔截內存分配和釋放函數,重定向到我們自己的實現後記錄分配的 內存地址、大小、來源so以及插樁調用棧此刻的內容
  • 3)、最後,定期掃描分配與釋放是否配對,對於不配對的分配輸出我們記錄的信息

9、設置內存兜底策略

設置內存兜底策略的目的,是爲了 在用戶無感知的情況下,在接近觸發系統異常前,選擇合適的場景殺死進程並將其重啓,從而使得應用內存佔用回到正常情況

通常執行內存兜底策略時至少需要滿足六個條件,如下所示:

  • 1)、是否在主界面退到後臺且位於後臺時間超過 30min
  • 2)、當前時間爲早上 2~5 點
  • 3)、不存在前臺服務(通知欄、音樂播放欄等情況)
  • 4)、Java heap 必須大於當前進程最大可分配的85% || native內存大於800MB
  • 5)、vmsize 超過了4G(32bit)的85%
  • 6)、非大量的流量消耗(不超過1M/min) && 進程無大量CPU調度情況

只有在滿足了以上條件之後,我們纔會去殺死當前主進程並通過 push 進程重新拉起及初始化

10、更深入的內存優化策略

除了在 Android性能優化之內存優化 => 優化內存空間 中講解過的一些常規的內存優化策略以外,在下面列舉了一些更深入的內存優化策略。

1、使 bitmap 資源在 native 中分配

對於 Android 2.x 系統,使用反射將 BitmapFactory.Options 裏面隱藏的 inNativeAlloc 打開

對於 Android 4.x 系統,使用或借鑑 Fresco 將 bitmap 資源在 native 中分配的方式

2、圖片加載時的降級處理

使用 Glide、Fresco 等圖片加載庫,通過定製,在加載 bitmap 時,若發生 OOM,則使用 try catch 將其捕獲,然後清除圖片 cache,嘗試降低 bitmap format(ARGB8888、RGB565、ARGB4444、ALPHA8)。

需要注意的是,OOM 是可以捕獲的,只要 OOM 是由 try 語句中的對象聲明所導致的,那麼在 catch 語句中,是可以釋放掉這些對象,解決 OOM 的問題的。

3、前臺每隔 3 分鐘去獲取當前應用內存佔最大內存的比例,超過設定的危險閾值(如80%)則主動釋放應用 cache(Bitmap 爲大頭),並且顯示地除去應用的 memory,以加速內存收集的過程。

計算當前應用內存佔最大內存的比例的代碼如下:

max = Runtime.getRuntime().maxMemory();
available = Runtime.getRuntime.totalMemory() - Runtime.getFreeMemory();
ratio = available / max;

顯示地除去應用的 memory,以加速內存收集過程的代碼如下所示:

WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);

5、由於 webview 存在內存系統泄漏,還有 圖庫佔用內存過多 的問題,可以採用單獨的進程。

6、當UI隱藏時釋放內存

當用戶切換到其它應用並且你的應用 UI 不再可見時,應該釋放應用 UI 所佔用的所有內存資源。這能夠顯著增加系統緩存進程的能力,能夠提升用戶體驗。

在所有 UI 組件都隱藏的時候會接收到 Activity 的 onTrimMemory() 回調並帶有參數 TRIM_MEMORY_UI_HIDDEN

7、Activity 的兜底內存回收策略

在 Activity 的 onDestory 中遞歸釋放其引用到的 Bitmap、DrawingCache 等資源,以降低發生內存泄漏時對應用內存的壓力。

8、使用類似 Hack 的方式修復系統內存泄漏

LeakCanary 的 AndroidExcludeRefs 列出了一些由於系統原因導致引用無法釋放的例子,可使用類似 Hack 的方式去修復。具體的實現代碼可以參考 Booster => 系統問題修復

9、應用發生 OOM 時,需要上傳更加詳細的內存相關信息。

10、當應用使用的Service不再使用時應該銷燬它,建議使用 IntentServcie。

11、謹慎使用第三方庫,避免爲了使用其中一兩個功能而導入一個大而全的解決方案。

六、內存優化演進

1、自動化測試階段

內存達到閾值後自動觸發 Hprof Dump,將得到的 Hprof 存檔後由人工通過 MAT 進行分析。

2、LeakCanary

檢測和分析報告都在一起,批量自動化測試和事後分析都不太方便。

3、使用基於 LeakCannary 的改進版 ResourceCanary

Matrix => ResourceCanary 實現原理

主要功能

目前,它的主要功能有 三個部分,如下所示:

1、分離 檢測和分析 兩部分流程

自動化測試由測試平臺進行,分析則由監控平臺的服務端離線完成,最後再通知相關開發解決問題。

2、裁剪 Hprof文件,以降低 傳輸 Hprof 文件與後臺存儲 Hprof 文件的開銷

獲取 需要的類和對象相關的字符串 信息即可,其它數據都可以在客戶端裁剪,一般能 Hprof 大小會減小至原來的 1/10 左右。

3、增加重複 Bitmap 對象檢測

方便通過減少冗餘 Bitmap 的數量,以降低內存消耗。

4、小結

在研發階段需要不斷實現 更多的工具和組件,以此係統化地提升自動化程度,以最終 提升發現問題的效率

七、內存優化工具

除了常用的內存分析工具 Memory Profiler、MAT、LeakCanary 之外,還有一些其它的內存分析工具,下面我將一一爲大家進行介紹。

1、top

top 命令是 Linux 下常用的性能分析工具,能夠 實時顯示系統中各個進程的資源佔用狀況,類似於 Windows 的任務管理器。top 命令提供了 實時的對系統處理器的狀態監視。它將 顯示系統中 CPU 最“敏感”的任務列表。該命令可以按 CPU使用、內存使用和執行時間 對任務進行排序

接下來,我們輸入以下命令查看top命令的用法:

quchao@quchaodeMacBook-Pro ~ % adb shell top --help
usage: top [-Hbq] [-k FIELD,] [-o FIELD,] [-s SORT] [-n NUMBER] [-d SECONDS] [-p PID,] [-u USER,]

Show process activity in real time.

-H	Show threads
-k	Fallback sort FIELDS (default -S,-%CPU,-ETIME,-PID)
-o	Show FIELDS (def PID,USER,PR,NI,VIRT,RES,SHR,S,%CPU,%MEM,TIME+,CMDLINE)
-O	Add FIELDS (replacing PR,NI,VIRT,RES,SHR,S from default)
-s	Sort by field number (1-X, default 9)
-b	Batch mode (no tty)
-d	Delay SECONDS between each cycle (default 3)
-n	Exit after NUMBER iterations
-p	Show these PIDs
-u	Show these USERs
-q	Quiet (no header lines)

Cursor LEFT/RIGHT to change sort, UP/DOWN move list, space to force
update, R to reverse sort, Q to exit.

這裏使用 top 僅顯示一次進程信息,以便來講解進程信息中各字段的含義。

image

整體的統計信息區

前四行 是當前系統情況 整體的統計信息區。下面我們看每一行信息的具體意義。

第一行:Tasks — 任務(進程)

具體信息說明如下所示:

系統現在共有 729 個進程,其中處於 運行中 的有 1 個,715 個在 休眠(sleep)stoped 狀態的有0個,zombie 狀態(殭屍)的有 8 個。

第二行:內存狀態

具體信息如下所示:

  • 1)、5847124k total:物理內存總量(5.8GB)
  • 2)、5758016k used:使用中的內存總量(5.7GB)
  • 3)、89108k free:空閒內存總量(89MB)
  • 4)、112428k buffers:緩存的內存量 (112M)

第三行:swap交換分區信息

具體信息說明如下所示:

  • 1)、2621436k total:交換區總量(2.6GB)
  • 2)、612572k used:使用的交換區總量(612MB)
  • 3)、2008864k free:空閒交換區總量(2GB)
  • 4)、2657696k cached:緩衝的交換區總量(2.6GB)

第四行:cpu狀態信息

具體屬性說明如下所示:

  • 1)、800% cpu:8核 CPU
  • 2)、39% user:39% CPU被用戶進程使用
  • 3)、0% nice:優先值爲負的進程佔 0%
  • 4)、42% sys — 內核空間佔用 CPU 的百分比爲 42%
  • 5)、712% idle:除 IO 等待時間以外的其它等待時間爲 712%
  • 6)、0% iow:IO 等待時間佔 0%
  • 7)、0% irq:硬中斷時間佔 0%
  • 8)、6% sirq - 軟中斷時間佔 0%

對於內存監控,在 top 裏我們要時刻監控 第三行 swap 交換分區的 used,如果這個數值在不斷的變化,說明內核在不斷進行內存和 swap 的數據交換,這是真正的內存不夠用了。

進程(任務)的狀態監控

第五行及以下,就是各進程(任務)的狀態監控,項目列信息說明如下所示:

  • 1)、PID:進程 id
  • 2)、USER:進程所有者
  • 3)、PR:進程優先級
  • 4)、NI:nice 值。負值表示高優先級,正值表示低優先級
  • 5)、VIRT:進程使用的虛擬內存總量。VIRT = SWAP + RES
  • 6)、RES:進程使用的、未被換出的物理內存大小。RES = CODE + DATA
  • 7)、SHR:共享內存大小
  • 8)、S:進程狀態。D = 不可中斷的睡眠狀態、R = 運行、 S = 睡眠、T = 跟蹤 / 停止、Z = 殭屍進程
  • 9)、%CPU — 上次更新到現在的 CPU 時間佔用百分比
  • 10)、%MEM:進程使用的物理內存百分比
  • 11)、TIME+:進程使用的 CPU 時間總計,單位 1/100秒
  • 12)、ARGS:進程名稱(命令名 / 命令行)

從上圖中可以看到,第一行的就是 Awesome-WanAndroid 這個應用的進程,它的進程名稱爲 json.chao.com.w+,PID 爲 23104,進程所有者 USER 爲 u0_a714,進程優先級 PR 爲 10,nice 置 NI 爲 -10。進程使用的虛擬內存總量 VIRT 爲 4.3GB,進程使用的、未被換出的物理內存大小 RES 爲138M,共享內存大小 SHR 爲 66M,進程狀態 S 是睡眠狀態,上次更新到現在的 CPU 時間佔用百分比 %CPU 爲 21.2。進程使用的物理內存百分比 %MEM 爲 2.4%,進程使用的 CPU 時間 TIME+ 爲 1:47.58 / 100小時。

2、dumpsys meminfo

四大內存指標

在講解 dumpsys meminfo 命令之前,我們必須先了解下 Android 中最重要的 四大內存指標 的概念,如下表所示:

內存指標 英文全稱 含義 等價
USS Unique Set Size 物理內存 進程獨佔的內存
PSS Proportional Set Size 物理內存 PSS = USS + 按比例包含共享庫
RSS Resident Set Size 物理內存 RSS= USS+ 包含共享庫
VSS Virtual Set Size 虛擬內存 VSS= RSS+ 未分配實際物理內存

從上可知,它們之間內存的大小關係爲 VSS >= RSS >= PSS >= USS

RSS 與 PSS 相似,也包含進程共享內存,但比較麻煩的是 RSS 並沒有把共享內存大小全都平分到使用共享的進程頭上,以至於所有進程的 RSS 相加會超過物理內存很多。而 VSS 是虛擬地址,它的上限與進程的可訪問地址空間有關,和當前進程的內存使用關係並不大。比如有很多的 map 內存也被算在其中,我們都知道,file 的 map 內存對應的可能是一個文件或硬盤,或者某個奇怪的設備,它與進程使用內存並沒有多少關係。

PSS、USS 最大的不同在於 “共享內存“(比如兩個 App 使用 MMAP 方式打開同一個文件,那麼打開文件而使用的這部分內存就是共享的),USS不包含進程間共享的內存,而PSS包含。這也造成了USS因爲缺少共享內存,所有進程的USS相加要小於物理內存大小的原因。

最早的時候官方就推薦使用 PSS 曲線圖來衡量 App 的物理內存佔用,而 Android 4.4 之後才加入 USS。但是 PSS,有個很大的問題,就是 ”共享內存“,考慮一種情況,如果 A 進程與 B 進程都會使用一個共享 SO 庫,那麼 So 庫中初始化所用掉的那部分內存就會平分到 A 與 B 的頭上。但是 A 是在 B 之後啓動的,那麼對於 B 的 PSS 曲線而言,在 A 啓動的那一刻,即使 B 沒有做任何事情,也會出現一個比較大的階梯狀下滑,這會給用曲線圖分析軟件內存的行爲造成致命的麻煩

USS 雖然沒有這個問題,但是由於 Dalvik 虛擬機申請內存牽扯到 GC 時延和多種 GC 策略,這些都會影響到曲線的異常波動。例如異步 GC 是 Android 4.0 以上系統很重要的特性,但是 GC 什麼時候結束?曲線什麼時候”降低“?就 無法預計 了。還有 GC 策略,什麼時候開始增加 Dalvik 虛擬機的預申請內存大小(Dalvik 啓動時是有一個標稱的 start 內存大小,它是爲 Java 代碼運行時預留的,避免 Java 運行時再申請而造成卡頓),但是這個 預申請大小是動態變化的,這一點也會 造成 USS 忽大忽小

dumpsys meminfo 命令解析

瞭解完 Android 內存的性能指標之後,下面我們便來說說 dumpsys meminfo 這個命令的用法,首先我們輸入 adb shell dumpsys meminfo -h 查看它的幫助文檔:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo -h
meminfo dump options: [-a] [-d] [-c] [-s] [--oom] [process]
-a: include all available information for each process.
-d: include dalvik details.
-c: dump in a compact machine-parseable representation.
-s: dump only summary of application memory usage.
-S: dump also SwapPss.
--oom: only show processes organized by oom adj.
--local: only collect details locally, don't call process.
--package: interpret process arg as package, dumping all
            processes that have loaded that package.
--checkin: dump data for a checkin
If [process] is specified it can be the name or
pid of a specific process to dump.

接着,我們之間輸入adb shell dumpsys meminfo命令:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo
Applications Memory Usage (in Kilobytes):
Uptime: 257501238 Realtime: 257501238

// 根據進程PSS佔用值從大到小排序
Total PSS by process:
    308,049K: com.tencent.mm (pid 3760 / activities)
    225,081K: system (pid 2088)
    189,038K: com.android.systemui (pid 2297 / activities)
    188,877K: com.miui.home (pid 2672 / activities)
    176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
    175,231K: json.chao.com.wanandroid (pid 23104 / activities)
    126,918K: com.tencent.mobileqq (pid 23741)
    ...

// 以oom來劃分,會詳細列舉所有的類別的進程
Total PSS by OOM adjustment:
    432,013K: Native
        76,700K: surfaceflinger (pid 784)
        59,084K: [email protected] (pid 743)
        26,524K: transport (pid 23418)
        25,249K: logd (pid 597)
        11,413K: media.codec (pid 1303)
        10,648K: rild (pid 1304)
        9,283K: media.extractor (pid 1297)
        ...
        
    661,294K: Persistent
        225,081K: system (pid 2088)
        189,038K: com.android.systemui (pid 2297 / activities)
        103,050K: com.xiaomi.finddevice (pid 3134)
        39,098K: com.android.phone (pid 2656)
        25,583K: com.miui.daemon (pid 3078)
        ...
        
    219,795K: Foreground
        175,231K: json.chao.com.wanandroid (pid 23104 / activities)
        44,564K: com.miui.securitycenter.remote (pid 2986)
        
    246,529K: Visible
        71,002K: com.sohu.inputmethod.sogou.xiaomi (pid 4820)
        52,305K: com.miui.miwallpaper (pid 2579)
        40,982K: com.miui.powerkeeper (pid 3218)
        24,604K: com.miui.systemAdSolution (pid 7986)
        14,198K: com.xiaomi.metoknlp (pid 3506)
        13,820K: com.miui.voiceassist:core (pid 8722)
        13,222K: com.miui.analytics (pid 8037)
        7,046K: com.miui.hybrid:entrance (pid 7922)
        5,104K: com.miui.wmsvc (pid 7887)
        4,246K: com.android.smspush (pid 8126)
        
    213,027K: Perceptible
        89,780K: com.eg.android.AlipayGphone (pid 8238)
        49,033K: com.eg.android.AlipayGphone:push (pid 8204)
        23,181K: com.android.thememanager (pid 11057)
        13,253K: com.xiaomi.joyose (pid 5558)
        10,292K: com.android.updater (pid 3488)
        9,807K: com.lbe.security.miui (pid 23060)
        9,734K: com.google.android.webview:sandboxed_process0 (pid 11150)
        7,947K: com.xiaomi.location.fused (pid 3524)
        
    308,049K: Backup
        308,049K: com.tencent.mm (pid 3760 / activities)
        
    74,250K: A Services
        59,701K: com.tencent.mm:push (pid 7234)
        9,247K: com.android.settings:remote (pid 27053)
        5,302K: com.xiaomi.drivemode (pid 27009)
        
    199,638K: Home
        188,877K: com.miui.home (pid 2672 / activities)
        10,761K: com.miui.hybrid (pid 7945)
        
    53,934K: B Services
        35,583K: com.tencent.mobileqq:MSF (pid 14119)
        6,753K: com.qualcomm.qti.autoregistration (pid 8786)
        4,086K: com.qualcomm.qti.callenhancement (pid 26958)
        3,809K: com.qualcomm.qti.StatsPollManager (pid 26993)
        3,703K: com.qualcomm.qti.smcinvokepkgmgr (pid 26976)
        
    692,588K: Cached
        176,665K: com.plan.kot32.tomatotime (pid 22744 / activities)
        126,918K: com.tencent.mobileqq (pid 23741)
        72,928K: com.tencent.mm:tools (pid 18598)
        68,208K: com.tencent.mm:sandbox (pid 27333)
        55,270K: com.tencent.mm:toolsmp (pid 18842)
        24,477K: com.android.mms (pid 27192)
        23,865K: com.xiaomi.market (pid 27825)
        ...

// 按內存的類別來進行劃分
Total PSS by category:
    957,931K: Native
    284,006K: Dalvik
    199,750K: Unknown
    193,236K: .dex mmap
    191,521K: .art mmap
    110,581K: .oat mmap
    101,472K: .so mmap
    94,984K: EGL mtrack
    87,321K: Dalvik Other
    84,924K: Gfx dev
    77,300K: GL mtrack
    64,963K: .apk mmap
    17,112K: Other mmap
    12,935K: Ashmem
     3,364K: Stack
     2,343K: .ttf mmap
     1,375K: Other dev
     1,071K: .jar mmap
        20K: Cursor
         0K: Other mtrack

// 手機整體內存使用情況
Total RAM: 5,847,124K (status normal)
Free RAM: 3,711,324K (  692,588K cached pss + 2,428,616K cached kernel +   117,492K cached ion +   472,628K free)
Used RAM: 2,864,761K (2,408,529K used pss +   456,232K kernel)
Lost RAM:   184,330K
    ZRAM:   174,628K physical used for   625,388K in swap (2,621,436K total swap)
Tuning: 256 (large 512), oom   322,560K, restore limit   107,520K (high-end-gfx)

根據 dumpsys meminfo 的輸出結果,可歸結爲如下表格:

劃分類型 排序指標 含義
process PSS 以進程的PSS從大到小依次排序顯示,每行顯示一個進程,一般用來做初步的競品分析
OOM adj PSS 展示當前系統內部運行的所有Android進程的內存狀態和被殺順序,越靠近下方的進程越容易被殺,排序按照一套複雜的算法,算法涵蓋了前後臺、服務或節目、可見與否、老化等
category PSS 以Dalvik/Native/.art mmap/.dex map等劃分並按降序列出各類進程的總PSS分佈情況
total - 總內存、剩餘內存、可用內存、其他內存

此外,爲了 查看單個 App 進程的內存信息,我們可以輸入如下命令:

dumpsys meminfo <pid> // 輸出指定pid的某一進程
dumpsys meminfo --package <packagename> // 輸出指定包名的進程,可能包含多個進程

這裏我們輸入 adb shell dumpsys meminfo 23104 這條命令,其中 23104 爲 Awesome-WanAndroid App 的 pid,結果如下所示:

quchao@quchaodeMacBook-Pro ~ % adb shell dumpsys meminfo 23104
Applications Memory Usage (in Kilobytes):
Uptime: 258375231 Realtime: 258375231

** MEMINFO in pid 23104 [json.chao.com.wanandroid] **
                Pss  Private  Private  SwapPss     Heap     Heap     Heap
                Total    Dirty    Clean    Dirty     Size    Alloc     Free
                ------   ------   ------   ------   ------   ------   ------
Native Heap    46674    46620        0      164    80384    60559    19824
Dalvik Heap     6949     6912       16       23    12064     6032     6032
Dalvik Other     7672     7672        0        0
       Stack      108      108        0        0
      Ashmem      134      132        0        0
     Gfx dev    16036    16036        0        0
   Other dev       12        0       12        0
   .so mmap     3360      228     1084       27
  .jar mmap        8        8        0        0
  .apk mmap    28279    11328    11584        0
  .ttf mmap      295        0       80        0
  .dex mmap     7780       20     4908        0
  .oat mmap      660        0       92        0
  .art mmap     8509     8028      104       69
 Other mmap      982        8      848        0
 EGL mtrack    29388    29388        0        0
  GL mtrack    14864    14864        0        0
    Unknown     2532     2500        8       20
      TOTAL   174545   143852    18736      303    92448    66591    25856

App Summary
                   Pss(KB)
                    ------
       Java Heap:    15044
     Native Heap:    46620
            Code:    29332
           Stack:      108
        Graphics:    60288
   Private Other:    11196
          System:    11957

           TOTAL:   174545       TOTAL SWAP PSS:      303

Objects
           Views:      171         ViewRootImpl:        1
     AppContexts:        3           Activities:        1
          Assets:       18        AssetManagers:        6
   Local Binders:       32        Proxy Binders:       27
   Parcel memory:       11         Parcel count:       45
Death Recipients:        1      OpenSSL Sockets:        0
        WebViews:        0

SQL
        MEMORY_USED:      371
 PAGECACHE_OVERFLOW:       72          MALLOC_SIZE:      117

DATABASES
    pgsz     dbsz   Lookaside(b)          cache  Dbname
        4       60            109      151/32/18  /data/user/0/json.chao.com.wanandroid/databases/bugly_db_
        4       20             19         0/15/1  /data/user/0/json.chao.com.wanandroid/databases/aws_wan_android.db

該命令輸出了 進程的內存概要,我們應該着重關注 四個要點,下面我將一一進行講解。

1、查看 Native Heap 的 Heap Alloc 與 Dalvik Heap 的 Heap Alloc

  • 1)、Heap Alloc:表示 native 的內存佔用,如果持續上升,則可能有泄漏
  • 2)、Heap Alloc:表示 Java 層的內存佔用

2、查看 Views、Activities、AppContexts 數量變化情況

如果 Views 與 Activities、AppContexts 持續上升,則表明有內存泄漏的風險

3、SQL 的 MEMORY_USED 與 PAGECACHE_OVERFLOW

  • 1)、MEMOERY_USED:表示數據庫使用的內存
  • 2)、PAGECACHE_OVERFLOW:表示溢出也使用的緩存,這個數值越小越好

4、查看 DATABASES 信息

  • 1)、pgsz:表示數據庫分頁大小,這裏全是 4KB
  • 2)、Lookaside(b):表示使用了多少個 Lookaside 的 slots,可理解爲內存佔用的大小
  • 3)、cache:一欄中的 151/32/18 則分別表示 分頁緩存命中次數/未命中次數/分頁緩存個數,這裏的未命中次數不應該大於命中次數

3、LeakInspector

LeakInspector 是騰訊內部的使用的 一站式內存泄漏解決方案,它是 Android 手機經過長期積累和提煉、集內存泄漏檢測、自動修復系統Bug、自動回收已泄露Activity內資源、自動分析GC鏈、白名單過濾 等功能於一體,並 深度對接研發流程、自動分析責任人並提缺陷單的全鏈路體系

那麼,LeakInspector 與 LeakCanary 又有什麼不同之處呢?

它們之間主要有 四個方面 的不同,如下所示:

一、檢測能力與原理方面不同

1、檢測能力

它們都支持對 Activity、Fragment 及其它自定義類的泄漏檢測,但是,LeakInspector 還 增加了 Btiamp 的檢測能力,如下所示:

  • 1)、檢測有沒有在 View 上 decode 超過該 View 尺寸的圖片,若有則上報出現問題的 Activity 及與其對應的 View id,並記錄它的個數與平均佔用內存的大小。
  • 2)、檢測圖片尺寸是否超過所有手機屏幕大小,違規則報警。

這一個部分的實現原理,我們可以採用 ARTHook 的方式來實現,還不清楚的朋友請再仔細看看大圖檢測的部分。

2、檢測原理

兩個工具的泄漏檢測原理都是在 onDestroy 時檢查弱引用,不同之處在於 LeakInspector 直接使用 WeakReference 來檢測對象是否已經被釋放,而 LeakCanary 則使用 ReferenceQueue,兩者效果是一樣的。

並且針對 Activity,我們通常都會使用 Application的 registerActivityLifecycleCallbacks 來註冊 Activity 的生命週期,以重寫 onActivityDestroyed 方法實現。但是在 Android 4.0 以下,系統並沒有提供這個方法,爲了避免手動在每一個 Activity 的 onDestroy 中去添加這份代碼,我們可以使用 反射 Instrumentation 來截獲 onDestory,以降低接入成本。代碼如下所示:

Class<?> clazz = Class.forName("android.app.ActivityThread");
Method method = clazz.getDeclaredMethod("currentActivityThread", null);
method.setAccessible(true);
sCurrentActivityThread = method.invoke(null, null);
Field field = sCurrentActivityThread.getClass().getDeclaredField("mInstumentation");
field.setAccessible(true);
field.set(sCurrentActivityThread, new MonitorInstumentation());

二、泄漏現場處理方面不同

1、dump 採集

兩者都能採集 dump,但是 LeakInspector 提供了回調方法,我們可以增加更多的自定義信息,如運行時 Log、trace、dumpsys meminfo 等信息,以輔助分析定位問題。

2、白名單定義

這裏的白名單是爲了處理一些系統引起的泄漏問題,以及一些因爲 業務邏輯要開後門的情形而設置 的。分析時如果碰到白名單上標識的類,則不對這個泄漏做後續的處理。二者的配置差異有如下兩點:

  • 1)、LeakInspector 的白名單以 XML 配置的形式存放在服務器上。

    • 優點:跟產品甚至不同版本的應用綁定,我們可以很方便地修改相應的配置。
    • 缺點:白名單裏的類不區分系統版本一刀切。
  • 1)、而LeakCanary的白名單是直接寫死在其源碼的AndroidExcludedRefs類裏。

    • 優點:定義非常詳細,並區分系統版本。
    • 缺點:每次修改必定得重新編譯。
  • 2)、LeakCanary 的系統白名單裏定義的類比 LeakInspector 中定義的多很多,因爲它沒有自動修復系統泄漏功能。

3、自動修復系統泄漏

針對系統泄漏,LeakInspector 通過 反射自動修復 了目前碰到的一些系統泄漏,只要在 onDestory 裏面 調用 一個修復系統泄漏的方法即可。而 LeakCanary 雖然能識別系統泄漏,但是它僅僅對該類問題給出了分析,沒有提供實際可用的解決方案。

4、回收資源(Activity內存泄漏兜底處理)

如果檢測到發生了內存泄漏,LeakInspector 會對整個 Activity 的 View 進行遍歷,把圖片資源等一些佔內存的數據釋放掉,保證此次泄漏只會泄漏一個Activity的空殼,儘量減少對內存的影響。代碼大致如下所示:

if (View instanceof ImageView) {
    // ImageView ImageButton處理
    recycleImageView(app, (ImageView) view);
} else if (view instanceof TextView) {
    // 釋放TextView、Button周邊圖片資源
    recycleTextView((TextView) view);
} else if (View instanceof ProgressBar) {
    recycleProgressBar((ProgressBar) view);
} else {
    if (view instancof android.widget.ListView) {
        recycleListView((android.widget.ListView) view);
    } else if (view instanceof android.support.v7.widget.RecyclerView) {
        recycleRecyclerView((android.support.v7.widget.RecyclerView) view);
    } else if (view instanceof FrameLayout) {
        recycleFrameLayout((FrameLayout) view);
    } else if (view instanceof LinearLayout) {
        recycleLinearLayout((LinearLayout) view);
    }
    
    if (view instanceof ViewGroup) {
        recycleViewGroup(app, (ViewGroup) view);
    }
}

這裏以 recycleTextView 爲例,它回收資源的方式如下所示:

private static void recycleTextView(TextView tv) {
    Drawable[] ds = tv.getCompoundDrawables();
    for (Drawable d : ds) {
        if (d != null) {
            d.setCallback(null);
        }
    }
    tv.setCompoundDrawables(null, null, null, null);
    // 取消焦點,讓Editor$Blink這個Runnable不再被post,解決內存泄漏。
    tv.setCursorVisible(false);
}

三、後期處理不同

1、分析與展示

採集 dump 之後,LeakInspector 會上傳 dump 文件,並* 調用 MAT 命令行來進行分析*,得到這次泄漏的 GC 鏈。而 LeakCanary 則用開源組件 HAHA 來分析得到一個 GC 鏈。但是 LeakCanary 得到的 GC 鏈包含被 hold 住的類對象,一般都不需要用 MAT 打開 Hporf 即可解決問題。而 LeakInpsector 得到的 GC 鏈只有類名,還需要 MAT 打開 Hprof 才能具體去定位問題,不是很方便。

2、後續跟進閉環

LeakInspector 在 dump 分析結束之後,會提交缺陷單,並且把缺陷單分配給對應類的負責人。如果發現重複的問題則更新舊單,同時具備重新打開單等狀態轉換邏輯。而 LeakCanary 僅會在通知欄提醒用戶,需要用戶自己記錄該問題並做後續處理。

四、配合自動化測試方面不同

LeakInspector 跟自動化測試可以無縫結合,當自動化腳本執行中發現內存泄漏,可以由它採集 dump 併發送到服務進行分析,最後提單,整個流程是不需要人力介入的。而 LeakCanary 則把分析結果通過通知欄告知用戶,需要人工介入才能進入下一個流程。

4、JHat

JHat 是 Oracle 推出的一款 Hprof 分析軟件,它和 MAT 並稱爲 Java 內存靜態分析利器。不同於 MAT 的單人界面式分析,jHat 使用多人界面式分析。它被 內置在 JDK 中,在命令行中輸入 jhat 命令可查看有沒有相應的命令。

quchao@quchaodeMacBook-Pro ~ % jhat
ERROR: No arguments supplied
Usage:  jhat [-stack <bool>] [-refs <bool>] [-port <port>] [-baseline <file>] [-debug <int>] [-version] [-h|-help] <file>

    -J<flag>          Pass <flag> directly to the runtime system. For
		    example, -J-mx512m to use a maximum heap size of 512MB
    -stack false:     Turn off tracking object allocation call stack.
    -refs false:      Turn off tracking of references to objects
    -port <port>:     Set the port for the HTTP server.  Defaults to 7000
    -exclude <file>:  Specify a file that lists data members that should
		    be excluded from the reachableFrom query.
    -baseline <file>: Specify a baseline object dump.  Objects in
		    both heap dumps with the same ID and same class will
		    be marked as not being "new".
    -debug <int>:     Set debug level.
		        0:  No debug output
		        1:  Debug hprof file parsing
		        2:  Debug hprof file parsing, no server
    -version          Report version number
    -h|-help          Print this help and exit
    <file>            The file to read

For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending "#<number>" to the file name, i.e. "foo.hprof#3".

出現如上輸出,則表明存在 jhat 命令。它的使用很簡單,直在命令行輸入 jhat xxx.hprof 即可,如下所示:

quchao@quchaodeMacBook-Pro ~ % jhat Documents/heapdump/new-33.hprof
Snapshot read, resolving...
Resolving 408200 objects...
Chasing references, expect 81 dots.................................................................................
Eliminating duplicate references.................................................................................
Snapshot resolved.
Started HTTP server on port 7000
Server is ready.

jHat 的執行過程是解析 Hprof 文件,然後啓動 httpsrv 服務,默認是在 7000 端口監聽 Web 客戶端鏈接,維護 Hprof 解析後的數據,以持續供給 Web 客戶端進行查詢操作

啓動服務器後,我們打開 入口地址 127.0.0.1:7000 即可查看 All Classes 界面,如下圖所示:

image

jHat 還有兩個比較重要的功能,分別如下所示:

1、統計表

打開 127.0.0.1:7000/histo/,統計表界面如下所示:

image

可以到,按 Total Size 降序 排列了所有的 Class,並且,我們還可以查看到每一個 Class 與之對應的實例數量。

2、OQL 查詢

OQL 是一種模仿 SQL 語句的查詢語句,通常用來查詢某個類的實例數量,打開 127.0.0.1:7000/oql/ 並輸入 java.lang.String 查詢 String 實例的數量,結果如下圖所示:

image

JHat 比 MAT 更加靈活,且符合大型團隊安裝簡單、團隊協作的需求。但是,並不適合中小型高效溝通型團隊使用。

5、ART GC Log

GC Log 分爲 Dalvik 和 ART 的 GC 日誌,關於 Dalvik 的 GC 日誌,我們在前篇 Android性能優化之內存優化 中已經詳細講解過了,接下來我們說說 ART 的 GC 日誌

ART 的日誌與 Dalvik 的日誌差距非常大,除了格式不同之外,打印的時間也不同,而且,它只有在慢 GC 時纔會打印出來。下面我們看看這條 ART GC Log:

Explicit (full) concurrent mark sweep GC freed 104710 (7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free,25MB/38MB paused 1.230ms total 67.216ms
GC產生的原因 GC類型 採集方法 釋放的數量和佔用的空間 釋放的大對象數量和所佔用的空間 堆中空閒空間的百分比和(對象的個數)/(堆的總空間) 暫停耗時

GC 產生的原因

GC 產生的原因有如下九種:

  • 1)、Concurrent、Alloc、Explicit 跟 Dalvik 的基本一樣,這裏就不重複介紹了。
  • 2)、NativeAlloc:Native 內存分配時,比如爲 Bitmaps 或者 RenderScript 分配對象, 這會導致Native內存壓力,從而觸發GC
  • 3)、Background:後臺 GC,觸發是爲了給後面的內存申請預留更多空間
  • 4)、CollectorTransition:由堆轉換引起的回收,這是運行時切換 GC 而引起的。收集器轉換包括將所有對象從空閒列表空間複製到碰撞指針空間(反之亦然)。當前,收集器轉換僅在以下情況下出現:在內存較小的設備上,App 將進程狀態從可察覺的暫停狀態變更爲可察覺的非暫停狀態(反之亦然)
  • 5)、HomogeneousSpaceCompact:齊性空間壓縮是指空閒列表到壓縮的空閒列表空間,通常發生在當 App 已經移動到可察覺的暫停進程狀態。這樣做的主要原因是減少了內存使用並對堆內存進行碎片整理
  • 6)、DisableMovingGc:不是真正的觸發 GC 原因,發生併發堆壓縮時,由於使用了 GetPrimitiveArrayCritical,收集會被阻塞。一般情況下,強烈建議不要使用 GetPrimitiveArrayCritical
  • 7)、HeapTrim:不是觸發GC原因,但是請注意,收集會一直被阻塞,直到堆內存整理完畢

GC 類型

GC 類型有如下三種:

  • 1)、Full:與Dalvik的 FULL GC 差不多
  • 2)、Partial:跟 Dalvik 的局部 GC 差不多,策略時不包含 Zygote Heap
  • 3)、Sticky:另外一種局部中的局部 GC,選擇局部的策略是上次垃圾回收後新分配的對象

GC採集的方法

GC 採集的方法有如下四種:

  • 1)、mark sweep:先記錄全部對象,然後從 GC ROOT 開始找出間接和直接的對象並標註。利用之前記錄的全部對象和標註的對象對比,其餘的對象就應該需要垃圾回收了
  • 2)、concurrent mark sweep:使用 mark sweep 採集器的併發 GC
  • 3)、mark compact:在標記存活對象的時候,所有的存活對象壓縮到內存的一端,而另一端可以更加高效地被回收
  • 4)、semispace:在做垃圾掃描的時候,把所有引用的對象從一個空間移到另外一個空間,然後直接 GC 剩餘在舊空間中的對象即可

通過 GC 日誌,我們可以知道 GC 的量和 它對卡頓的影響,也可以 初步定位一些如主動調用GC、可分配的內存不足、過多使用Weak Reference 等問題。

6、Chrome Devtool

對於 HTML5 頁面而言,抓取 JavaScript 的內存需要使用 Chrome Devtools 來進行遠程調試。方式有如下兩種:

  • 1)、直接把 URL 抓取出來放到 Chrome 裏訪問。
  • 2)、用 Android H5 遠程調試。

純H5

1、手機安裝 Chrome,打開 USB 調試模式,通過 USB 連上電腦,在 Chrome 裏打開一個頁面,比如百度頁面。然後在 PC Chrome 地址欄裏訪問 Chrome://inspect,如下圖所示:

image

2、最後,直接點擊 Chrome 下面的 inspect 選項即可彈出開發者工具界面。如下圖所示:

image

默認 Hybrid H5 調試

Android 4.4 及以上系統的原生瀏覽器就是 Chrome 瀏覽器,可以使用 Chrome Devtool 遠程調試 WebView,前提是需要在 App 的代碼裏把調試開關打開,如下代碼所示:

if (Build.VERSION_SDK_INT >= Build.VERSION_CODES.KITKAT && 是debug模式) {
    WebView.setWebContentsDebuggingEnabled(ture);
}

打開後的調試方法跟純 H5 頁面調試方法一樣,直接在 App 中打開 H5 頁面,再到 PC Chrome 的 inpsector 頁面就可以看到調試目標頁面。

這裏總結一下 JS 中幾種常見的內存問題點

  • 1)、closure 閉包函數
  • 2)、事件監聽
  • 3)、變量作用域使用不當,全局變量的引用導致無法釋放
  • 4)、DOM 節點的泄漏

若想更深入地學習 Chrome 開發者工具的使用方法,請查看 《Chrome開發者工具中文手冊》

八、內存問題總結

在我們進行內存優化的過程中,有許多內存問題都可以歸結爲一類問題,爲了便於以後快速地解決類似的內存問題,我將它們歸結成了以下的多個要點

1、內類是有危險的編碼方式

說道內類就不得不提到 ”this$0“,它是一種奇特的內類成員,每個類實例都具有一個 this$0,當它的內類需要訪問它的成員時,內類就會持有外類的 this$0,通過 this$0 就可以訪問外部類所有的成員。

解決方案是在 Activity 關閉,即觸發 onDestory 時解除內類和外部的引用關係。

2、普通 Hanlder 內部類的問題

這也是一個 this$0 間接引用的問題,對於 Handler 的解決方案一般可以歸結爲如下三個步驟:

  • 1)、把內類聲明成 static:用來斷絕 this$0 的引用。因爲 static 描述的內類從 Java 編譯原理的角度看,”內類“與”外類“相互獨立,互相都沒有訪問對方成員變量的能力
  • 2、使用 WeakReference 來引用外部類的實例
  • 3、在外部類(如 Activity)銷燬的時候使用 removeCallbackAndMessages 來移除回調和消息

這裏需要在使用過程中注意對 WeakReference 進行判空

3、登錄界面的內存問題

如果在閃屏頁跳轉到登錄界面時沒有調用 finish(),則會造成閃屏頁的內存泄漏,在碰到這種”過渡界面“的情況時,需要注意不要產生這樣的內存 Bug

4、使用系統服務時產生的內存問題

我們通常都會使用 getSystemService 方法來獲取系統服務,但是當在 Activity 中調用時,會默認把 Activity 的 Context 傳給系統服務,在某些不確定的情況下,某些系統服務內部會產生異常,從而 hold 住外界傳入的 Context。

解決方案是 直接使用 Applicaiton 的 Context 去獲取系統服務

5、把 WebView 類型的泄漏裝進垃圾桶進程

我們都知道,對應 WebView 來說,其 網絡延時、引擎 Session 管理、Cookies 管理、引擎內核線程、HTML5 調用系統聲音、視頻播放組件等產生的引用鏈條無法及時打斷,造成的內存問題基本上可以用”無解“來形容。

解決方案是我們可以 把 WebView 裝入另一個進程
具體爲在 AndroidManifes 中對當前的 Activity 設置 android:process 屬性即可,最後,在 Activity 的 onDestory 中退出進程,這樣即可基本上終結 WebView 造成的泄漏

6、在適當的時候對組件進行註銷

我們在平常開發過程中經常需要在Activity創建的時候去註冊一些組件,如廣播、定時器、事件總線等等。這個時候我們應該在適當的時候對組件進行註銷,如 onPause 或 onDestory 方法中

7、Handler / FrameLayout 的 postDelyed 方法觸發的內存問題

不僅在使用 Handler 的 sendMessage 方法時,我們需要在 onDestory 中使用 removeCallbackAndMessage 移除回調和消息,在使用到 Handler / FrameLayout 的 postDelyed 方法時,我們需要調用 removeCallbacks 去移除實現控件內部的延時器對 Runnable 內類的持有

8、圖片放錯資源目錄也會有內存問題

在做資源適配的時候,因爲需要考慮到 APK 的瘦身問題,無法爲每張圖片在每個 drawable / mipmap 目錄下安置一張適配圖片的副本。很多同學不知道圖片應該放哪個目錄,如果放到分辨率低的目錄如 hdpi 目錄,則可能會造成內存問題,這個時候建議儘量問設計人員要高品質圖片然後往高密度目錄下方,如 xxhdpi 目錄,這樣 在低密屏上”放大倍數“是小於1的,在保證畫質的前提下,內存也是可控的。也可以使用 Drawable.createFromSream 替換 getResources().getDrawable 來加載,這樣便可以繞過 Android 的默認適配規則

對於已經被用戶使用物理“返回鍵”退回到後臺的進程,如果包含了以下 兩點,則 不會被輕易殺死

  • 1)、進程包含了服務 startService,而服務本身調用了 startForeground(低版本需通過反射調用)
  • 2)、主 Activity 沒有實現 onSaveInstanceState 接口

但建議 在運行一段時間(如3小時)後主動保存界面進程(位於後臺),然後重啓它,這樣可以有效地降低內存負載

9、列表 item 被回收時注意釋放圖片的引用

我們應該在 item 被回收不可見時去釋放掉對圖片的引用。如果你使用的是 ListView,由於每次 item 被回收後被再次利用都會去重新綁定數據,所以只需在 ImageView 回調其 onDetchFromWindow 方法的時候區釋放掉圖片的引用即可。如果你使用的是 RecyclerView,因爲被回收不可見時第一次選擇是放進 mCacheView中,但是這裏面的 item 被複用時並不會去執行 bindViewHolder 來重新綁定數據,只有被回收進 mRecyclePool 後拿出來複用纔會重新綁定數據。所以此時我們應該在 item 被回收進 RecyclePool 的時候去釋放圖片的引用,這裏我們只要去 重寫 Adapter 中的 onViewRecycled 方法 就可以了,代碼如下所示:

@Override
public void onViewRecycled(@Nullable VH holder) {
    super.onViewRecycled(holder);
    if (holder != null) {
        //做釋放圖片引用的操作
    }
}

10、使用 ViewStub 進行佔位

我們應該使用 ViewStub 對那些沒有馬上用到的資源去做延遲加載,並且還有很多大概率不會出現的 View 更要去做懶加載,這樣可以等到要使用時再去爲它們分配相應的內存。

11、注意定時清理 App 過時的埋點數據

產品或者運營爲了統計數據會在每個版本中不斷地增加新的埋點。所以我們需要定期地去清理一些過時的埋點,以此來 適當地優化內存以及CPU的壓力

12、針對匿名內部類 Runnable 造成內存泄漏的處理

我們在做子線程操作的時候,喜歡使用匿名內部類 Runnable 來操作。但是,如果某個 Activity 放在線程池中的任務不能及時執行完畢,在 Activity 銷燬時很容易導致內存泄漏。因爲這個匿名內部類 Runnable 類持有一個指向 Outer 類的引用,這樣一來如果 Activity 裏面的 Runnable 不能及時執行,就會使它外圍的 Activity 無法釋放,產生內存泄漏。從上面的分析可知,只要在 Activity 退出時沒有這個引用即可,那我們就通過反射,在 Runnable 進入線程池前先幹掉它,代碼如下所示:

Field f = job.getClass().getDeclaredField("this$0");
f.setAccessible(true);
f.set(job, null);

這個任務就是我們的 Runnable 對象,而 ”this$0“ 就是上面所指的外部類的引用了。這裏注意使用 WeakReference 裝起來,要執行了先 get 一下,如果是 null 則說明 Activity 已經回收,任務就放棄執行。

九、內存優化常見問題

1、你們內存優化項目的過程是怎麼做的?

1、分析現狀、確認問題

我們發現我們的 APP 在內存方面可能存在很大的問題,第一方面的原因是我們的線上的 OOM 率比較高。

第二點呢,我們經常會看到在我們的 Android Studio 的 Profiler 工具中內存的抖動比較頻繁。

這是我們一個初步的現狀,然後在我們知道了這個初步的現狀之後,進行了問題的確認,我們經過一系列的調研以及深入研究,我們最終發現我們的項目中存在以下幾點大問題,比如說:內存抖動、內存溢出、內存泄漏,還有我們的Bitmap 使用非常粗獷

2、針對性優化

比如 內存抖動的解決 => Memory Profiler 工具的使用(呈現了鋸齒張圖形) => 分析到具體代碼存在的問題(頻繁被調用的方法中出現了日誌字符串的拼接),也可以說說 內存泄漏或內存溢出的解決

3、效率提升

爲了不增加業務同學的工作量,我們使用了一些工具類或 ARTHook 這樣的 大圖檢測方案,沒有任何的侵入性。同時,我們將這些技術教給了大家,然後讓大家一起進行 工作效率上的提升

我們對內存優化工具Profiler Memory、MAT 的使用比較熟悉,因此 針對一系列不同問題的情況,我們寫了 一系列解決方案的文檔,分享給大家。這樣,我們 整個團隊成員的內存優化意識就變強 了。

2、你做了內存優化最大的感受是什麼?

1、磨刀不誤砍柴工

我們一開始並沒有直接去分析項目中代碼哪些地方存在內存問題,而是先去學習了 Google 官方的一些文檔,比如說學習了 Memory Profiler 工具的使用、學習了 MAT 工具的使用,在我們將這些工具學習熟練之後,當在我們的項目中遇到內存問題時,我們就能夠很快地進行排查定位問題進行解決。

2、技術優化必須結合業務代碼

一開始,我們做了整體 APP 運行階段的一個內存上報,然後,我們在一些重點的內存消耗模塊進行了一些監控,但是,後面發現這些監控並沒有緊密地結合我們的業務代碼,比如說在梳理完項目之後,發現我們項目中存在使用多個圖片庫的情況,多個圖片庫的內存緩存肯定是不公用的,所以 導致我們整個項目的內存使用量非常高。所以進行技術優化時必須結合我們的業務代碼。

3、系統化完善解決方案

我們在做內存優化的過程中,不僅做了 Android 端的優化工作,還將我們 Android 端一些數據的採集上報到了我們的服務器,然後傳到我們的 APM 後臺,這樣,方便我們的無論是 Bug 跟蹤人員或者是 Crash 跟蹤人員進行一系列問題的解決。

3、如何檢測所有不合理的地方?

比如說 大圖片的檢測,我們最初的一個方案是通過繼承 ImageView重寫 它的 onDraw 方法來實現。但是,我們在推廣它的過程中,發現很多開發人員並不接受,因爲很多 ImageView 之前已經寫過了,你現在讓他去替換,工作成本是比較高的。所以說,後來我們就想,有沒有一種方案可以 免替換,最終我們就找到了 ARTHook 這樣一個 Hook 的方案。

十、總結

對於 內存優化的專項優化 而言,我們要着重注意兩點,即 優化大方向 和 優化細節

1、優化大方向

對於 優化的大方向,我們應該 優先去做見效快的地方,主要有以下三部分:

  • 1)、內存泄漏
  • 2)、內存抖動
  • 3)、Bitmap

2、優化細節

對於 優化細節,我們應該 注意一些系統屬性或內存回調的使用 等等,主要可以細分爲如下六部分:

  • 1)、LargeHeap 屬性
  • 2)、onTrimMemory / onLowMemory
  • 3)、使用優化過後的集合:如 SparseArray 類簇
  • 4)、謹慎使用 SharedPreference
  • 5)、謹慎使用外部庫
  • 6)、業務架構設計合理

3、內存優化體系化建設總結

在這篇文章中,我們除了建立了 內存的監控閉環 這一核心體系之外,還實現了以下 十大組件 / 策略

  • 1)、根據設備分級來使用不同的內存和分配回收策略
  • 2)、針對低端機做了功能或圖片加載格式的降級處理
  • 3)、針對緩存濫用的問題實現了統一的緩存管理組件
  • 4)、實現了大圖監控和重複圖片的監控
  • 5)、在前臺每隔一定時間去獲取當前應用內存佔最大內存的比例,當超過設定閾值時則主動釋放應用 cache
  • 6)、當 UI 隱藏時釋放內存以增加系統緩存應用進程的能力
  • 7)、高效實現了應用全局內的 Bitmap 監控
  • 8)、實現了全局的線程監控
  • 9)、針對內存使用的重度場景實現了 GC 監控
  • 10)、實現了線下的 native 內存泄漏監控

最後,當監控到 應用內存超過閾值時,還定製了 完善的兜底策略重啓應用進程

總的來看,要建立一套 全面且成體系的內存優化及監控 是非常重要也是極具挑戰性的一項工作。並且,目前各大公司的 內存優化體系 也正處於 不斷演進的歷程 之中,其目的不外乎:實現更健全的功能、更深層次的定位問題、快速準確地發現線上問題

路漫漫其修遠兮,吾將上下而求索

參考鏈接:

1、國內Top團隊大牛帶你玩轉Android性能分析與優化 第四章 內存優化

2、極客時間之Android開發高手課 內存優化

3、微信 Android 終端內存優化實踐

4、GMTC-Android內存泄漏自動化鏈路分析組件Probe.key

5、Manage your app’s memory

6、Overview of memory management

7、Android內存優化雜談

8、Android性能優化之內存篇

9、管理應用的內存

10、《Android移動性能實戰》第二章 內存

11、每天一個linux命令(44):top命令

12、Android內存分析命令

Contanct Me

● 微信:

歡迎關注我的微信:bcce5360

● 微信羣:

微信羣如果不能掃碼加入,麻煩大家想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎大家加入~

About me

很感謝您閱讀這篇文章,希望您能將它分享給您的朋友或技術羣,這對我意義重大。

希望我們能成爲朋友,在 Github掘金上一起分享知識。

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