深入探索Android内存优化
前言
成為一名優秀的Android開發,需要一份完備的知識體系,在這里,讓我們一起成長為自己所想的那樣~。
本篇是Android內存優化的進階篇,難度會比較大,建議對內存優化不是非常熟悉的前仔細看看在前幾篇文章中,筆者曾經寫過的一篇Android性能優化之內存優化,其中詳細分析了以下幾大模塊:
Android的內存管理機制
優化內存的意義
避免內存泄漏
優化內存空間
圖片管理模塊的設計與實現
如果你對以上基礎內容都比較了解了,那么我們便開始接下來的Android內存優化探索之旅吧。
一、內存優化相關概念
Android的給每個應用進程分配的內存都是非常有限的,那么為什么不能把圖片下載來都放到磁盤中呢?那是因為放在內存中,展示會更“快”,快的原因有兩點:
硬件快:內存本身讀取、存入速度快。
復用快:解碼成果有效保存,復用時,直接使用解碼后對象,而不是再做一次圖像解碼。
這里說一下解碼的概念。Android系統要在屏幕上展示圖片的時候只認“像素緩沖”,而這也是大多數操作系統的特征。而我們常見的jpg,png等圖片格式,都是把“像素緩沖”使用不同的手段壓縮后的結果,所以這些格式的圖片,要在設備上展示,就必須經過一次解碼,它的執行速度會受圖片壓縮比、尺寸等因素影響。(官方建議:把從內存淘汰的圖片,降低壓縮比存儲到本地,以備后用,這樣可以最大限度地降低以后復用時的解碼開銷。)
接下來,我們來了解一下內存優化的一些重要概念。
手機RAM:
手機不使用PC的DDR內存,采用的是LPDDR RAM,即”低功耗雙倍數據速率內存“。
LPDDR系列的帶寬 = 時鐘頻率 ??內存總線位數 / 8
LPDDR4 = 1600MHZ ??64 / 8 ??雙倍速率 = 25.6GB/s。
那么內存占用是否越少越好?
當系統內存充足的時候,我們可以多用一些獲得更好的性能。當系統內存不足的時候,希望可以做到”用時分配,及時釋放“。
內存優化的緯度
對于Android內存優化來說又可以細分為兩個維度:
1、RAM優化
主要是降低運行時內存。它的目的如下:
防止應用發生OOM。
降低應用由于內存過大被LMK機制殺死的概率。
避免不合理使用內存導致GC次數增多,從而導致應用發生卡頓。
2、ROM優化
降低應用占ROM的體積。APK瘦身。它的目的為:
降低應用占用空間,避免因ROM空間不足導致程序無法安裝
內存問題
那么,內存問題主要是有哪幾類呢?下面我來一一敘述:
1、內存抖動
內存波動圖形呈鋸齒張、GC導致卡頓。
這個問題在Dalvik虛擬機上會更加明顯,而ART虛擬機在內存管理跟回收策略上都做了大量優化,內存分配和GC效率相比提升了5~10倍。
2、內存泄漏
對象被持有導致無法釋放或不能按照對象正常的生命周期進行釋放。
可用內存減少、頻繁GC,容易導致內存泄漏。
3、內存溢出
OOM、程序異常。
二、常見工具選擇
在內存優化的上一篇我們已經介紹過了相關的工具,這里再簡單回憶一下。
1、Memory Profiler
它的作用如下:
實時圖表展示應用內存使用量
識別內存泄漏、抖動等
提供捕獲堆轉儲、強制GC以及根據內存分配的能力
它的優點即:
方便直觀
線下使用
2、Memory Analyzer
強大的Java Heap分析工具,查找內存泄漏及內存占用
生成整體報告、分析問題等。建議線下深入使用。
3、LeakCanary
自動內存泄漏檢測神器。僅用于線下集成。
它的缺點比較明顯,雖然使用了idleHandler與多進程,但是dumphprof的SuspendAll Thread的特性依然會導致應用卡頓。
在三星等手機,系統會緩存最后一個Activity,此時應該采用更嚴格的檢測模式。
4、那么如何定制線上的LeakCanary?
定制LeakCanary其實就是對haha組件來進行定制。haha庫是square出品的一款自動分析Android堆棧的java庫。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的時候主要要做2個工作:
1、在過程中加上對大對象的分析過程。
2、解決掉將hprof文件映射到內存中的時候可能內存暴漲的問題。
5、實現內存泄漏監控閉環
在實現了線上版的LeakCanary之后,就需要將線上版的LeakCanary與服務器和前端頁面結合起來。例如,當LeakCanary上發現內存泄漏時,手機將上傳內存快照至服務器,此時服務器分析Hprof,如果不是系統原因導致誤報則通過git得到該最近修改人,最后將內存泄漏bug單提交給負責人。該負責人通過前端實現的bug單系統即可看到自己新增的bug。
三、Android內存管理機制回顧
ART和Dalvik虛擬機使用分頁和內存映射來管理內存。下面我們先從Java的內存分配開始說起。
1、Java內存分配
Java的內存分配區域為如下幾部分:
方法區:主要存放靜態常量
虛擬機棧:Java變量引用
本地方法棧:native變量引用
堆:對象
程序計數器:計算當前線程的當前方法執行到多少行
2、Java內存回收算法
1、標記-清除算法
流程可簡述為兩步:
標記所有需要回收的對象
統一回收所有被標記的對象
它的優點實現比較簡單,缺點也很明顯:
標記、清除效率不高
產生大量內存碎片
2、復制算法
流程可簡述為三步:
將內存劃分為大小相等的兩塊
一塊內存用完之后復制存活對象到另一塊
清理另一塊內存
它的優點為 實現簡單,運行高效,每次僅需遍歷標記一半的內存區域。而缺點則會浪費一半空間,代價大。
3、標記-整理算法
流程可簡述為三步:
標記過程與”標記-清除算法“一樣
存活對象往一端進行移動
清理其余內存
它的優點如下:
避免標記-清除導致的內存碎片
避免復制算法的空間浪費
4、分代收集算法
現在主流的虛擬機一般用的比較多的還是分帶收集算法,它具有如下特點:
結合多種算法優勢
新生代對象存活率低,復制
老年代對象存活率高,標記-整理
3、Android內存管理機制
Android中的內存是彈性分配的,分配值與最大值受具體設備影響。
對于OOM場景其實由細分為兩種,一種是內存真正不足
了,二另一種則是可用內存不足。要注意一下這兩種的區分。
以Android中的虛擬機的角度來說,我們要清楚Dalvik與Art區別,Dalvik僅固定一種回收算法,而Art回收算法可運行期選擇,并且,Art具備內存整理能力,減少內存空洞。
最后,LMK機制(Low Memory killer)保證了進程資源的合理利用,它的實現原理主要是根據進程分類和回收收益來綜合決定的。
四、內存抖動
當內存頻繁分配和回收導致內存不穩定,就會出現內存抖動,它通常表現為 頻繁GC、內存曲線呈鋸齒狀。
它的危害也很嚴重,通常會導致頁面卡頓,甚至造成OOM。
那么為什么內存抖動會導致OOM?
主要原因有兩點:
頻繁創建對象,導致內存不足及碎片(不連續)
不連續的內存片無法被分配,導致OOM
內存抖動解決實戰
點擊按鈕使用handler發送一個空消息,handler的handleMessage接收到消息后創建內存抖動:即在for循環創建100個容量為10萬的strings數組并在30ms后繼續發送空消息。
一般使用Memory Profiler或CPU Profiler結合代碼排查即可找到內存抖動出現的地方。
通常的技巧就是著重查看循環或頻繁調用的地方。
下面列舉一些導致內存抖動的常見案例:
1、字符串使用加號拼接:
使用StringBuilder替代。
初始化時設置容量,減少StringBuilder的擴容。
2、資源復用
使用全局緩存池,以重用頻繁申請和釋放的對象。
注意結束使用后,需要手動釋放對象池中的對象。
3、減少不合理的對象創建
ondraw、getView中對象的創建盡量進行復用。
避免在循環中不斷創建局部變量。
4、使用合理的數據結構
使用SparseArray類族來替代HashMap。
五、內存優化體系搭建
在開始我們今天正式的主題之前,我們先來回歸一下內存泄漏的概念與解決技巧。
所謂的內存泄漏就是內存中存在已經沒有用的對象。它的表現一般為 內存抖動、可用內存逐漸減少。
它的危害即會導致內存不足、GC頻繁、OOM。
內存泄漏的分析一般可簡述為兩步:
1、使用Memory Profiler初步觀察。
2、通過Memory Analyzer結合代碼確認。
1、MAT回顧
MAT查找內存泄漏
首先找到當前Activity,在Histogram中選擇其List Objects中的 with incoming reference(哪些強引用引向了我),然后選擇當前的一個Path to GC Roots/Merge to GC Roots的exclude All 弱軟虛引用。最后找到最后的泄漏對象在左下角下會有一個小圓圈。
MAT的關鍵使用細節
要全面掌握MAT的用法,必須先了解下面的一些細節:
善于使用Regex查找對應泄漏類。
使用group by package查找對應包下的具體類。
其次,要明白with outgoing references和with incoming references的區別。
with outgoing references為它引用了哪些對象,with incoming references為哪些對象引用了它。
還需要了解Shallow Heap和Retained Heap的區別。
Shallow Heap為對象自身占用的內存,而Retained Heap則還包含對象引用的對象所占用的內存。
除此之外,MAT共有5個關鍵組件幫助我們去分析內存方面的問題,他們分別是Dominator_tree
、Histogram、thread_overview、Top Consumers、Leak Suspects。下面我們簡單地了解一下它們。
Dominator(支配者):
如果從GC Root到達對象A的路徑上必須經過對象B,那么B就是A的支配者。
Histogram和dominator_tree的區別:
Histogram顯示Shallow Heap、Retained Heap、Objects,而dominator_tree顯示的是Shallow Heap、Retained Heap、Percentage。
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可以查看其產生內存泄漏的引用鏈。
最后,我列舉一些內存泄漏優化的技巧:
1、使用類似Hack的方式修復系統內存泄漏:
LeakCanary的AndroidExcludeRefs列出了一些由于系統原因導致引用無法釋放的例子,可使用類似Hack的方式去修復。
2、Activity的兜底內存回收策略:
在Activity的onDestory中遞歸釋放其引用到的Bitmap、DrawingCache等資源,降低發生內存泄漏對應用內存的壓力。
2、建立線上內存泄漏監控組件:使用定制化的LeakCanary
在線上也可以使用類似LeakCanary的自動化檢測方案,但是需要對生成的Hprof內存快照文件做一些優化,裁剪大部分圖片對應的byte數據以減少文件開銷,最后使用7zip壓縮,一般可節省90%大小。
3、建立線上OOM監控組件:Probe
美團Android內存泄漏自動化鏈路分析組件Probe
在OOM時生成Hprof內存快照,然后通過單獨進程對這個文件做進一步分析。
它的缺點比較多,具體為如下幾點:
在崩潰的時候生成內存快照容易導致二次崩潰。
部分手機生成Hprof快照比較耗時。
部分OOM是由虛擬內存不足導致。
在實現自動化鏈路分析組件Probe的過程中主要要解決如下問題:
1、鏈路分析時間過長
使用鏈路歸并,將具有相同層級與結構的鏈路進行合并。
使用自適應擴容法,通過不斷比較現有鏈路和新鏈路,結合擴容因子,逐漸完善為完整的泄漏鏈路。
2、分析進程占用內存過大
分析進程占用的內存跟內存快照文件的大小不成正相關,而跟內存快照文件的Instance數量呈正相關。所以應該盡可能排除不需要的Instance實例。
Prope分析流程
1、hprof 映射到內存 -> 解析成Snapshot & 計數壓縮:
解析后的Snapshot中的Heap有四種類型,具體為:
DefaultHeap
ImageHeap
App Heap:包括ClassInstance、ClassObj、ArrayInstance、RootObj。
System Heap
解析完后使用了計數壓縮策略,對相同的Instance使用計數,以減少占用內存。超過計數閾值的需要計入計數桶(計數桶記錄了丟棄個數和每個Instance的大小)。
2、生成Dominator Tree。
3、計算RetainSize。
4、生成Reference鏈 & 基礎數據類型增強:
如果對象是基礎數據類型,會將自身的RetainSize累加到父節點上,將懷疑對象替換為它的父節點。
5、鏈路歸并。
6、計數桶補償 & 基礎數據類型和父節點融合:
使用計數補償策略計算RetainSize,主要是判斷對象是否在計數桶中,如果在的話則將丟棄的個數和大小補償到對象上,累積計算RetainSize,最后對RetainSize排序以查找可疑對象。
7、排序擴容。
8、查找泄露鏈路。
總體架構圖如下:
image
4、實現單機版的Profile - Memory自動化內存分析
項目地址點擊此處
在配置的時候要注意兩個問題:
1、liballoc-lib.so在構建后工程的build->intermediates->cmake目錄下。將對應的cpu abi目錄拷貝到新建的libs目錄下。
2、在DumpPrinter Java庫的build.gradle中的jar閉包中需要加入以下代碼以識別源碼路徑
sourceSets.main.java.srcDirs = [‘src’]
具體的使用步驟如下:
1、點擊”開
始記錄“按鈕可以看到觸發對象分配的記錄,說明對象已經開始記錄對象的分配。
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)
5、圖片監控體系搭建
在介紹圖片監控體系的搭建之前,首先我們來回顧下Android Bitmap內存分配的變化:
在Android 3.0之前
Bitmap對象存放在Java Heap,而像素數據是存放在Native內存中的。
如果不手動調用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及之后
使用了能夠輔助回收Native內存的NativeAllocationRegistry,以實現將像素數據放到Native內存中,并且可以和Bitmap對象一起快速釋放,最后,在GC的時候還可以考慮這些Bitmap內存以防止被濫用。
Android 8.0為了解決圖片內存占用過多和圖像繪制效率過慢的問題新增了硬件位圖Hardware Bitmap。
那么,如何將圖片內存存放在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、設備分級
內存優化首先需要根據設備環境來綜合考慮,讓高端設備使用更多的內存,做到針對設備性能的好壞使用不同的內存分配和回收策略。
使用類似device-year-class的策略對設備進行分級,對于低端機用戶可以關閉復雜的動畫或”重功能“,使用565格式的圖片或更小的緩存內存等。
業務開發人員需要考慮功能是否對低端機開啟,在系統資源不夠時主動去做降級處理。
2、建立統一的緩存管理組件
建立統一的緩存管理組件,合理使用OnTrimMemory回調,根據系統不同的狀態去釋放相應的內存。
在實現過程中,需要解決使用static LRUCache來緩存大尺寸Bitmap等問題。
并且,在通過實際的測試后,發現onTrimMemory的ComponetnCallbacks2.TRIM_MEMORY_COMPLETE并不等價于onLowMemory,因此建議仍然要去監聽onLowMemory回調。
3、低端機避免使用多進程
一個空進程也會占用10MB內存,低端機應該盡可能減少使用多進程。
針對低端機用戶可以推出4MB的輕量級版本,如今日頭條極速版、Facebook Lite。
4、統一圖片庫
需要收攏圖片的調用,避免使用Bitmap.createBitmap、BitmapFactory相關的接口創建Bitmap,應該使用自己的圖片框架。
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、在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…”);
}
}
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);
// 這里找到所有通過ImageView的setImageBitmap方法設置的切入點,
// 其中最后一個參數ImageHook對象是繼承了XC_MethodHook類以便于
// 重寫afterHookedMethod方法拿到相應的參數進行監控邏輯的判斷
DexposedBridge.findAndHookMethod(ImageView.class, “setImageBitmap”, Bitmap.class, new ImageHook());
}
});
接下來,我們來實現我們的ImageHook類,如下所示:
public class ImageHook extends XC_MethodHook {
@Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {super.afterHookedMethod(param);// 實現我們的邏輯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() {@Overridepublic 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); }}
在上面,我們重寫了ImageHook的afterHookedMethod方法,拿到了當前的ImageView和要設置的Bitmap對象,如果當前ImageView的寬高大于0,我們便進行大圖檢測的處理:ImageView的寬高都大于View的2倍以上,則警告,如果當前ImageView的寬高等于0,則說明ImageView還沒有進行繪制,則使用ImageView的ViewTreeObserer獲取其寬高進行大圖檢測的處理。至此,我們的大圖檢測檢測組件就實現了。
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有哪些特點:
創建場景比較單一:在Java層調用Bitmap.create或BitmapFactory等方法創建,可以封裝一層對Bitmap創建的接口,注意要包含調用外部庫產生的Bitmap。
創建頻率比較低。
和Java對象的生命周期一樣服從GC,可以使用WeakReference來追蹤Bitmap的銷毀。
根據以上特點,我們可以建立一套Bitmap的高性價比監控組件:
1、首先,在接口層將所有創建出來的Bitmap放入一個WeakHashMap中,并記錄創建Bitmap的數據、堆棧等信息,然后每隔一定時間查看WeakHashMap中有哪些Bitmap仍然存活來判斷是否出現Bitmap濫用或泄漏。
2、這個方案性能消耗很低,可以在正式環境中進行。注意正式與測試環境需要采用不同程度的監控。
6、建立全局的線程監控組件
每個線程初始化都需要mmap一定的棧大小,在默認情況下初始化一個線程需要mmap 1MB左右的內存空間,在32bit的應用中有4g的vmsize,實際能使用的有3g+,這樣一個進程最大能創建的線程數可以達到3000個,但是linux對每個進程可創建的線程數也有一定的限制(/proc/pid/limits),并且不同廠商也能修改這個限制,超過該限制就會OOM。
對線程數量的限制,一定程度上可以避免OOM的發生。
線程監控組件的實現原理
在線下或灰度的環境下通過一個定時器每隔10分鐘dump出應用所有的線程相關信息,當線程數超過當前閾值時,將當前的線程信息上報并預警。
7、建立線上應用內存監控體系
具體的相關數據獲取方式如下:
1、首先,ActivityManager的getProcessMemoryInfo -> Debug.MemoryInfo數據。
2、通過hook Debug.MemoryInfo的getMemoryStat方法(os v23及以上)可以獲得Memory Profiler中的多項數據,進而獲得細分內存使用情況。
3、通過Runtime獲取DalvikHeap。
4、通過Debug.getNativeHeapAllocatedSize獲取NativeHeap。
對于監控場景,需要劃分為兩大類:
1、常規內存監控
根據斐波那契數列每隔一段時間(max:30min)獲取內存的使用情況。內存監控方法有多種實現方式,我們先來介紹幾種常規方式。
針對場景進行線上Dump內存的方式:
具體使用Debug.dumpHprofData()實現。
其實現的流程為:
1、超過最大內存的80%
2、內存Dump
3、回傳文件
4、MAT手動分析
但是有如下缺點:
1、Dump文件太大,和對象數正相關,可以進行裁剪。
2、上傳失敗率高,分析困難。
LeakCanary帶到線上的方式:
預設泄漏懷疑點,一旦發現泄漏進行回傳。但這種實現方式缺點比較明顯:
不適合所有情況,需要預設懷疑點。
分析比較耗時,容易導致OOM。
定制LeakCanary方式
定制LeakCanary需要解決以上產生的一些問題,下面這里分別列出對應的解決方案:
1、預設懷疑點->自動找懷疑點。
2、分析泄漏鏈路慢->分析Retain size大的對象。
3、分析OOM->對象裁剪,不全部加載到內存。
2、低內存監控
利用onLowMemory、onTrimMemory監聽物理內存警告。
代碼設置超過虛擬內存大小最大限制的90%則直接觸發內存警告。
對于監控指標,一般為:發生頻率、發生時各項內存使用狀況、發生時App的當前場景。
并且,為了準確衡量內存性能,我們引入了內存異常率和觸頂率的指標。
內存異常率
內存UV異常率 = PSS 超過400MB的UV / 采集UV,PSS獲取:通過Debug.MemoryInfo。
如果出現新的內存使用不當或內存泄漏的場景,這個指標會有所上漲。
觸頂率
內存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和卡頓。
這里小結一下,客戶端只負責上報數據,由后臺來計算平均PSS、圖片內存、Java內存、異常率、觸頂率等指標值,這樣便可以通過版本對比來監控是否有新增內存問題。因此,建立線上監控完整方案需包含以下幾點:
待機內存、重點模塊內存、OOM率。
整體及重點模塊GC次數、GC時間。
增強的LeakCanry自動化內存泄漏分析。
低內存監控模塊的設置。
8、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的次數和耗時,因為它會暫停線程,可能導致應用發生卡頓。建議僅對重度場景使用。
9、設置內存兜底策略
設置內存兜底策略的目的,是為了在用戶無感知的情況下,在接近觸發系統異常前,選擇合適的場景殺死進程并將其重啟,從而使得應用內存占用回到正常情況。
一般進行執行內存兜底策略時需要滿足以下條件:
是否在主界面退到后臺且位于后臺時間超過30min。
當前時間為早上2~5點。
不存在前臺服務(通知欄、音樂播放欄等情況)。
java heap必須大于當前進程最大可分配的85% || native內存大于800MB
vmsize超過了4G(32bit)的85%。
非大量的流量消耗(不超過1M/min) && 進程無大量CPU調度情況。
滿足以上條件則殺死當前主進程并通過push進程重新拉起及初始化。
10、內存優化的一些策略
下面列舉一些我在內存優化過程中常用的一些策略。
1、使bitmap資源在native中分配:
對于Android 2.x系統,使用反射將BitmapFactory.Options里面隱藏的inNativeAlloc打開。
對于Android 4.x系統,使用Fresco將bitmap資源在native中分配。
2、使用inSampleSize避免不必要的大圖加載。
3、使用Glide、Fresco等圖片加載庫,通過定制,在加載bitmap時,若發生OOM,則使用try catch將其捕獲,然后清除圖片cache,嘗試降低bitmap format(ARGB8888、RGB565、ARGB4444、ALPHA8)。
4、前臺每隔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、應用發生OOM時,需要上傳更加詳細的內存相關信息。
7、當應用使用的Service不再使用時應該銷毀它,建議使用IntentServcie。
8、當UI隱藏時釋放內存
當用戶切換到其它應用并且你的應用UI不再可見時,應該釋放應用UI所占用的所有內存資源。這能夠顯著增加系統緩存進程的能力,能夠提升用戶體驗。
在所有UI組件都隱藏的時候會接收到Activity的onTrimMemory()回調并帶有參數TRIM_MEMORY_UI_HIDDEN。
9、謹慎使用第三方庫,避免為了使用其中一兩個功能而導入一個大而全的解決方案。
六、線下Native內存泄漏監控搭建
在Android 8.0之后,可以使用Address Sanitizer、Malloc調試和Malloc鉤子進行native內存分析,參見native_memory
對于線下Native內存泄漏監控的建立,主要針對是否能重編so的情況來進行記錄分配的內存信息。
針對無法重編so的情況
使用PLT Hook攔截庫的內存分配函數,然后重定向到我們自己的實現后去記錄分配的內存地址、大小、來源so庫路徑等信息。
定期掃描分配與釋放釋放配對,對于不配對的分配輸出上述記錄的信息。
針對可重編的so情況
通過GCC的”-finstrument-functions“參數給所有函數插樁,然后在樁中模擬調用棧的入棧與出棧操作。
通過ld的”–warp“參數攔截內存分配和釋放函數,重定向到我們自己的實現后記錄分配的內存地址、大小、來源so以及插樁調用棧此刻的內容。
定期掃描分配與釋放是否配對,對于不配對的分配輸出我們記錄的信息。
七、內存優化演進
1、自動化測試階段
內存達到閾值后自動觸發Hprof Dump,將得到的Hprof存檔后由人工通過MAT進行分析。
2、LeakCanary
檢測和分析報告都在一起,批量自動化測試和事后分析不太方便。
3、使用基于LeakCannary的改進版ResourceCanary
它的主要特點如下:
1、分離檢測和分析兩部分流程
自動化測試由測試平臺進行,分析則由監控平臺的服務端離線完成,再通知相關開發解決問題。
2、裁剪Hprof文件,以降低后臺存儲Hprof的開銷
獲取需要的類和對象相關的字符串信息即可,其它數據都可以在客戶端裁剪,一般能Hprof大小會減小至原來的1/10左右。
小結
在研發階段需要不斷實現更多的工具和組件,以此系統化地提升自動化程度,以最終提升發現問題的效率。
八、內存優化工具
除了常用的內存分析工具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個。
第二行,內存狀態,具體信息如下:
5847124k total — 物理內存總量(5.8GB)
5758016k used — 使用中的內存總量(5.7GB)
89108k free — 空閑內存總量(89MB)
112428k buffers — 緩存的內存量 (112M)
第三行,swap交換分區信息,具體信息說明如下:
2621436k total — 交換區總量(2.6GB)
612572k used — 使用的交換區總量(612MB)
2008864k free — 空閑交換區總量(2GB)
2657696k cached — 緩沖的交換區總量(2.6GB)
第四行,cpu狀態信息,具體屬性說明如下:
800%cpu - 8核CPU。
39%user - 39%CPU被用戶進程使用。
0%nice - 優先值為負的進程占0%。
42%sys — 內核空間占用CPU的百分比為42%。
712%idle - 除IO等待時間以外的其它等待時間為712%。
0%iow - IO等待時間占0%。
0%irq - 硬中斷時間占0%。
6%sirq - 軟中斷時間占0%。
對于內存監控,在top里我們要時刻監控第三行swap交換分區的used,如果這個數值在不斷的變化,說明內核在不斷進行內存和swap的數據交換,這是真正的內存不夠用了。
在第五行及以下,就是各進程(任務)的狀態監控,項目列信息說明如下:
PID — 進程id。
USER — 進程所有者。
PR — 進程優先級。
NI — nice值。負值表示高優先級,正值表示低優先級。
VIRT — 進程使用的虛擬內存總量。VIRT = SWAP + RES。
RES — 進程使用的、未被換出的物理內存大小。RES = CODE + DATA。
SHR — 共享內存大小。
S — 進程狀態。D=不可中斷的睡眠狀態、R=運行、 S=睡眠、T=跟蹤/停止、Z=僵尸進程。
%CPU — 上次更新到現在的CPU時間占用百分比。
%MEM — 進程使用的物理內存百分比。
TIME+ — 進程使用的CPU時間總計,單位1/100秒。
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忽大忽小。
了解完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)
…
// 按內存的類別來進行劃分
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的某一進程
dumpsys meminfo --package // 輸出指定包名的進程,可能包含多個進程
這里我們輸入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
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
我們可以查看Native Heap的Heap Alloc的數值變化,它表示native的內存占用,如果持續上升,則可能有泄漏,而Dalvik Heap的Heap Alloc則表示Java層的內存占用。
2、查看Views、Activities、AppContexts數量變化情況
如果Views與Activities、AppContexts持續上升,則表明有內存泄漏的風險。
3、SQL的MEMORY_USED與PAGECACHE_OVERFLOW
SQL的MEMOERY_USED表示數據庫使用的內存,而PAGECACHE_OVERFLOW則表示溢出也使用的緩存,這個數值越小越好。
4、查看DATABASES信息
其中pgsz表示數據庫分頁大小,這里全是4KB;Lookaside(b)表示使用了多少個Lookaside的slots,可理解為內存占用的大小;而cache一欄中的 151/32/18 則分別表示分頁緩存命中次數/未命中次數/分頁緩存個數,這里的未命中次數不應該大于命中次數。
3、LeakInspector
LeakInspector是騰訊內部的使用的一站式內存泄漏解決方案,它是Android手機經過長期積累和提煉、集內存泄漏檢測、自動修復系統Bug、自動回收已泄露Activity內資源、自動分析GC鏈、白名單過濾等功能于一體,并深度對接研發流程、自動分析責任人并提缺陷單的全鏈路體系。
那么LeakInspector與LeakCanary又有什么不同呢?
它們之間主要有四個方面的不同:
一、檢測能力與原理方面不同
1、檢測能力
它們都支持對Activity、Fragment及其它自定義類的泄漏檢測,但是LeakInspector還增加了Btiamp的檢測能力:
檢測有沒有在View上decode超過該View尺寸的圖片,若有則上報出現問題的Activity及與其對應的View id,并記錄它的個數與平均占用內存的大小。
檢測圖片尺寸是否超過所有手機屏幕大小,違規則報警。
這一個部分的實現原理,主要是采用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配置的形式存放在服務器上。
優點:跟產品甚至不同版本的應用綁定,我們可以很方便地修改相應的配置。
缺點:白名單里的類不區分系統版本一刀切。
而LeakCanary的白名單是直接寫死在其源碼的AndroidExcludedRefs類里。
優點:定義非常詳細,并區分系統版本。
缺點:每次修改必定得重新編譯。
(2)LeakCanary的系統白名單里定義的類比LeakInspector中定義的多很多,因為它沒有自動修復系統泄漏功能。
3、自動修復系統泄漏
針對系統泄漏,LeakInspector通過反射自動修復了目前碰到的一些系統泄漏,只要在onDestory里面調研一個修復系統泄漏的方法即可。而LeakCanary雖然能識別系統泄漏,但是它僅僅對該類問題給出了分析,沒有提供實際可用的解決方案。
4、回收資源
如果檢測到發生了內存泄漏,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);
}
}
這里以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 ] [-refs ] [-port ] [-baseline ] [-debug ] [-version] [-h|-help]
For a dump file that contains multiple heap dumps,
you may specify which dump in the file
by appending “#” 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
2、OQL查詢
OQL是一種模仿SQL語句的查詢語句,通常用來查詢某個類的實例數量,打開127.0.0.1:7000/oql/并輸入java.lang.String查詢String實例的數量,如下所示:
image
JHat比MAT更加靈活,且符合大型團隊安裝簡單、團隊協作的需求你,并不適合中小型高效溝通型團隊使用。
5、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產生的原因如下:
Concurrent、Alloc、Explicit跟Dalvik的基本一樣,這里就不重復介紹了。
NativeAlloc:Native內存分配時,比如為Bitmaps或者RenderScript分配對象, 這會導致Native內存壓力,從而觸發GC。
Background:后臺GC,觸發是為了給后面的內存申請預留更多空間。
CollectorTransition:由堆轉換引起的回收,這是運行時切換GC而引起的。收集器轉換包括將所有對象從空閑列表空間復制到碰撞指針空間(反之亦然)。當前,收集器轉換僅在以下情況下出現:在內存較小的設備上,App將進程狀態從可察覺的暫停狀態變更為可察覺的非暫停狀態(反之亦然)。
HomogeneousSpaceCompact:齊性空間壓縮是指空閑列表到壓縮的空閑列表空間,通常發生在當App已經移動到可察覺的暫停進程狀態。這樣做的主要原因是減少了內存使用并對堆內存進行碎片整理。
DisableMovingGc:不是真正的觸發GC原因,發生并發堆壓縮時,由于使用了
GetPrimitiveArrayCritical,收集會被阻塞。一般情況下,強烈建議不要使用
GetPrimitiveArrayCritical,因為它在移動收集器方面具有限制。
HeapTrim:不是觸發GC原因,但是請注意,收集會一直被阻塞,直到堆內存整理完畢。
GC類型如下:
Full:與Dalvik的FULL GC差不多。
Partial:跟Dalvik的局部GC差不多,策略時不包含Zygote Heap。
Sticky:另外一種局部中的局部GC,選擇局部的策略是上次垃圾回收后新分配的對象。
GC采集的方法如下:
mark sweep:先記錄全部對象,然后從GC ROOT開始找出間接和直接的對象并標注。利用之前記錄的全部對象和標注的對象對比,其余的對象就應該需要垃圾回收了。
concurrent mark sweep:使用mark sweep采集器的并發GC。
mark compact:在標記存活對象的時候,所有的存活對象壓縮到內存的一端,而另一端可以更加高效地被回收。
semispace:在做垃圾掃描的時候,把所有引用的對象從一個空間移到另外一個空間,然后直接GC剩余在舊空間中的對象即可。
通過GC日志,我們可以知道GC的量和它對卡頓的影響,也可以初步定位一些如主動調用GC、可分配的內存不足、過多使用Weak Reference等問題。
6、自帶防泄漏功能的線程池組件
我們在做子線程操作的時候,喜歡使用匿名內部類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已經回收,任務就放棄執行。
7、Chrome Devtool
對于HTML5頁面而言,抓取JavaScript的內存需要使用Chrome Devtools來進行遠程調試。方式有如下兩種:
直接把URL抓取出來放到Chrome里訪問。
用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中幾種常見的內存問題點:
closure閉包函數。
事件監聽。
變量作用域使用不當,全局變量的引用導致無法釋放。
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裝入另一個進程。
在AndroidManifest中對當前的Activity設置android:process屬性即可,最后再Activity的onDestory中退出進程,這樣即可基本上終結WebView造成的泄漏。
6、在適當的時候對組件進行注銷
我們在平常開發過程中經常需要在Activity創建的時候去注冊一些組件,如廣播、定時器、事件總線等等。這個時候我們應該在適當的時候對組件進行注銷,如onPause或onDestory方法中。
7、Handler/FrameLayout的postDelyed方法觸發的內存問題
不僅在使用Handler的sendMessage方法時,我們需要在onDestory中使用removeCallbackAndMessage移除回調和消息,在使用到Handlerh/FrameLayout的postDelyed方法時,我們需要調用removeCallbacks去移除實現控件內部的延時器對Runnable內類的持有。
8、圖片放錯資源目錄也會有內存問題
在做資源適配的時候,因為需要考慮到APK的瘦身問題,無法為每張圖片在每個drawable/mipmap目錄下安置一張適配圖片的副本。很多同學不知道圖片應該放哪個目錄,如果放到分辨率低的目錄如hdpi目錄,則可能會造成內存問題,這個時候建議盡量問設計人員要高品質圖片然后往高密度目錄下方,如xxhdpi目錄,這樣在低密屏上”放大倍數“是小于1的,在保證畫質的前提下,內存也是可控的。也可以使用Drawable.createFromSream替換getResources().getDrawable來加載,這樣便可以繞過Android的默認適配規則。
對于已經被用戶使用物理“返回鍵”退回到后臺的進程,如果包含了以下幾點,則不會被輕易殺死。
進程包含了服務startService,而服務本身調用了startForeground(需通過反射調用)。
主Activity沒有實現onSaveInstanceState接口。
但建議在運行一段時間(如3小時)后主動保存界面進程,然后重啟它,這樣可以有效地降低內存負載。
十、內存優化常見問題
1、你們內存優化項目的過程是怎么做的?
1、分析現狀、確認問題
我們發現我們的APP在內存方面可能存在很大的問題,第一方面的原因是我們的線上的OOM率比較高。第二點呢,我們經常會看到在我們的Android Studio的Profiler工具中內存的抖動比較頻繁。這是我一個初步的現狀,然后在我們知道了這個初步的現狀之后,進行了問題的確認,我們經過一系列的調研以及深入研究,我們最終發現我們的項目中存在以下幾點大問題,比如說:內存抖動、內存溢出、內存泄漏,還有我們的Bitmap使用非常粗獷。
2、針對性優化
比如內存抖動的解決 -> Memory Profiler工具的使用(呈現了鋸齒張圖形) -> 分析到具體代碼存在的問題(頻繁被調用的方法中出現了日志字符串的拼接),也可以說說內存泄漏或內存溢出的解決。
3、效率提升
為了不增加業務同學的工作量,我們使用了一些工具類或ARTHook這樣的大圖檢測方案,沒有任何的侵入性,同時,我們將這些技術教給了大家,然后讓大家一起進行工作效率上的提升。
我們對內存優化工具Memory Profiler、MAT的使用比較熟悉,因此針對一系列不同問題的情況,我們寫了一系列解決方案的文檔,分享給大家。這樣,我們整個團隊成員的內存優化意識就變強了。
2、你做了內存優化最大的感受是什么?
1、磨刀不誤砍柴工
我們一開始并沒有直接去分析項目中代碼哪些地方存在內存問題,而是先去學習了Google官方的一些文檔,比如說學習了Memory Profiler工具的使用、學習了MAT工具的使用,在我們將這些工具學習熟練之后,當在我們的項目中遇到內存問題時,我們就能夠很快地進行排查定位問題進行解決。
2、技術優化必須結合業務代碼
一開始,我們做了整體APP運行階段的一個內存上報,然后,我們在一些重點的內存消耗模塊進行了一些監控,但是后面發現這些監控并沒有緊密地結合我們的業務代碼,比如說在梳理完項目之后,發現我們項目中存在使用多個圖片庫的情況,多個圖片庫的內存緩存肯定是不公用的,所以導致我們整個項目的內存使用量非常高。所以進行技術優化時必須結合我們的業務代碼。
3、系統化完善解決方案
我們在做內存優化的過程中,不僅做了Android端的優化工作,還將我們Android端一些數據的采集上報到了我們的服務器,然后傳到我們的后臺,這樣,方便我們的無論是Bug跟蹤人員或者是Crash跟蹤人員進行一系列問題的解決。
3、如何檢測所有不合理的地方?
比如說大圖片的檢測,我們最初的一個方案是通過繼承ImageView,重寫它的onDraw方法來實現。但是,我們在推廣它的過程中,發現很多開發人員并不接受,因為很多ImageView之前已經寫過了,你現在讓他去替換,工作成本是比較高的。所以說,后來我們就想,有沒有一種方案可以免替換,最終我們就找到了ARTHook這樣一個Hook的方案。
十一、總結
對于內存優化的專項優化而言,我們要著重注意兩點,即優化大方向和優化細節。
對于優化的大方向,我們應該優先去做見效快的地方,主要有以下三部分:
1、內存泄漏
2、內存抖動
3、Bitmap
對于優化細節,我們應該注意一些系統屬性或內存回調的使用等等,如下:
1、LargeHeap屬性
2、onTrimMemory
3、使用優化過后的集合:如SparseArray
4、謹慎使用SharedPreference
5、謹慎使用外部庫
6、業務架構設計合理
在本篇文章,我們除了建立了內存的監控閉環這一核心體系之外,還是實現了以下組件:
1、實現了線下的native內存泄漏監控。
2、根據設備分級來使用不同的內存和分配回收策略。
3、針對低端機做了功能或圖片加載格式的降級處理。
4、針對緩存濫用的問題實現了統一的緩存管理組件。
5、實現了大圖監控和重復圖片的監控。
6、在前臺每隔一定時間去獲取當前應用內存占最大內存的比例,當超過設定閾值時則主動釋放應用cache。
7、當UI隱藏時釋放內存以增加系統緩存應用進程的能力。
8、高效實現了應用全局內的Bitmap監控。
9、實現了全局的線程監控
10、針對內存使用的重度場景實現了GC監控。
最后,當監控到應用內存超過閾值時,還定制了完善的兜底策略來重啟應用進程。
從性能分類的緯度來看,除了內存監控方面外,是否也同樣建立了卡頓、緩存、電量、異常流量、布局、包體積、IO、存儲相關的監控與優化體系。總的來說,要想實現更健全的功能、更深層次的定位問題、線上問題快速準確的發現,還是有很多事情可以做的。
路漫漫其修遠兮,吾將上下而求索。
參考鏈接:
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內存分析命令
總結
以上是生活随笔為你收集整理的深入探索Android内存优化的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SDCC 2017·深圳站八大不容错过的
- 下一篇: 刚装修的房子多久能住?集成墙面真的好吗?