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

前言

成爲一名優秀的Android開發,須要一份完備的知識體系,在這裏,讓咱們一塊兒成長爲本身所想的那樣~。

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

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

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

本篇文章很是長,建議收藏後慢慢享用~前端

目錄

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

1、重識內存優化

Android給每一個應用進程分配的內存都是很是有限的,那麼,爲何不能把圖片下載下來都放到磁盤中呢?那是由於放在 內存 中,展現會更 「」,快的緣由有兩點,以下所示:java

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

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

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

一、手機RAM

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

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

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

當系統 內存充足 的時候,咱們能夠 多用 一些得到 更好的性能。當系統 內存不足 的時候,咱們但願能夠作到 」用時分配,及時釋放「。github

二、內存優化的緯度

對於Android內存優化來講又能夠細分爲以下兩個維度,以下所示:web

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

一、RAM優化

主要是 下降運行時內存。它的 目的 有以下三個:算法

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

二、ROM優化

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

三、內存問題

那麼,內存問題主要是有哪幾類呢?內存問題一般來講,能夠細分爲以下 三類:

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

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

一、內存抖動

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

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

二、內存泄漏

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

三、內存溢出

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

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

2、常見工具選擇

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

一、Memory Profiler

做用

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

優勢

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

二、Memory Analyzer

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

三、LeakCanary

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

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

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

3、Android內存管理機制回顧

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

一、Java 內存分配

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

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

二、Java 內存回收算法

一、標記-清除算法

流程可簡述爲 兩步

  • 1)、標記全部須要回收的對象
  • 2)、統一回收全部被標記的對象

優勢

實現比較簡單。

缺點

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

二、複製算法

流程可簡述爲 三步

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

優勢

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

缺點

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

三、標記-整理算法

流程可簡述爲 三步

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

優勢

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

四、分代收集算法

如今 主流的虛擬機 通常用的比較多的仍是分代收集算法,它具備以下 特色

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

三、Android 內存管理機制

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

對於 OOM場景 其實能夠細分爲以下兩種:

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

咱們須要着重注意一下這兩種的區分。

四、小結

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

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

4、內存抖動

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

而且,它的危害也很嚴重,一般會致使 頁面卡頓,甚至形成 OOM

一、那麼,爲何內存抖動會致使 OOM?

主要緣由有以下兩點:

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

二、內存抖動解決實戰

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

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

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

三、內存抖動常見案例

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

一、字符串使用加號拼接

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

二、資源複用

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

三、減小不合理的對象建立

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

四、使用合理的數據結構

使用 SparseArray類族、ArrayMap 來替代 HashMap

5、內存優化體系化搭建

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

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

而對於 內存泄漏的分析 通常可簡述爲以下 兩步

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

一、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

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

一、Dominator(支配者):

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

二、Histogram和dominator_tree的區別:

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

三、thread_overview

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

四、Top Consumers

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

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

五、Leak Suspects

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

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

在介紹圖片監控體系的搭建以前,首先咱們來回顧下 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 通常限制爲 25六、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 如下所佔的比例

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

一、統一圖片庫

在項目中,咱們須要 收攏圖片的調用,避免使用 Bitmap.createBitmap、BitmapFactory 相關的接口建立 Bitmap,而應該使用本身的圖片框架

二、設備分級優化策略

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

所以,咱們能夠使用相似 device-year-class 的策略對設備進行分級,對於低端機用戶能夠關閉複雜的動畫或」重功能「,使用565格式的圖片或更小的緩存內存 等等。

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

三、創建統一的緩存管理組件

創建統一的緩存管理組件(參考 ACache),併合理使用 OnTrimMemory / LowMemory 回調,根據系統不一樣的狀態去釋放相應的緩存與內存

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

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

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

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

針對低端機用戶能夠推出 4MB 的輕量級版本,現在日頭條極速版、Facebook Lite。

五、線下大圖片檢測

在開發過程當中,若是檢測到不合規的圖片使用(如圖片寬度超過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一般的使用步驟爲以下三個步驟:

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

compile 'me.weishu:epic:0.6.0'
複製代碼

二、繼承 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..");
    }
}
複製代碼

三、注入 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) {
                    // 二、圖標寬高都大於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 {
                    // 三、當寬高度等於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 獲取其寬高進行大圖檢測的處理。至此,咱們的大圖檢測檢測組件就已經實現了。若是有小夥伴對 epic 的實現原理感興趣的,能夠查看這篇文章

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

六、線下重複圖片檢測

首先咱們來了解一下這裏的 重複圖片 所指的概念: 即 Bitmap 像素數據徹底一致,可是有多個不一樣的對象存在

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

已徹底配置好的項目請參見這裏

使用說明

使用很是簡單,只須要修改 Main 類的 main 方法的第一行代碼,以下所示:

// 設置咱們本身 App 中對應的 hprof 文件路徑
String dumpFilePath = "//Users//quchao//Documents//heapdump//memory-40.hprof";
複製代碼

而後,咱們執行 main 方法便可在 //Users//quchao//Documents//heapdump 這個路徑下看到生成的 images 文件夾,裏面保存了項目中檢測出來的重複的圖片。images 目錄以下所示:

注意:須要使用 8.0 如下的機器,由於 8.0 及之後 Bitmap 中的 buffer 已保存在 native 內存之中。

實現步驟

具體的實現能夠細分爲以下三個步驟:

  • 1)、首先,獲取 android.graphics.Bitmap 實例對象的 mBuffer 做爲 ArrayInstance ,經過 getValues 獲取的數據爲 Object 類型。因爲後面計算 md5 須要爲 byte[] 類型,因此經過反射的方式調用 ArrayInstance#asRawByteArray 直接返回 byte[] 數據
  • 2)、而後,根據 mBuffer 的數據生成 png 圖片文件,這裏直接參考了 github.com/JetBrains/a… 的實現方式。
  • 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 獲取整個引用鏈信息並打印

七、創建全局的線上 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 後臺。

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

三、創建線上應用內存監控體系

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

  • 一、首先,經過 ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 獲取內存信息數據
  • 二、而後,經過 hook Debug.MemoryInfo 的 getMemoryStat 方法(os v23 及以上)能夠得到 Memory Profiler 中的多項數據,進而得到 細份內存的使用狀況
  • 三、接着,經過 Runtime 獲取 DalvikHeap
  • 四、最後,經過 Debug.getNativeHeapAllocatedSize 獲取 NativeHeap

對於監控場景,咱們須要將其劃分爲兩大類,以下所示:

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

一、常規內存監控

根據 斐波那契數列 每隔一段時間(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庫,它的 基本用法 通常遵循爲以下四個步驟:

一、導出堆棧文件

File heapDumpFile = ...
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
複製代碼

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

DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
複製代碼

三、根據文件緩存區建立出對應的快照

Snapshot snapshot = Snapshot.createSnapshot(buffer);
複製代碼

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

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

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

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

二、低內存監控

對於低內存的監控,一般有兩種方式,分別以下所示:

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

三、內存監控指標

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

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 和 卡頓

四、小結

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

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

四、創建全局的線程監控組件

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

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

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

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

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

五、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的次數和耗時,由於它會 暫停線程,可能致使應用發生 卡頓。建議 僅對重度場景使用

六、創建線上 OOM 監控組件:Probe

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

Probe 組件的缺陷及解決方案

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

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

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

一、鏈路分析時間過長

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

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

分析進程佔用的內存內存快照文件的大小 不成正相關,而跟 內存快照文件的 Instance 數量正相關。因此在開發過程當中咱們應該 儘量排除不須要的Instance實例

Prope 分析流程揭祕

Prope 的 整體架構圖 以下所示:

image

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

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

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

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

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

二、生成 Dominator Tree

三、計算 RetainSize

四、生成 Reference 鏈 && 基礎數據類型加強:

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

五、鏈路歸併

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

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

七、排序擴容

八、查找泄露鏈路

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

項目地址請點擊此處

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

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

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

    sourceSets.main.java.srcDirs = ['src']

使用步驟

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

一、首先,點擊 」開始記錄「 按鈕能夠看到觸發對象分配的記錄,說明對象已經開始記錄對象的分配,log以下所示:

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

二、而後,點擊屢次 」生成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
複製代碼

三、此時,能夠看到數據保存在 sdk 下的 crashDump 目錄下。

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

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

五、最後,就能夠在 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)
複製代碼

八、搭建線下 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)、最後,按期掃描分配與釋放是否配對,對於不配對的分配輸出咱們記錄的信息

九、設置內存兜底策略

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

一般執行內存兜底策略時至少須要知足六個條件,以下所示:

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

只有在知足了以上條件以後,咱們纔會去殺死當前主進程並經過 push 進程從新拉起及初始化

十、更深刻的內存優化策略

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

一、使 bitmap 資源在 native 中分配

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

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

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

使用 Glide、Fresco 等圖片加載庫,經過定製,在加載 bitmap 時,若發生 OOM,則使用 try catch 將其捕獲,而後清除圖片 cache,嘗試下降 bitmap format(ARGB888八、RGB56五、ARGB444四、ALPHA8)。

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

三、前臺每隔 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);
複製代碼

五、因爲 webview 存在內存系統泄漏,還有 圖庫佔用內存過多 的問題,能夠採用單獨的進程。

六、當UI隱藏時釋放內存

當用戶切換到其它應用而且你的應用 UI 再也不可見時,應該釋放應用 UI 所佔用的全部內存資源。這可以顯著增長系統緩存進程的能力,可以提高用戶體驗。

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

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

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

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

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

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

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

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

6、內存優化演進

一、自動化測試階段

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

二、LeakCanary

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

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

Matrix => ResourceCanary 實現原理

主要功能

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

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

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

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

獲取 須要的類和對象相關的字符串 信息便可,其它數據均可以在客戶端裁剪,通常能 Hprof 大小會減少至原來的 1/10 左右。

三、增長重複 Bitmap 對象檢測

方便經過減小冗餘 Bitmap 的數量,以下降內存消耗。

四、小結

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

7、內存優化工具

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

一、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小時。

二、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: android.hardware.camera.provider@2.4-service (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
複製代碼

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

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

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

二、查看 Views、Activities、AppContexts 數量變化狀況

若是 Views 與 Activities、AppContexts 持續上升,則代表有內存泄漏的風險

三、SQL 的 MEMORY_USED 與 PAGECACHE_OVERFLOW

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

四、查看 DATABASES 信息

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

三、LeakInspector

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

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

它們之間主要有 四個方面 的不一樣,以下所示:

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

一、檢測能力

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

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

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

二、檢測原理

兩個工具的泄漏檢測原理都是在 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());
複製代碼

2、泄漏現場處理方面不一樣

一、dump 採集

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

二、白名單定義

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

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

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

    • 優勢:定義很是詳細,並區分系統版本。
    • 缺點:每次修改一定得從新編譯。
  • 2)、LeakCanary 的系統白名單裏定義的類比 LeakInspector 中定義的多不少,由於它沒有自動修復系統泄漏功能。

三、自動修復系統泄漏

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

四、回收資源(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);
}
複製代碼

3、後期處理不一樣

一、分析與展現

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

二、後續跟進閉環

LeakInspector 在 dump 分析結束以後,會提交缺陷單,而且把缺陷單分配給對應類的負責人。若是發現重複的問題則更新舊單,同時具有從新打開單等狀態轉換邏輯。而 LeakCanary 僅會在通知欄提醒用戶,須要用戶本身記錄該問題並作後續處理。

4、配合自動化測試方面不一樣

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

四、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 還有兩個比較重要的功能,分別以下所示:

一、統計表

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

image

能夠到,按 Total Size 降序 排列了全部的 Class,而且,咱們還能夠查看到每個 Class 與之對應的實例數量。

二、OQL 查詢

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

image

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

五、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 等問題。

六、Chrome Devtool

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

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

純H5

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

image

二、最後,直接點擊 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開發者工具中文手冊》

8、內存問題總結

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

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

說道內類就不得不提到 」this$0「,它是一種奇特的內類成員,每一個類實例都具備一個 this$0,當它的內類須要訪問它的成員時,內類就會持有外類的 this$0,經過 this$0 就能夠訪問外部類全部的成員。

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

二、普通 Hanlder 內部類的問題

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

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

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

三、登陸界面的內存問題

若是在閃屏頁跳轉到登陸界面時沒有調用 finish(),則會形成閃屏頁的內存泄漏,在碰到這種」過渡界面「的狀況時,須要注意不要產生這樣的內存 Bug

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

九、列表 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) {
        //作釋放圖片引用的操做
    }
}
複製代碼

十、使用 ViewStub 進行佔位

咱們應該使用 ViewStub 對那些沒有立刻用到的資源去作延遲加載,而且還有不少大機率不會出現的 View 更要去作懶加載,這樣能夠等到要使用時再去爲它們分配相應的內存。

十一、注意定時清理 App 過期的埋點數據

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

十二、針對匿名內部類 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 已經回收,任務就放棄執行。

9、內存優化常見問題

一、大家內存優化項目的過程是怎麼作的?

一、分析現狀、確認問題

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

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

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

二、針對性優化

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

三、效率提高

爲了避免增長業務同窗的工做量,咱們使用了一些工具類或 ARTHook 這樣的 大圖檢測方案,沒有任何的侵入性。同時,咱們將這些技術教給了你們,而後讓你們一塊兒進行 工做效率上的提高

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

二、你作了內存優化最大的感覺是什麼?

一、磨刀不誤砍柴工

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

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

一開始,咱們作了總體 APP 運行階段的一個內存上報,而後,咱們在一些重點的內存消耗模塊進行了一些監控,可是,後面發現這些監控並無緊密地結合咱們的業務代碼,好比說在梳理完項目以後,發現咱們項目中存在使用多個圖片庫的狀況,多個圖片庫的內存緩存確定是不公用的,因此 致使咱們整個項目的內存使用量很是高。因此進行技術優化時必須結合咱們的業務代碼。

三、系統化完善解決方案

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

三、如何檢測全部不合理的地方?

好比說 大圖片的檢測,咱們最初的一個方案是經過繼承 ImageView重寫 它的 onDraw 方法來實現。可是,咱們在推廣它的過程當中,發現不少開發人員並不接受,由於不少 ImageView 以前已經寫過了,你如今讓他去替換,工做成本是比較高的。因此說,後來咱們就想,有沒有一種方案能夠 免替換,最終咱們就找到了 ARTHook 這樣一個 Hook 的方案。

10、總結

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

一、優化大方向

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

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

二、優化細節

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

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

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

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

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

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

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

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

參考連接:

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

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

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

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

五、Manage your app's memory

六、Overview of memory management

七、Android內存優化雜談

八、Android性能優化以內存篇

九、管理應用的內存

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

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

十二、Android內存分析命令

Contanct Me

● 微信:

歡迎關注個人微信:bcce5360

● 微信羣:

微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。

● QQ羣:

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

About me

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

但願咱們能成爲朋友,在 Github掘金上一塊兒分享知識。

相關文章
相關標籤/搜索