本篇是 Android 內存優化的進階篇,難度能夠說達到了煉獄級別,建議對內存優化不是很是熟悉的仔細看看前篇文章: Android性能優化以內存優化,其中詳細分析瞭如下幾大模塊:php
若是你對以上基礎內容都比較瞭解了,那麼咱們便開始 Android 內存優化的探索之旅吧。html
本篇文章很是長,建議收藏後慢慢享用~前端
Android給每一個應用進程分配的內存都是很是有限的,那麼,爲何不能把圖片下載下來都放到磁盤中呢?那是由於放在 內存 中,展現會更 「快」,快的緣由有兩點,以下所示:java
這裏說一下解碼的概念。Android系統要在屏幕上展現圖片的時候只認 「像素緩衝」,而這也是大多數操做系統的特徵。而咱們 常見的jpg,png等圖片格式,都是把 「像素緩衝」 使用不一樣的手段壓縮後的結果,因此這些格式的圖片,要在設備上 展現,就 必須通過一次解碼,它的 執行速度會受圖片壓縮比、尺寸等因素影響。(官方建議:把從內存中淘汰的圖片,下降壓縮比後存儲到本地,以備後用,這樣能夠最大限度地下降之後複用時的解碼開銷。)linux
下面,咱們來了解一下內存優化的一些重要概念。android
手機不使用 PC 的 DDR內存,採用的是 LPDDR RAM,即 」低功耗雙倍數據速率內存「。其計算規則以下所示:git
LPDDR系列的帶寬 = 時鐘頻率 ✖️內存總線位數 / 8
LPDDR4 = 1600MHZ ✖️64 / 8 ✖️雙倍速率 = 25.6GB/s。
複製代碼
當系統 內存充足 的時候,咱們能夠 多用 一些得到 更好的性能。當系統 內存不足 的時候,咱們但願能夠作到 」用時分配,及時釋放「。github
對於Android內存優化來講又能夠細分爲以下兩個維度,以下所示:web
主要是 下降運行時內存。它的 目的 有以下三個:算法
下降應用佔ROM的體積,進行APK瘦身。它的 目的 主要是爲了 下降應用佔用空間,避免因ROM空間不足致使程序沒法安裝。
那麼,內存問題主要是有哪幾類呢?內存問題一般來講,能夠細分爲以下 三類:
下面,咱們來了解下它們。
內存波動圖形呈 鋸齒張、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就是因圖片處理不當 而產生的。
在 Android性能優化以內存優化 中咱們已經介紹過了相關的優化工具,這裏再簡單回顧一下。
強大的 Java Heap 分析工具,查找 內存泄漏及內存佔用, 生成 總體報告、分析內存問題 等等。建議 線下深刻使用。
自動化 內存泄漏檢測神器。建議僅用於線下集成。
它的 缺點 比較明顯,具體有以下兩點:
ART 和 Dalvik 虛擬機使用 分頁和內存映射 來管理內存。下面咱們先從Java的內存分配開始提及。
Java的 內存分配區域 分爲以下 五部分:
流程可簡述爲 兩步:
實現比較簡單。
流程可簡述爲 三步:
實現簡單,運行高效,每次僅需遍歷標記一半的內存區域。
會浪費一半的空間,代價大。
流程可簡述爲 三步:
如今 主流的虛擬機 通常用的比較多的仍是分代收集算法,它具備以下 特色:
Android 中的內存是 彈性分配 的,分配值 與 最大值 受具體設備影響。
對於 OOM場景 其實能夠細分爲以下兩種:
咱們須要着重注意一下這兩種的區分。
以Android中虛擬機的角度來講,咱們要清楚 Dalvik 與 ART 區別,Dalvik 僅固定一種回收算法,而 ART 回收算法可在 運行期按需選擇,而且,ART 具有 內存整理 能力,減小內存空洞。
最後,LMK(Low Memory killer) 機制保證了進程資源的合理利用,它的實現原理主要是 根據進程分類和回收收益來綜合決定的一套算法集。
當 內存頻繁分配和回收 致使內存 不穩定,就會出現內存抖動,它一般表現爲 頻繁GC、內存曲線呈鋸齒狀。
而且,它的危害也很嚴重,一般會致使 頁面卡頓,甚至形成 OOM。
主要緣由有以下兩點:
這裏咱們假設有這樣一個場景:點擊按鈕使用 handler 發送一個空消息,handler 的 handleMessage 接收到消息後建立內存抖動,即在 for 循環建立 100個容量爲10萬 的 strings 數組並在 30ms 後繼續發送空消息。
通常使用 Memory Profiler (表現爲 頻繁GC、內存曲線呈鋸齒狀)結合代碼排查便可找到內存抖動出現的地方。
一般的技巧就是着重查看 循環或頻繁被調用 的地方。
下面列舉一些致使內存抖動的常見案例,以下所示:
使用 SparseArray類族、ArrayMap 來替代 HashMap。
在開始咱們今天正式的主題以前,咱們先來回歸一下內存泄漏的概念與解決技巧。
所謂的內存泄漏就是 內存中存在已經沒有用的對象。它的 表現 通常爲 內存抖動、可用內存逐漸減小。 它的 危害 即會致使 內存不足、GC頻繁、OOM。
而對於 內存泄漏的分析 通常可簡述爲以下 兩步:
對於MAT來講,其常規的查找內存泄漏的方式能夠細分爲以下三步:
此外,在 Android性能優化以內存優化 還有幾種進階的使用方式,這裏就不一一贅述了,下面,咱們來看看關於 MAT 使用時的一些關鍵細節。
要全面掌握MAT的用法,必需要先了解 隱藏在 MAT 使用中的四大細節,以下所示:
除此以外,MAT 共有 5個關鍵組件 幫助咱們去分析內存方面的問題,分別以下所示:
下面咱們這裏再簡單地回顧一下它們。
若是從GC Root到達對象A的路徑上必須通過對象B,那麼B就是A的支配者。
查看 線程數量 和 線程的 Shallow Heap、Retained Heap、Context Class Loader 與 is Daemon。
經過 圖形 的形式列出 佔用內存比較多的對象。
在下方的 Biggest Objects 還能夠查看其 相對比較詳細的信息,例如 Shallow Heap、Retained Heap。
列出有內存泄漏的地方,點擊 Details 能夠查看其產生內存泄漏的引用鏈。
在介紹圖片監控體系的搭建以前,首先咱們來回顧下 Android Bitmap 內存分配的變化。
將 Bitmap對象 和 像素數據 統一放到 Java Heap 中,即便不調用 recycle,Bitmap 像素數據也會隨着對象一塊兒被回收。
可是,Bitmap 所有放在 Java Heap 中的缺點很明顯,大體有以下兩點:
將圖片內存存放在Native中的步驟有 四步,以下所示:
咱們都知道的是,當 系統內存不足 的時候,LMK 會根據 OOM_adj 開始殺進程,從 後臺、桌面、服務、前臺,直到手機重啓。而且,若是頻繁申請釋放 Java Bitmap 也很容易致使內存抖動。對於這種種問題,咱們該 如何評估內存對應用性能的影響 呢?
對此,咱們能夠主要從如下 兩個方面 進行評估,以下所示:
對於具體的優化策略與手段,咱們能夠從如下 七個方面 來搭建一套 成體系化的圖片優化 / 監控機制。
在項目中,咱們須要 收攏圖片的調用,避免使用 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,即 掛鉤,用額外的代碼勾住原有的方法,以修改執行邏輯,主要能夠用於如下四個方面:
具體咱們是使用 Epic 來進行 Hook,Epic 是 一個虛擬機層面,以 Java 方法爲粒度的運行時 Hook 框架。簡單來講,它就是 ART 上的 Dexposed,而且它目前 支持 Android 4.0~10.0。
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 的實現原理感興趣的,能夠查看這篇文章。
首先咱們來了解一下這裏的 重複圖片 所指的概念: 即 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 內存之中。
具體的實現能夠細分爲以下三個步驟:
其中,獲取堆棧 的信息也能夠直接使用 haha 庫來進行獲取。這裏簡單說一下 使用 haha 庫獲取堆棧的流程,其具體能夠細分爲八個步驟,以下所示:
爲了創建全局的 Bitmap 監控,咱們必須 對 Bitmap 的分配和回收 進行追蹤。咱們先來看看 Bitmap 有哪些特色:
根據以上特色,咱們能夠創建一套 Bitmap 的高性價比監控組件:
這個方案的 性能消耗很低,能夠在 正式環境 中進行。可是,須要注意的一點是,正式與測試環境須要採用不一樣程度的監控。
要創建線上應用的內存監控體系,咱們須要 先獲取 App 的 DalvikHeap 與 NativeHeap,它們的獲取方式可歸結爲以下四個步驟:
對於監控場景,咱們須要將其劃分爲兩大類,以下所示:
根據 斐波那契數列 每隔一段時間(max:30min)獲取內存的使用狀況。常規內存的監控方法有多種實現方式,下面,咱們按照 項目早期 => 壯大期 => 成熟期 的常規內存監控方式進行 演進式 講解。
具體使用 Debug.dumpHprofData() 實現。
其實現的流程爲以下四個步驟:
可是,這種方式有以下幾個缺點:
在使用 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的時候主要要解決的問題有三個,以下所示:
在實現了線上版的 LeakCanary 以後,就須要 將線上版的 LeakCanary 與服務器和前端頁面結合 起來。具體的 內存泄漏監控閉環流程 以下所示:
此外,在實現 圖片內存監控 的過程當中,應注意 兩個關鍵點,以下所示:
對於低內存的監控,一般有兩種方式,分別以下所示:
爲了準確衡量內存性能,咱們須要引入一系列的內存監控指標,以下所示:
內存 UV 異常率 = PSS 超過 400MB 的 UV / 採集UV
PSS 獲取:調用 Debug.MemoryInfo 的 API 便可
複製代碼
若是出現 新的內存使用不當或內存泄漏 的場景,這個指標會有所 上漲。
內存 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 和 卡頓。
在具體實現的時候,客戶端 儘可能只負責 上報數據,而 指標值的計算 能夠由 後臺 來計算。這樣即可以經過 版本對比 來監控是否有 新增內存問題。所以,創建線上內存監控的完整方案 至少須要包含如下四點:
每一個線程初始化都須要 mmap 必定的棧大小,在默認狀況下初始化一個線程須要 mmap 1MB 左右的內存空間。
在 32bit 的應用中有 4g 的 vmsize,實際能使用的有 3g+,這樣一個進程 最大能建立的線程數 能夠達到 3000個,可是,linux 對每一個進程可建立的線程數也有必定的限制(/proc/pid/limits),而且,不一樣廠商也能修改這個限制,超過該限制就會 OOM。
所以,對線程數量的限制,在必定程度上能夠 有效地避免 OOM 的發生。那麼,實現一套 全局的線程監控組件 即是 刻不容緩 的了。
在線下或灰度的環境下經過一個定時器每隔 10分鐘 dump 出應用全部的線程相關信息,當線程數超過當前閾值時,則將當前的線程信息上報並預警。
經過** 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的次數和耗時,由於它會 暫停線程,可能致使應用發生 卡頓。建議 僅對重度場景使用。
美團的 Android 內存泄漏自動化鏈路分析組件 Probe 在 OOM 時會生成 Hprof 內存快照,而後,它會經過 單獨進程 對這個 文件 作進一步 分析。
它的缺點比較多,具體爲以下幾點:
在實現自動化鏈路分析組件 Probe 的過程當中主要要解決兩個問題,以下所示:
分析進程佔用的內存 跟 內存快照文件的大小 不成正相關,而跟 內存快照文件的 Instance 數量 呈 正相關。因此在開發過程當中咱們應該 儘量排除不須要的Instance實例。
Prope 的 整體架構圖 以下所示:
而它的整個分析流程具體能夠細分爲八個步驟,以下所示:
解析後的 Snapshot 中的 Heap 有四種類型,具體爲:
解析完 後使用了 計數壓縮策略,對 相同的 Instance 使用 計數,以 減小佔用內存。超過計數閾值的須要計入計數桶(計數桶記錄了 丟棄個數 和 每一個 Instance 的大小)。
若是對象是 基礎數據類型,會將 自身的 RetainSize 累加到父節點 上,將 懷疑對象 替換爲它的 父節點。
使用計數補償策略計算 RetainSize,主要是 判斷對象是否在計數桶中,若是在的話則將 丟棄的個數和大小補償到對象上,累積計算RetainSize,最後對 RetainSize 排序以查找可疑對象。
在配置的時候要注意兩個問題:
一、liballoc-lib.so在構建後工程的 build => intermediates => cmake 目錄下。將對應的 cpu abi 目錄拷貝到新建的 libs 目錄下。
二、在 DumpPrinter Java 庫的 build.gradle 中的 jar 閉包中須要加入如下代碼以識別源碼路徑:
sourceSets.main.java.srcDirs = ['src']
具體的使用步驟以下所示:
12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
複製代碼
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
複製代碼
java -jar tools/DumpPrinter-1.0.jar dump文件路徑 > 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)
複製代碼
在 Android 8.0 及以後,能夠使用 Address Sanitizer、Malloc 調試和 Malloc 鉤子 進行 native 內存分析,參見 native_memory
對於線下 Native 內存泄漏監控的創建,主要針對 是否能重編 so 的狀況 來記錄分配的內存信息。
設置內存兜底策略的目的,是爲了 在用戶無感知的狀況下,在接近觸發系統異常前,選擇合適的場景殺死進程並將其重啓,從而使得應用內存佔用回到正常狀況。
一般執行內存兜底策略時至少須要知足六個條件,以下所示:
只有在知足了以上條件以後,咱們纔會去殺死當前主進程並經過 push 進程從新拉起及初始化。
除了在 Android性能優化以內存優化 => 優化內存空間 中講解過的一些常規的內存優化策略之外,在下面列舉了一些更深刻的內存優化策略。
對於 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 的問題的。
計算當前應用內存佔最大內存的比例的代碼以下:
max = Runtime.getRuntime().maxMemory();
available = Runtime.getRuntime.totalMemory() - Runtime.getFreeMemory();
ratio = available / max;
複製代碼
顯示地除去應用的 memory,以加速內存收集過程的代碼以下所示:
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
複製代碼
當用戶切換到其它應用而且你的應用 UI 再也不可見時,應該釋放應用 UI 所佔用的全部內存資源。這可以顯著增長系統緩存進程的能力,可以提高用戶體驗。
在全部 UI 組件都隱藏的時候會接收到 Activity 的 onTrimMemory() 回調並帶有參數 TRIM_MEMORY_UI_HIDDEN。
在 Activity 的 onDestory 中遞歸釋放其引用到的 Bitmap、DrawingCache 等資源,以下降發生內存泄漏時對應用內存的壓力。
LeakCanary 的 AndroidExcludeRefs 列出了一些因爲系統緣由致使引用沒法釋放的例子,可以使用相似 Hack 的方式去修復。具體的實現代碼能夠參考 Booster => 系統問題修復。
內存達到閾值後自動觸發 Hprof Dump,將獲得的 Hprof 存檔後由人工經過 MAT 進行分析。
檢測和分析報告都在一塊兒,批量自動化測試和過後分析都不太方便。
目前,它的主要功能有 三個部分,以下所示:
自動化測試由測試平臺進行,分析則由監控平臺的服務端離線完成,最後再通知相關開發解決問題。
獲取 須要的類和對象相關的字符串 信息便可,其它數據均可以在客戶端裁剪,通常能 Hprof 大小會減少至原來的 1/10 左右。
方便經過減小冗餘 Bitmap 的數量,以下降內存消耗。
在研發階段須要不斷實現 更多的工具和組件,以此係統化地提高自動化程度,以最終 提高發現問題的效率。
除了經常使用的內存分析工具 Memory Profiler、MAT、LeakCanary 以外,還有一些其它的內存分析工具,下面我將一一爲你們進行介紹。
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 僅顯示一次進程信息,以便來說解進程信息中各字段的含義。
前四行 是當前系統狀況 總體的統計信息區。下面咱們看每一行信息的具體意義。
具體信息說明以下所示:
系統如今共有 729 個進程,其中處於 運行中 的有 1 個,715 個在 休眠(sleep),stoped 狀態的有0個,zombie 狀態(殭屍)的有 8 個。
具體信息以下所示:
具體信息說明以下所示:
具體屬性說明以下所示:
對於內存監控,在 top 裏咱們要時刻監控 第三行 swap 交換分區的 used,若是這個數值在不斷的變化,說明內核在不斷進行內存和 swap 的數據交換,這是真正的內存不夠用了。
在 第五行及如下,就是各進程(任務)的狀態監控,項目列信息說明以下所示:
從上圖中能夠看到,第一行的就是 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 命令以前,咱們必須先了解下 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 忽大忽小。
瞭解完 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
複製代碼
該命令輸出了 進程的內存概要,咱們應該着重關注 四個要點,下面我將一一進行講解。
若是 Views 與 Activities、AppContexts 持續上升,則代表有內存泄漏的風險。
LeakInspector 是騰訊內部的使用的 一站式內存泄漏解決方案,它是 Android 手機通過長期積累和提煉、集內存泄漏檢測、自動修復系統Bug、自動回收已泄露Activity內資源、自動分析GC鏈、白名單過濾 等功能於一體,並 深度對接研發流程、自動分析責任人並提缺陷單的全鏈路體系。
它們之間主要有 四個方面 的不一樣,以下所示:
它們都支持對 Activity、Fragment 及其它自定義類的泄漏檢測,可是,LeakInspector 還 增長了 Btiamp 的檢測能力,以下所示:
這一個部分的實現原理,咱們能夠採用 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());
複製代碼
二者都能採集 dump,可是 LeakInspector 提供了回調方法,咱們能夠增長更多的自定義信息,如運行時 Log、trace、dumpsys meminfo 等信息,以輔助分析定位問題。
這裏的白名單是爲了處理一些系統引發的泄漏問題,以及一些由於 業務邏輯要開後門的情形而設置 的。分析時若是碰到白名單上標識的類,則不對這個泄漏作後續的處理。兩者的配置差別有以下兩點:
1)、LeakInspector 的白名單以 XML 配置的形式存放在服務器上。
1)、而LeakCanary的白名單是直接寫死在其源碼的AndroidExcludedRefs類裏。
2)、LeakCanary 的系統白名單裏定義的類比 LeakInspector 中定義的多不少,由於它沒有自動修復系統泄漏功能。
針對系統泄漏,LeakInspector 經過 反射自動修復 了目前碰到的一些系統泄漏,只要在 onDestory 裏面 調用 一個修復系統泄漏的方法便可。而 LeakCanary 雖然能識別系統泄漏,可是它僅僅對該類問題給出了分析,沒有提供實際可用的解決方案。
若是檢測到發生了內存泄漏,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);
}
複製代碼
採集 dump 以後,LeakInspector 會上傳 dump 文件,並* 調用 MAT 命令行來進行分析*,獲得此次泄漏的 GC 鏈。而 LeakCanary 則用開源組件 HAHA 來分析獲得一個 GC 鏈。可是 LeakCanary 獲得的 GC 鏈包含被 hold 住的類對象,通常都不須要用 MAT 打開 Hporf 便可解決問題。而 LeakInpsector 獲得的 GC 鏈只有類名,還須要 MAT 打開 Hprof 才能具體去定位問題,不是很方便。
LeakInspector 在 dump 分析結束以後,會提交缺陷單,而且把缺陷單分配給對應類的負責人。若是發現重複的問題則更新舊單,同時具有從新打開單等狀態轉換邏輯。而 LeakCanary 僅會在通知欄提醒用戶,須要用戶本身記錄該問題並作後續處理。
LeakInspector 跟自動化測試能夠無縫結合,當自動化腳本執行中發現內存泄漏,能夠由它採集 dump 併發送到服務進行分析,最後提單,整個流程是不須要人力介入的。而 LeakCanary 則把分析結果經過通知欄告知用戶,須要人工介入才能進入下一個流程。
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 界面,以下圖所示:
jHat 還有兩個比較重要的功能,分別以下所示:
打開 127.0.0.1:7000/histo/,統計表界面以下所示:
能夠到,按 Total Size 降序 排列了全部的 Class,而且,咱們還能夠查看到每個 Class 與之對應的實例數量。
OQL 是一種模仿 SQL 語句的查詢語句,一般用來查詢某個類的實例數量,打開 127.0.0.1:7000/oql/ 並輸入 java.lang.String 查詢 String 實例的數量,結果以下圖所示:
JHat 比 MAT 更加靈活,且符合大型團隊安裝簡單、團隊協做的需求。可是,並不適合中小型高效溝通型團隊使用。
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 類型有以下三種:
GC 採集的方法有以下四種:
經過 GC 日誌,咱們能夠知道 GC 的量和 它對卡頓的影響,也能夠 初步定位一些如主動調用GC、可分配的內存不足、過多使用Weak Reference 等問題。
對於 HTML5 頁面而言,抓取 JavaScript 的內存須要使用 Chrome Devtools 來進行遠程調試。方式有以下兩種:
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 中幾種常見的內存問題點:
若想更深刻地學習 Chrome 開發者工具的使用方法,請查看 《Chrome開發者工具中文手冊》。
在咱們進行內存優化的過程當中,有許多內存問題均可以歸結爲一類問題,爲了便於之後快速地解決相似的內存問題,我將它們歸結成了如下的多個要點:
說道內類就不得不提到 」this$0「,它是一種奇特的內類成員,每一個類實例都具備一個 this$0,當它的內類須要訪問它的成員時,內類就會持有外類的 this$0,經過 this$0 就能夠訪問外部類全部的成員。
解決方案是在 Activity 關閉,即觸發 onDestory 時解除內類和外部的引用關係。
這也是一個 this$0 間接引用的問題,對於 Handler 的解決方案通常能夠歸結爲以下三個步驟:
這裏須要在使用過程當中注意對 WeakReference 進行判空。
若是在閃屏頁跳轉到登陸界面時沒有調用 finish(),則會形成閃屏頁的內存泄漏,在碰到這種」過渡界面「的狀況時,須要注意不要產生這樣的內存 Bug。
咱們一般都會使用 getSystemService 方法來獲取系統服務,可是當在 Activity 中調用時,會默認把 Activity 的 Context 傳給系統服務,在某些不肯定的狀況下,某些系統服務內部會產生異常,從而 hold 住外界傳入的 Context。
解決方案是 直接使用 Applicaiton 的 Context 去獲取系統服務。
咱們都知道,對應 WebView 來講,其 網絡延時、引擎 Session 管理、Cookies 管理、引擎內核線程、HTML5 調用系統聲音、視頻播放組件等產生的引用鏈條沒法及時打斷,形成的內存問題基本上能夠用」無解「來形容。
解決方案是咱們能夠 把 WebView 裝入另外一個進程。 具體爲在 AndroidManifes 中對當前的 Activity 設置 android:process 屬性便可,最後,在 Activity 的 onDestory 中退出進程,這樣便可基本上終結 WebView 形成的泄漏。
咱們在日常開發過程當中常常須要在Activity建立的時候去註冊一些組件,如廣播、定時器、事件總線等等。這個時候咱們應該在適當的時候對組件進行註銷,如 onPause 或 onDestory 方法中。
不只在使用 Handler 的 sendMessage 方法時,咱們須要在 onDestory 中使用 removeCallbackAndMessage 移除回調和消息,在使用到 Handler / FrameLayout 的 postDelyed 方法時,咱們須要調用 removeCallbacks 去移除實現控件內部的延時器對 Runnable 內類的持有。
在作資源適配的時候,由於須要考慮到 APK 的瘦身問題,沒法爲每張圖片在每一個 drawable / mipmap 目錄下安置一張適配圖片的副本。不少同窗不知道圖片應該放哪一個目錄,若是放到分辨率低的目錄如 hdpi 目錄,則可能會形成內存問題,這個時候建議儘可能問設計人員要高品質圖片而後往高密度目錄下方,如 xxhdpi 目錄,這樣 在低密屏上」放大倍數「是小於1的,在保證畫質的前提下,內存也是可控的。也能夠使用 Drawable.createFromSream 替換 getResources().getDrawable 來加載,這樣即可以繞過 Android 的默認適配規則。
對於已經被用戶使用物理「返回鍵」退回到後臺的進程,若是包含了如下 兩點,則 不會被輕易殺死。
但建議 在運行一段時間(如3小時)後主動保存界面進程(位於後臺),而後重啓它,這樣能夠有效地下降內存負載。
咱們應該在 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 對那些沒有立刻用到的資源去作延遲加載,而且還有不少大機率不會出現的 View 更要去作懶加載,這樣能夠等到要使用時再去爲它們分配相應的內存。
產品或者運營爲了統計數據會在每一個版本中不斷地增長新的埋點。因此咱們須要按期地去清理一些過期的埋點,以此來 適當地優化內存以及CPU的壓力。
咱們在作子線程操做的時候,喜歡使用匿名內部類 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 已經回收,任務就放棄執行。
咱們發現咱們的 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 的方案。
對於 內存優化的專項優化 而言,咱們要着重注意兩點,即 優化大方向 和 優化細節。
對於 優化的大方向,咱們應該 優先去作見效快的地方,主要有如下三部分:
對於 優化細節,咱們應該 注意一些系統屬性或內存回調的使用 等等,主要能夠細分爲以下六部分:
在這篇文章中,咱們除了創建了 內存的監控閉環 這一核心體系以外,還實現瞭如下 十大組件 / 策略:
最後,當監控到 應用內存超過閾值時,還定製了 完善的兜底策略 來 重啓應用進程。
總的來看,要創建一套 全面且成體系的內存優化及監控 是很是重要也是極具挑戰性的一項工做。而且,目前各大公司的 內存優化體系 也正處於 不斷演進的歷程 之中,其目的不外乎:實現更健全的功能、更深層次的定位問題、快速準確地發現線上問題。
路漫漫其修遠兮,吾將上下而求索
一、國內Top團隊大牛帶你玩轉Android性能分析與優化 第四章 內存優化
四、GMTC-Android內存泄漏自動化鏈路分析組件Probe.key
六、Overview of memory management
九、管理應用的內存
十、《Android移動性能實戰》第二章 內存
歡迎關注個人微信:
bcce5360
微信羣若是不能掃碼加入,麻煩你們想進微信羣的朋友們,加我微信拉你進羣。
2千人QQ羣,Awesome-Android學習交流羣,QQ羣號:959936182, 歡迎你們加入~