https://zhuanlan.zhihu.com/p/133816365
四、內(nèi)存抖動(dòng)
當(dāng)?內(nèi)存頻繁分配和回收?導(dǎo)致內(nèi)存?不穩(wěn)定,就會(huì)出現(xiàn)內(nèi)存抖動(dòng),它通常表現(xiàn)為?頻繁GC、內(nèi)存曲線呈鋸齒狀。
并且,它的危害也很嚴(yán)重,通常會(huì)導(dǎo)致?頁(yè)面卡頓,甚至造成?OOM。
1、那么,為什么內(nèi)存抖動(dòng)會(huì)導(dǎo)致 OOM?
主要原因有如下兩點(diǎn):
1)、頻繁創(chuàng)建對(duì)象,導(dǎo)致內(nèi)存不足及碎片(不連續(xù))。
2)、不連續(xù)的內(nèi)存片無(wú)法被分配,導(dǎo)致OOM。
2、內(nèi)存抖動(dòng)解決實(shí)戰(zhàn)
這里我們假設(shè)有這樣一個(gè)場(chǎng)景:點(diǎn)擊按鈕使用 handler 發(fā)送一個(gè)空消息,handler 的 handleMessage 接收到消息后創(chuàng)建內(nèi)存抖動(dòng),即在 for 循環(huán)創(chuàng)建 100個(gè)容量為10萬(wàn) 的 strings 數(shù)組并在 30ms 后繼續(xù)發(fā)送空消息。
一般使用?Memory Profiler (表現(xiàn)為 頻繁GC、內(nèi)存曲線呈鋸齒狀)結(jié)合代碼排查即可找到內(nèi)存抖動(dòng)出現(xiàn)的地方。
通常的技巧就是著重查看?循環(huán)或頻繁被調(diào)用?的地方。
3、內(nèi)存抖動(dòng)常見(jiàn)案例
下面列舉一些導(dǎo)致內(nèi)存抖動(dòng)的常見(jiàn)案例,如下所示:
1、字符串使用加號(hào)拼接
1)、使用StringBuilder替代。
2)、初始化時(shí)設(shè)置容量,減少StringBuilder的擴(kuò)容。
2、資源復(fù)用
1)、使用?全局緩存池,以?重用頻繁申請(qǐng)和釋放的對(duì)象。
2)、注意?結(jié)束?使用后,需要?手動(dòng)釋放對(duì)象池中的對(duì)象。
3、減少不合理的對(duì)象創(chuàng)建
1)、ondraw、getView 中創(chuàng)建的對(duì)象盡量進(jìn)行復(fù)用。
2)、避免在循環(huán)中不斷創(chuàng)建局部變量。
4、使用合理的數(shù)據(jù)結(jié)構(gòu)
使用?SparseArray類族、ArrayMap?來(lái)替代?HashMap。
五、內(nèi)存優(yōu)化體系化搭建
在開(kāi)始我們今天正式的主題之前,我們先來(lái)回歸一下內(nèi)存泄漏的概念與解決技巧。
所謂的內(nèi)存泄漏就是?內(nèi)存中存在已經(jīng)沒(méi)有用的對(duì)象。它的?表現(xiàn)?一般為?內(nèi)存抖動(dòng)、可用內(nèi)存逐漸減少。 它的?危害?即會(huì)導(dǎo)致?內(nèi)存不足、GC頻繁、OOM。
而對(duì)于?內(nèi)存泄漏的分析?一般可簡(jiǎn)述為如下?兩步:
1)、使用 Memory Profiler 初步觀察。
2)、通過(guò) Memory Analyzer 結(jié)合代碼確認(rèn)。
1、MAT回顧
MAT查找內(nèi)存泄漏
對(duì)于MAT來(lái)說(shuō),其常規(guī)的查找內(nèi)存泄漏的方式可以細(xì)分為如下三步:
1)、首先,找到當(dāng)前 Activity,在 Histogram 中選擇其 List Objects 中的 with incoming reference(哪些引用引向了我)。
2)、然后,選擇當(dāng)前的一個(gè) Path to GC Roots/Merge to GC Roots 的 exclude All 弱軟虛引用。
3)、最后,找到的泄漏對(duì)象在左下角下會(huì)有一個(gè)小圓圈。
此外,在?Android性能優(yōu)化之內(nèi)存優(yōu)化?還有幾種進(jìn)階的使用方式,這里就不一一贅述了,下面,我們來(lái)看看關(guān)于 MAT 使用時(shí)的一些關(guān)鍵細(xì)節(jié)。
MAT的關(guān)鍵使用細(xì)節(jié)
要全面掌握MAT的用法,必須要先了解?隱藏在 MAT 使用中的四大細(xì)節(jié),如下所示:
1)、善于使用 Regex 查找對(duì)應(yīng)泄漏類。
2)、使用 group by package 查找對(duì)應(yīng)包下的具體類。
3)、明白 with outgoing references 和 with incoming references 的區(qū)別。
with outgoing references:它引用了哪些對(duì)象。
with incoming references:哪些對(duì)象引用了它。
4)、了解 Shallow Heap 和 Retained Heap 的區(qū)別。
Shallow Heap:表示對(duì)象自身占用的內(nèi)存。
Retained Heap:對(duì)象自身占用的內(nèi)存 + 對(duì)象引用的對(duì)象所占用的內(nèi)存。
MAT 關(guān)鍵組件回顧
除此之外,MAT 共有?5個(gè)關(guān)鍵組件?幫助我們?nèi)シ治鰞?nèi)存方面的問(wèn)題,分別如下所示:
1)、Dominator_tree
2)、Histogram
3)、thread_overview
4)、Top Consumers
5)、Leak Suspects
下面我們這里再簡(jiǎn)單地回顧一下它們。
1、Dominator(支配者):
如果從GC Root到達(dá)對(duì)象A的路徑上必須經(jīng)過(guò)對(duì)象B,那么B就是A的支配者。
2、Histogram和dominator_tree的區(qū)別:
1)、Histogram 顯示 Shallow Heap、Retained Heap、Objects,而 dominator_tree 顯示的是 Shallow Heap、Retained Heap、Percentage。
2)、Histogram 基于?類?的角度,dominator_tree是基于?實(shí)例?的角度。Histogram 不會(huì)具體顯示每一個(gè)泄漏的對(duì)象,而dominator_tree會(huì)。
3、thread_overview
查看?線程數(shù)量?和?線程的 Shallow Heap、Retained Heap、Context Class Loader 與 is Daemon。
4、Top Consumers
通過(guò)?圖形?的形式列出?占用內(nèi)存比較多的對(duì)象。
在下方的?Biggest Objects?還可以查看其?相對(duì)比較詳細(xì)的信息,例如?Shallow Heap、Retained Heap。
5、Leak Suspects
列出有內(nèi)存泄漏的地方,點(diǎn)擊 Details 可以查看其產(chǎn)生內(nèi)存泄漏的引用鏈。
2、搭建體系化的圖片優(yōu)化 / 監(jiān)控機(jī)制
在介紹圖片監(jiān)控體系的搭建之前,首先我們來(lái)回顧下?Android Bitmap 內(nèi)存分配的變化。
Android Bitmap 內(nèi)存分配的變化
在Android 3.0之前
1)、Bitmap 對(duì)象存放在 Java Heap,而像素?cái)?shù)據(jù)是存放在 Native 內(nèi)存中的。
2)、如果不手動(dòng)調(diào)用 recycle,Bitmap Native 內(nèi)存的回收完全依賴 finalize 函數(shù)回調(diào),但是回調(diào)時(shí)機(jī)是不可控的。
Android 3.0 ~ Android 7.0
將?Bitmap對(duì)象?和?像素?cái)?shù)據(jù)?統(tǒng)一放到?Java Heap?中,即使不調(diào)用 recycle,Bitmap 像素?cái)?shù)據(jù)也會(huì)隨著對(duì)象一起被回收。
但是,Bitmap 全部放在 Java Heap 中的缺點(diǎn)很明顯,大致有如下兩點(diǎn):
1)、Bitmap是內(nèi)存消耗的大戶,而 Max Java Heap 一般限制為 256、512MB,Bitmap 過(guò)大過(guò)多容易導(dǎo)致 OOM。
2)、容易引起大量 GC,沒(méi)有充分利用系統(tǒng)的可用內(nèi)存。
Android 8.0及以后
1)、使用了能夠輔助回收 Native 內(nèi)存的NativeAllocationRegistry,以實(shí)現(xiàn)將像素?cái)?shù)據(jù)放到 Native 內(nèi)存中,并且可以和 Bitmap 對(duì)象一起快速釋放,最后,在 GC 的時(shí)候還可以考慮到這些 Bitmap 內(nèi)存以防止被濫用。
2)、Android 8.0 為了?解決圖片內(nèi)存占用過(guò)多和圖像繪制效率過(guò)慢?的問(wèn)題新增了?硬件位圖 Hardware Bitmap。
那么,我們?nèi)绾螌D片內(nèi)存存放在 Native 中呢?
將圖片內(nèi)存存放在Native中的步驟有?四步,如下所示:
1)、調(diào)用 libandroid_runtime.so 中的 Bitmap 構(gòu)造函數(shù),申請(qǐng)一張空的 Native Bitmap。對(duì)于不同 Android 版本而言,這里的獲取過(guò)程都有一些差異需要適配。
2)、申請(qǐng)一張普通的 Java Bitmap。
3)、將 Java Bitmap 的內(nèi)容繪制到 Native Bitmap 中。
4)、釋放 Java Bitmap 內(nèi)存。
我們都知道的是,當(dāng)?系統(tǒng)內(nèi)存不足?的時(shí)候,LMK?會(huì)根據(jù)?OOM_adj?開(kāi)始?xì)⑦M(jìn)程,從?后臺(tái)、桌面、服務(wù)、前臺(tái),直到手機(jī)重啟。并且,如果頻繁申請(qǐng)釋放 Java Bitmap 也很容易導(dǎo)致內(nèi)存抖動(dòng)。對(duì)于這種種問(wèn)題,我們?cè)?如何評(píng)估內(nèi)存對(duì)應(yīng)用性能的影響?呢?
對(duì)此,我們可以主要從以下?兩個(gè)方面?進(jìn)行評(píng)估,如下所示:
1)、崩潰中異常退出和 OOM 的比例。
2)、低內(nèi)存設(shè)備更容易出現(xiàn)內(nèi)存不足和卡頓,需要查看應(yīng)用中用戶的手機(jī)內(nèi)存在 2GB 以下所占的比例。
對(duì)于具體的優(yōu)化策略與手段,我們可以從以下?七個(gè)方面?來(lái)搭建一套?成體系化的圖片優(yōu)化 / 監(jiān)控機(jī)制。
1、統(tǒng)一圖片庫(kù)
在項(xiàng)目中,我們需要?收攏圖片的調(diào)用,避免使用 Bitmap.createBitmap、BitmapFactory 相關(guān)的接口創(chuàng)建 Bitmap,而應(yīng)該使用自己的圖片框架。
2、設(shè)備分級(jí)優(yōu)化策略
內(nèi)存優(yōu)化首先需要根據(jù)?設(shè)備環(huán)境?來(lái)綜合考慮,讓高端設(shè)備使用更多的內(nèi)存,做到?針對(duì)設(shè)備性能的好壞使用不同的內(nèi)存分配和回收策略。
因此,我們可以使用類似?device-year-class?的策略對(duì)設(shè)備進(jìn)行分級(jí),對(duì)于低端機(jī)用戶可以關(guān)閉復(fù)雜的動(dòng)畫(huà)或”重功能“,使用565格式的圖片或更小的緩存內(nèi)存?等等。
業(yè)務(wù)開(kāi)發(fā)人員需要?考慮功能是否對(duì)低端機(jī)開(kāi)啟,在系統(tǒng)資源不夠時(shí)主動(dòng)去做降級(jí)處理。
3、建立統(tǒng)一的緩存管理組件
建立統(tǒng)一的緩存管理組件(參考?ACache),并合理使用 OnTrimMemory / LowMemory 回調(diào),根據(jù)系統(tǒng)不同的狀態(tài)去釋放相應(yīng)的緩存與內(nèi)存。
在實(shí)現(xiàn)過(guò)程中,需要?解決使用 static LRUCache 來(lái)緩存大尺寸 Bitmap 的問(wèn)題。
并且,在通過(guò)實(shí)際的測(cè)試后,發(fā)現(xiàn)?onTrimMemory 的 ComponetnCallbacks2.TRIM_MEMORY_COMPLETE 并不等價(jià)于 onLowMemory,因此建議仍然要去監(jiān)聽(tīng) onLowMemory 回調(diào)。
4、低端機(jī)避免使用多進(jìn)程
一個(gè)?空進(jìn)程?也會(huì)占用?10MB?內(nèi)存,低端機(jī)應(yīng)該盡可能減少使用多進(jìn)程。
針對(duì)低端機(jī)用戶可以推出?4MB 的輕量級(jí)版本,如今日頭條極速版、Facebook Lite。
5、線下大圖片檢測(cè)
在開(kāi)發(fā)過(guò)程中,如果檢測(cè)到不合規(guī)的圖片使用(如圖片寬度超過(guò)View的寬度甚至屏幕寬度),應(yīng)該立刻提示圖片所在的Activity和堆棧,讓開(kāi)發(fā)人員更快發(fā)現(xiàn)并解決問(wèn)題。在灰度和線上環(huán)境,可以將異常信息上報(bào)到后臺(tái),還可以計(jì)算超寬率(圖片超過(guò)屏幕大小所占圖片總數(shù)的比例)。
下面,我們介紹下如何實(shí)現(xiàn)對(duì)大圖片的檢測(cè)。
常規(guī)實(shí)現(xiàn)
繼承 ImageView,重寫(xiě)實(shí)現(xiàn)計(jì)算圖片大小。但是侵入性強(qiáng),并且不通用。
因此,這里我們介紹一種更好的方案:ARTHook。
ARTHook優(yōu)雅檢測(cè)大圖
ARTHook,即?掛鉤,用額外的代碼勾住原有的方法,以修改執(zhí)行邏輯,主要可以用于以下四個(gè)方面:
1)、AOP編程
2)、運(yùn)行時(shí)插樁
3)、性能分析
4)、安全審計(jì)
具體我們是使用?Epic?來(lái)進(jìn)行 Hook,Epic 是?一個(gè)虛擬機(jī)層面,以 Java 方法為粒度的運(yùn)行時(shí) Hook 框架。簡(jiǎn)單來(lái)說(shuō),它就是?ART 上的 Dexposed,并且它目前?支持 Android 4.0~10.0。
使用步驟
Epic通常的使用步驟為如下三個(gè)步驟:
1、在項(xiàng)目 moudle 的 build.gradle 中添加
compile 'me.weishu:epic:0.6.0'
2、繼承 XC_MethodHook,實(shí)現(xiàn) Hook 方法前后的邏輯。如?監(jiān)控Java線程的創(chuàng)建和銷毀:
class ThreadMethodHook extends XC_MethodHook{
? ? @Override
? ? protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
? ? ? ? super.beforeHookedMethod(param);
? ? ? ? Thread t = (Thread) param.thisObject;
? ? ? ? Log.i(TAG, "thread:" + t + ", started..");
? ? }
? ? @Override
? ? protected void afterHookedMethod(MethodHookParam param) throws Throwable {
? ? ? ? super.afterHookedMethod(param);
? ? ? ? Thread t = (Thread) param.thisObject;
? ? ? ? Log.i(TAG, "thread:" + t + ", exit..");
? ? }
}
3、注入 Hook 好的方法:
DexposedBridge.findAndHookMethod(Thread.class, "run", new ThreadMethodHook());
知道了 Epic 的基本使用方法之后,我們便可以利用它來(lái)實(shí)現(xiàn)大圖片的監(jiān)控報(bào)警了。
項(xiàng)目實(shí)戰(zhàn)
以?Awesome-WanAndroid?項(xiàng)目為例,首先,在 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處,我們?通過(guò)調(diào)用 DexposedBridge 的 findAndHookMethod 方法找到所有通過(guò) ImageView 的 setImageBitmap 方法設(shè)置的切入點(diǎn),其中最后一個(gè)參數(shù) ImageHook 對(duì)象是繼承了 XC_MethodHook 類,其目的是為了?重寫(xiě) afterHookedMethod 方法拿到相應(yīng)的參數(shù)進(jìn)行監(jiān)控邏輯的判斷。
接下來(lái),我們來(lái)實(shí)現(xiàn)我們的 ImageHook 類,代碼如下所示:
public class ImageHook extends XC_MethodHook {
? ? @Override
? ? protected void afterHookedMethod(MethodHookParam param) throws Throwable {
? ? ? ? super.afterHookedMethod(param);
? ? ? ? // 1
? ? ? ? ImageView imageView = (ImageView) param.thisObject;
? ? ? ? checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
? ? }
? ? private static void checkBitmap(Object thiz, Drawable drawable) {
? ? ? ? if (drawable instanceof BitmapDrawable && thiz instanceof View) {
? ? ? ? ? ? final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
? ? ? ? ? ? if (bitmap != null) {
? ? ? ? ? ? ? ? final View view = (View) thiz;
? ? ? ? ? ? ? ? int width = view.getWidth();
? ? ? ? ? ? ? ? int height = view.getHeight();
? ? ? ? ? ? ? ? if (width > 0 && height > 0) {
? ? ? ? ? ? ? ? ? ? // 2、圖標(biāo)寬高都大于view的2倍以上,則警告
? ? ? ? ? ? ? ? ? ? if (bitmap.getWidth() >= (width << 1)
? ? ? ? ? ? ? ? ? ? ? ? &&? bitmap.getHeight() >= (height << 1)) {
? ? ? ? ? ? ? ? ? ? warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
? ? ? ? ? ? ? ? }
? ? ? ? ? ? ? ? } else {
? ? ? ? ? ? ? ? ? ? // 3、當(dāng)寬高度等于0時(shí),說(shuō)明ImageView還沒(méi)有進(jìn)行繪制,使用ViewTreeObserver進(jìn)行大圖檢測(cè)的處理。
? ? ? ? ? ? ? ? ? ? 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處,我們重寫(xiě)了 ImageHook 的 afterHookedMethod 方法,拿到了當(dāng)前的 ImageView 和要設(shè)置的 Bitmap 對(duì)象。然后,在注釋2處,如果當(dāng)前 ImageView 的寬高大于0,我們便進(jìn)行大圖檢測(cè)的處理:ImageView 的寬高都大于 View 的2倍以上,則警告。接著,在注釋3處,如果當(dāng)前 ImageView 的寬高等于0,則說(shuō)明 ImageView 還沒(méi)有進(jìn)行繪制,則使用 ImageView 的 ViewTreeObserver 獲取其寬高進(jìn)行大圖檢測(cè)的處理。至此,我們的大圖檢測(cè)檢測(cè)組件就已經(jīng)實(shí)現(xiàn)了。如果有小伙伴對(duì)?epic?的實(shí)現(xiàn)原理感興趣的,可以查看這篇文章。
ARTHook方案實(shí)現(xiàn)小結(jié)
1)、無(wú)侵入性
2)、通用性強(qiáng)
3)、兼容性問(wèn)題大,開(kāi)源方案不能帶到線上環(huán)境。
6、線下重復(fù)圖片檢測(cè)
首先我們來(lái)了解一下這里的?重復(fù)圖片?所指的概念: 即?Bitmap 像素?cái)?shù)據(jù)完全一致,但是有多個(gè)不同的對(duì)象存在。
重復(fù)圖片檢測(cè)的原理其實(shí)就是 使用內(nèi)存 Hprof 分析工具,自動(dòng)將重復(fù) Bitmap 的圖片和引用堆棧輸出。
已完全配置好的項(xiàng)目請(qǐng)參見(jiàn)這里
使用說(shuō)明
使用非常簡(jiǎn)單,只需要修改?Main?類的?main?方法的第一行代碼,如下所示:
// 設(shè)置我們自己 App 中對(duì)應(yīng)的 hprof 文件路徑
String dumpFilePath = "http://Users//quchao//Documents//heapdump//memory-40.hprof";
然后,我們執(zhí)行?main?方法即可在?//Users//quchao//Documents//heapdump?這個(gè)路徑下看到生成的?images?文件夾,里面保存了項(xiàng)目中檢測(cè)出來(lái)的重復(fù)的圖片。images?目錄如下所示:

注意:需要使用 8.0 以下的機(jī)器,因?yàn)?8.0 及以后 Bitmap 中的 buffer 已保存在 native 內(nèi)存之中。
實(shí)現(xiàn)步驟
具體的實(shí)現(xiàn)可以細(xì)分為如下三個(gè)步驟:
1)、首先,獲取 android.graphics.Bitmap 實(shí)例對(duì)象的 mBuffer 作為 ArrayInstance ,通過(guò) getValues 獲取的數(shù)據(jù)為 Object 類型。由于后面計(jì)算 md5 需要為 byte[] 類型,所以通過(guò)反射的方式調(diào)用 ArrayInstance#asRawByteArray 直接返回 byte[] 數(shù)據(jù)。
2)、然后,根據(jù) mBuffer 的數(shù)據(jù)生成 png 圖片文件,這里直接參考了?github.com/JetBrains/a…?的實(shí)現(xiàn)方式。
3)、最后,獲取堆棧信息,直接?使用LeakCanary 獲取 stack 的方法,使用 leakcanary-analyzer-1.6.2.jar 和 leakcanary-watcher-1.6.2.jar 這兩個(gè)庫(kù)文件。并用?反射?的方式調(diào)用了?HeapAnalyzer#findLeakTrace?方法。
其中,獲取堆棧?的信息也可以直接使用?haha?庫(kù)來(lái)進(jìn)行獲取。這里簡(jiǎn)單說(shuō)一下?使用 haha 庫(kù)獲取堆棧的流程,其具體可以細(xì)分為八個(gè)步驟,如下所示:
1)、首先,預(yù)備一個(gè)已經(jīng)存在重復(fù) bitmap 的 hprof 文件。
2)、利用 haha 庫(kù)上的 MemoryMappedFileBuffer 讀取 hrpof 文件 [關(guān)鍵代碼 new MemoryMappedFileBuffer(heapDumpFile) ]。
3)、解析生成 snapshot,獲取 heap,這里我只獲取了 app heap [關(guān)鍵代碼 snapshot.getHeaps(); heap.getName().equals("app") ]。
4)、從 snapshot 中根據(jù)指定 class 查找出所有的 Bitmap Classes [關(guān)鍵代碼snapshot.findClasses(Bitmap.class.getName()) ]。
5)、從 heap 中獲得所有的 Bitmap 實(shí)例 instance [關(guān)鍵代碼 clazz.getHeapInstances(heap.getId()) ]。
6)、根據(jù) instance 中獲取所有的屬性信息 Field[],并從 Field[] 查找出我們需要的 "mWidth" "mHeight" "mBuffer" 信息。
7)、通過(guò) "mBuffer" 屬性即可獲取到他們的 hashcode 來(lái)判斷是否是重復(fù)圖片。
8)、最后,通過(guò) instance 中 mNextInstanceToGcRoot 獲取整個(gè)引用鏈信息并打印。
7、建立全局的線上 Bitmap 監(jiān)控
為了建立全局的 Bitmap 監(jiān)控,我們必須?對(duì) Bitmap 的分配和回收 進(jìn)行追蹤。我們先來(lái)看看 Bitmap 有哪些特點(diǎn):
1)、創(chuàng)建場(chǎng)景比較單一:在 Java 層調(diào)用 Bitmap.create 或 BitmapFactory 等方法創(chuàng)建,可以封裝一層對(duì) Bitmap 創(chuàng)建的接口,注意要?包含調(diào)用第三方庫(kù)產(chǎn)生的 Bitmap,這里我們具體可以使用?ASM 編譯插樁 + Gradle Transform?的方式來(lái)高效地實(shí)現(xiàn)。
2)、創(chuàng)建頻率比較低。
3)、和 Java 對(duì)象的生命周期一樣服從 GC,可以使用 WeakReference 來(lái)追蹤 Bitmap 的銷毀。
根據(jù)以上特點(diǎn),我們可以建立一套 Bitmap 的高性價(jià)比監(jiān)控組件:
1)、首先,在接口層將所有創(chuàng)建出來(lái)的 Bitmap 放入一個(gè) WeakHashMap 中,并記錄創(chuàng)建 Bitmap 的數(shù)據(jù)、堆棧等信息。
2)、然后,每隔一定時(shí)間查看 WeakHashMap 中有哪些 Bitmap 仍然存活來(lái)判斷是否出現(xiàn) Bitmap 濫用或泄漏。
3)、最后,如果發(fā)生了 Bitmap 濫用或泄露,則將相關(guān)的數(shù)據(jù)與堆棧等信息打印出來(lái)或上報(bào)至 APM 后臺(tái)。
這個(gè)方案的?性能消耗很低,可以在?正式環(huán)境?中進(jìn)行。但是,需要注意的一點(diǎn)是,正式與測(cè)試環(huán)境需要采用不同程度的監(jiān)控。
3、建立線上應(yīng)用內(nèi)存監(jiān)控體系
要建立線上應(yīng)用的內(nèi)存監(jiān)控體系,我們需要?先獲取 App 的 DalvikHeap 與 NativeHeap,它們的獲取方式可歸結(jié)為如下四個(gè)步驟:
1、首先,通過(guò) ActivityManager 的 getProcessMemoryInfo => Debug.MemoryInfo 獲取內(nèi)存信息數(shù)據(jù)。
2、然后,通過(guò)?hook Debug.MemoryInfo 的 getMemoryStat 方法(os v23 及以上)可以獲得 Memory Profiler 中的多項(xiàng)數(shù)據(jù),進(jìn)而獲得?細(xì)分內(nèi)存的使用情況。
3、接著,通過(guò) Runtime 獲取 DalvikHeap。
4、最后,通過(guò) Debug.getNativeHeapAllocatedSize 獲取 NativeHeap。
對(duì)于監(jiān)控場(chǎng)景,我們需要將其劃分為兩大類,如下所示:
1)、常規(guī)內(nèi)存監(jiān)控
2)、低內(nèi)存監(jiān)控
1、常規(guī)內(nèi)存監(jiān)控
根據(jù) 斐波那契數(shù)列 每隔一段時(shí)間(max:30min)獲取內(nèi)存的使用情況。常規(guī)內(nèi)存的監(jiān)控方法有多種實(shí)現(xiàn)方式,下面,我們按照?項(xiàng)目早期 => 壯大期 => 成熟期?的常規(guī)內(nèi)存監(jiān)控方式進(jìn)行?演進(jìn)式講解。
項(xiàng)目早期:針對(duì)場(chǎng)景進(jìn)行線上 Dump 內(nèi)存的方式
具體使用?Debug.dumpHprofData()?實(shí)現(xiàn)。
其實(shí)現(xiàn)的流程為如下四個(gè)步驟:
1)、超過(guò)最大內(nèi)存的 80%。
2)、內(nèi)存 Dump。
3)、回傳文件至服務(wù)器。
4)、MAT 手動(dòng)分析。
但是,這種方式有如下幾個(gè)缺點(diǎn):
1)、Dump文件太大,和對(duì)象數(shù)正相關(guān),可以進(jìn)行裁剪。
2)、上傳失敗率高,分析困難。
壯大期:LeakCanary帶到線上的方式
在使用 LeakCanary 的時(shí)候我們需要?預(yù)設(shè)泄漏懷疑點(diǎn),一旦發(fā)現(xiàn)泄漏進(jìn)行回傳。但這種實(shí)現(xiàn)方式缺點(diǎn)比較明顯,如下所示:
1)、不適合所有情況,需要預(yù)設(shè)懷疑點(diǎn)。
2)、分析比較耗時(shí),容易導(dǎo)致 OOM。
成熟期:定制 LeakCanary 方式
那么,如何定制線上的LeakCanary?
定制 LeakCanary 其實(shí)就是對(duì)?haha組件?來(lái)進(jìn)行?定制。haha庫(kù)是?square?出品的一款?自動(dòng)分析Android堆棧的java庫(kù)。這是haha庫(kù)的?鏈接地址。
對(duì)于haha庫(kù),它的?基本用法?一般遵循為如下四個(gè)步驟:
1、導(dǎo)出堆棧文件
File heapDumpFile = ...
Debug.dumpHprofData(heapDumpFile.getAbsolutePath());
2、根據(jù)堆棧文件創(chuàng)建出內(nèi)存映射文件緩沖區(qū)
DataBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);
3、根據(jù)文件緩存區(qū)創(chuàng)建出對(duì)應(yīng)的快照
Snapshot snapshot = Snapshot.createSnapshot(buffer);
4、從快照中獲取指定的類
ClassObj someClass = snapshot.findClass("com.example.SomeClass");
我們?cè)趯?shí)現(xiàn)線上版的LeakCanary的時(shí)候主要要解決的問(wèn)題有三個(gè),如下所示:
1)、解決 預(yù)設(shè)懷疑點(diǎn) 時(shí)不準(zhǔn)確的問(wèn)題 => 自動(dòng)找懷疑點(diǎn)。
2)、解決掉將 hprof 文件映射到內(nèi)存中的時(shí)候可能導(dǎo)致內(nèi)存暴漲甚至發(fā)生 OOM 的問(wèn)題 => 對(duì)象裁剪,不全部加載到內(nèi)存。即對(duì)生成的 Hprof 內(nèi)存快照文件做一些優(yōu)化:裁剪大部分圖片對(duì)應(yīng)的 byte 數(shù)據(jù) 以減少文件開(kāi)銷,最后,使用 7zip 壓縮,一般可 節(jié)省 90% 大小。
3)、分析泄漏鏈路慢而導(dǎo)致分析時(shí)間過(guò)長(zhǎng) => 分析 Retain size 大的對(duì)象。
成熟期:實(shí)現(xiàn)內(nèi)存泄漏監(jiān)控閉環(huán)
在實(shí)現(xiàn)了線上版的 LeakCanary 之后,就需要?將線上版的 LeakCanary 與服務(wù)器和前端頁(yè)面結(jié)合起來(lái)。具體的?內(nèi)存泄漏監(jiān)控閉環(huán)流程?如下所示:
1)、當(dāng)在線上版 LeakCanary 上發(fā)現(xiàn)內(nèi)存泄漏時(shí),手機(jī)將上傳內(nèi)存快照至服務(wù)器。
2)、此時(shí)服務(wù)器分析 Hprof,如果不是系統(tǒng)原因?qū)е抡`報(bào)則通過(guò) git 得到該最近修改人。
3)、最后將內(nèi)存泄漏 bug 單提交給負(fù)責(zé)人。該負(fù)責(zé)人通過(guò)前端實(shí)現(xiàn)的 bug 單系統(tǒng)即可看到自己新增的bug。
此外,在實(shí)現(xiàn)?圖片內(nèi)存監(jiān)控?的過(guò)程中,應(yīng)注意?兩個(gè)關(guān)鍵點(diǎn),如下所示:
1)、在線上可以按照?不同的系統(tǒng)、屏幕分辨率?等緯度去?分析圖片內(nèi)存的占用情況。
2)、在 OOM 崩潰時(shí),可以將?圖片總內(nèi)存、Top N 圖片占用內(nèi)存?寫(xiě)入?崩潰日志。
2、低內(nèi)存監(jiān)控
對(duì)于低內(nèi)存的監(jiān)控,通常有兩種方式,分別如下所示:
1、利用 onTrimMemory / onLowMemory 監(jiān)聽(tīng)系統(tǒng)回調(diào)的物理內(nèi)存警告。
2、在后臺(tái)起一個(gè)服務(wù)定時(shí)監(jiān)控系統(tǒng)的內(nèi)存占用,只要超過(guò)虛擬內(nèi)存大小最大限制的 90% 則直接觸發(fā)內(nèi)存警告。
3、內(nèi)存監(jiān)控指標(biāo)
為了準(zhǔn)確衡量?jī)?nèi)存性能,我們需要引入一系列的內(nèi)存監(jiān)控指標(biāo),如下所示:
1)、發(fā)生頻率
2)、發(fā)生時(shí)各項(xiàng)內(nèi)存使用狀況
3)、發(fā)生時(shí)App的當(dāng)前場(chǎng)景
4)、內(nèi)存異常率
內(nèi)存 UV 異常率 = PSS 超過(guò) 400MB 的 UV / 采集UV
PSS 獲?。赫{(diào)用 Debug.MemoryInfo 的 API 即可
如果出現(xiàn)?新的內(nèi)存使用不當(dāng)或內(nèi)存泄漏?的場(chǎng)景,這個(gè)指標(biāo)會(huì)有所?上漲。
5)、觸頂率
內(nèi)存 UV 觸頂率 = Java 堆占用超過(guò)最大堆限制的 85% 的 UV / 采集UV
計(jì)算觸頂率的代碼如下所示:
long javaMax = Runtime.maxMemory();
long javaTotal = Runtime.totalMemory();
long javaUsed = javaTotal - runtime.freeMemory();
float proportion = (float) javaUsed / javaMax;
如果超過(guò)?85% 最大堆?的限制,GC?會(huì)變得更加?頻發(fā),容易造成?OOM 和 卡頓。
4、小結(jié)
在具體實(shí)現(xiàn)的時(shí)候,客戶端?盡量只負(fù)責(zé)?上報(bào)數(shù)據(jù),而?指標(biāo)值的計(jì)算?可以由?后臺(tái)?來(lái)計(jì)算。這樣便可以通過(guò)?版本對(duì)比?來(lái)監(jiān)控是否有?新增內(nèi)存問(wèn)題。因此,建立線上內(nèi)存監(jiān)控的完整方案?至少需要包含以下四點(diǎn):
1)、待機(jī)內(nèi)存、重點(diǎn)模塊內(nèi)存、OOM率。
2)、整體及重點(diǎn)模塊 GC 次數(shù)、GC 時(shí)間。
3)、增強(qiáng)的 LeakCanry 自動(dòng)化內(nèi)存泄漏分析。
4)、低內(nèi)存監(jiān)控模塊的設(shè)置。
4、建立全局的線程監(jiān)控組件
每個(gè)線程初始化都需要 mmap 一定的棧大小,在默認(rèn)情況下初始化一個(gè)線程需要 mmap 1MB 左右的內(nèi)存空間。
在?32bit?的應(yīng)用中有?4g 的 vmsize,實(shí)際能使用的有?3g+,這樣一個(gè)進(jìn)程?最大能創(chuàng)建的線程數(shù)可以達(dá)到?3000個(gè),但是,linux 對(duì)每個(gè)進(jìn)程可創(chuàng)建的線程數(shù)也有一定的限制(/proc/pid/limits),并且,不同廠商也能修改這個(gè)限制,超過(guò)該限制就會(huì) OOM。
因此,對(duì)線程數(shù)量的限制,在一定程度上可以?有效地避免 OOM 的發(fā)生。那么,實(shí)現(xiàn)一套?全局的線程監(jiān)控組件?便是?刻不容緩?的了。
全局線程監(jiān)控組件的實(shí)現(xiàn)原理
在線下或灰度的環(huán)境下通過(guò)一個(gè)定時(shí)器每隔 10分鐘 dump 出應(yīng)用所有的線程相關(guān)信息,當(dāng)線程數(shù)超過(guò)當(dāng)前閾值時(shí),則將當(dāng)前的線程信息上報(bào)并預(yù)警。
5、GC 監(jiān)控組件搭建
通過(guò)** Debug.startAllocCounting** 來(lái)監(jiān)控?GC?情況,注意有一定?性能影響。
在?Android 6.0 之前?可以拿到?內(nèi)存分配次數(shù)和大小以及 GC 次數(shù),其對(duì)應(yīng)的代碼如下所示:
long allocCount = Debug.getGlobalAllocCount();
long allocSize = Debug.getGlobalAllocSize();
long gcCount = Debug.getGlobalGcInvocationCount();
并且,在?Android 6.0 及之后?可以拿到?更精準(zhǔn)?的?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");
對(duì)于?GC 信息的排查,我們一般關(guān)注?阻塞式GC的次數(shù)和耗時(shí),因?yàn)樗鼤?huì)?暫停線程,可能導(dǎo)致應(yīng)用發(fā)生?卡頓。建議?僅對(duì)重度場(chǎng)景使用。
6、建立線上 OOM 監(jiān)控組件:Probe
美團(tuán)的 Android?內(nèi)存泄漏自動(dòng)化鏈路分析組件?Probe?在?OOM?時(shí)會(huì)生成?Hprof 內(nèi)存快照,然后,它會(huì)通過(guò)?單獨(dú)進(jìn)程?對(duì)這個(gè)?文件?做進(jìn)一步?分析。
Probe 組件的缺陷及解決方案
它的缺點(diǎn)比較多,具體為如下幾點(diǎn):
1、在崩潰的時(shí)候生成內(nèi)存快照容易導(dǎo)致二次崩潰。
2、部分手機(jī)生成 Hprof 快照比較耗時(shí)。
3、部分 OOM 是由虛擬內(nèi)存不足導(dǎo)致。
在實(shí)現(xiàn)自動(dòng)化鏈路分析組件 Probe 的過(guò)程中主要要解決兩個(gè)問(wèn)題,如下所示:
1、鏈路分析時(shí)間過(guò)長(zhǎng)
1)、使用鏈路歸并:將具有?相同層級(jí)與結(jié)構(gòu)?的鏈路進(jìn)行?合并。
2)、使用?自適應(yīng)擴(kuò)容法:通過(guò)不斷比較現(xiàn)有鏈路和新鏈路,結(jié)合擴(kuò)容因子,逐漸完善為完整的泄漏鏈路。
2、分析進(jìn)程占用內(nèi)存過(guò)大
分析進(jìn)程占用的內(nèi)存?跟?內(nèi)存快照文件的大小?不成正相關(guān),而跟?內(nèi)存快照文件的 Instance 數(shù)量呈?正相關(guān)。所以在開(kāi)發(fā)過(guò)程中我們應(yīng)該?盡可能排除不需要的Instance實(shí)例。
Prope 分析流程揭秘
Prope 的?總體架構(gòu)圖?如下所示:

而它的整個(gè)分析流程具體可以細(xì)分為八個(gè)步驟,如下所示:
1、hprof 映射到內(nèi)存 => 解析成 Snapshot & 計(jì)數(shù)壓縮:
解析后的 Snapshot 中的 Heap 有四種類型,具體為:
1)、DefaultHeap
2)、ImageHeap
3)、App Heap:包括?ClassInstance、ClassObj、ArrayInstance、RootObj。
4)、System Heap
解析完?后使用了?計(jì)數(shù)壓縮策略,對(duì)?相同的 Instance?使用?計(jì)數(shù),以?減少占用內(nèi)存。超過(guò)計(jì)數(shù)閾值的需要計(jì)入計(jì)數(shù)桶(計(jì)數(shù)桶記錄了 丟棄個(gè)數(shù) 和 每個(gè) Instance 的大小)。
2、生成 Dominator Tree。
3、計(jì)算 RetainSize。
4、生成 Reference 鏈 && 基礎(chǔ)數(shù)據(jù)類型增強(qiáng):
如果對(duì)象是?基礎(chǔ)數(shù)據(jù)類型,會(huì)將?自身的 RetainSize 累加到父節(jié)點(diǎn)?上,將?懷疑對(duì)象?替換為它的?父節(jié)點(diǎn)。
5、鏈路歸并。
6、計(jì)數(shù)桶補(bǔ)償 & 基礎(chǔ)數(shù)據(jù)類型和父節(jié)點(diǎn)融合:
使用計(jì)數(shù)補(bǔ)償策略計(jì)算 RetainSize,主要是 判斷對(duì)象是否在計(jì)數(shù)桶中,如果在的話則將 丟棄的個(gè)數(shù)和大小補(bǔ)償?shù)綄?duì)象上,累積計(jì)算RetainSize,最后對(duì) RetainSize 排序以查找可疑對(duì)象。
7、排序擴(kuò)容。
8、查找泄露鏈路。
7、實(shí)現(xiàn) 單機(jī)版 的 Profile Memory 自動(dòng)化內(nèi)存分析
在配置的時(shí)候要注意兩個(gè)問(wèn)題:
1、liballoc-lib.so在構(gòu)建后工程的 build => intermediates => cmake 目錄下。將對(duì)應(yīng)的 cpu abi 目錄拷貝到新建的 libs 目錄下。
2、在 DumpPrinter Java 庫(kù)的 build.gradle 中的 jar 閉包中需要加入以下代碼以識(shí)別源碼路徑:
sourceSets.main.java.srcDirs = ['src']
使用步驟
具體的使用步驟如下所示:
1、首先,點(diǎn)擊 ”開(kāi)始記錄“ 按鈕可以看到觸發(fā)對(duì)象分配的記錄,說(shuō)明對(duì)象已經(jīng)開(kāi)始記錄對(duì)象的分配,log如下所示:
12-26 10:54:03.963 30450-30450/com.dodola.alloctrack I/AllocTracker: ====current alloc count 388=====
2、然后,點(diǎn)擊多次 ”生成1000個(gè)對(duì)象“ 按鈕,當(dāng)對(duì)象達(dá)到設(shè)置的最大數(shù)量的時(shí)候觸發(fā)內(nèi)存dump,會(huì)得到保存數(shù)據(jù)路徑的日志。如下所示:
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、此時(shí),可以看到數(shù)據(jù)保存在 sdk 下的 crashDump 目錄下。
4、接著,通過(guò) gradle task :buildAlloctracker 任務(wù)編譯出存放在 tools/DumpPrinter-1.0.jar 的 dump 工具,然后采用如下命令來(lái)將數(shù)據(jù)解析 到dump_log.txt 文件中。
java -jar tools/DumpPrinter-1.0.jar dump文件路徑 > dump_log.txt
5、最后,就可以在 dump_log.txt 文件中看到解析出來(lái)的數(shù)據(jù),如下所示:
Found 4949 records:
tid=1 byte[] (94208 bytes)
? ? dalvik.system.VMRuntime.newNonMovableArray (Native method)
? ? android.graphics.Bitmap.nativeCreate (Native method)
? ? android.graphics.Bitmap.createBitmap (Bitmap.java:975)
? ? android.graphics.Bitmap.createBitmap (Bitmap.java:946)
? ? android.graphics.Bitmap.createBitmap (Bitmap.java:913)
? ? android.graphics.drawable.RippleDrawable.updateMaskShaderIfNeeded (RippleDrawable.java:776)
? ? android.graphics.drawable.RippleDrawable.drawBackgroundAndRipples (RippleDrawable.java:860)
? ? android.graphics.drawable.RippleDrawable.draw (RippleDrawable.java:700)
? ? android.view.View.getDrawableRenderNode (View.java:17736)
? ? android.view.View.drawBackground (View.java:17660)
? ? android.view.View.draw (View.java:17467)
? ? android.view.View.updateDisplayListIfDirty (View.java:16469)
? ? android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
? ? android.view.ViewGroup.dispatchGetDisplayList (ViewGroup.java:3885)
? ? android.view.View.updateDisplayListIfDirty (View.java:16429)
? ? android.view.ViewGroup.recreateChildDisplayList (ViewGroup.java:3905)
8、搭建線下 Native 內(nèi)存泄漏監(jiān)控體系
在?Android 8.0 及之后,可以使用?Address Sanitizer、Malloc 調(diào)試和 Malloc 鉤子?進(jìn)行?native 內(nèi)存分析,參見(jiàn)?native_memory
對(duì)于線下 Native 內(nèi)存泄漏監(jiān)控的建立,主要針對(duì)?是否能重編 so 的情況?來(lái)記錄分配的內(nèi)存信息。
針對(duì)無(wú)法重編so的情況
1)、首先,使用?PLT Hook 攔截庫(kù)的內(nèi)存分配函數(shù),然后,重定向到我們自己的實(shí)現(xiàn)后去?記錄分配的 內(nèi)存地址、大小、來(lái)源so庫(kù)路徑?等信息。
2)、最后,定期 掃描分配與釋放 的配對(duì)內(nèi)存塊,對(duì)于 不配對(duì)的分配 輸出上述記錄的信息。
針對(duì)可重編的so情況
1)、首先,通過(guò)?GCC?的?”-finstrument-functions“?參數(shù)給?所有函數(shù)插樁,然后,在樁中模擬調(diào)用棧的入棧與出棧操作。
2)、接著,通過(guò)?ld?的?”--warp“?參數(shù)?攔截內(nèi)存分配和釋放函數(shù),重定向到我們自己的實(shí)現(xiàn)后記錄分配的 內(nèi)存地址、大小、來(lái)源so以及插樁調(diào)用棧此刻的內(nèi)容。
3)、最后,定期掃描分配與釋放是否配對(duì),對(duì)于不配對(duì)的分配輸出我們記錄的信息。
9、設(shè)置內(nèi)存兜底策略
設(shè)置內(nèi)存兜底策略的目的,是為了?在用戶無(wú)感知的情況下,在接近觸發(fā)系統(tǒng)異常前,選擇合適的場(chǎng)景殺死進(jìn)程并將其重啟,從而使得應(yīng)用內(nèi)存占用回到正常情況。
通常執(zhí)行內(nèi)存兜底策略時(shí)至少需要滿足六個(gè)條件,如下所示:
1)、是否在主界面退到后臺(tái)且位于后臺(tái)時(shí)間超過(guò) 30min。
2)、當(dāng)前時(shí)間為早上 2~5 點(diǎn)。
3)、不存在前臺(tái)服務(wù)(通知欄、音樂(lè)播放欄等情況)。
4)、Java heap 必須大于當(dāng)前進(jìn)程最大可分配的85% || native內(nèi)存大于800MB。
5)、vmsize 超過(guò)了4G(32bit)的85%。
6)、非大量的流量消耗(不超過(guò)1M/min) && 進(jìn)程無(wú)大量CPU調(diào)度情況。
只有在滿足了以上條件之后,我們才會(huì)去殺死當(dāng)前主進(jìn)程并通過(guò) push 進(jìn)程重新拉起及初始化。
10、更深入的內(nèi)存優(yōu)化策略
除了在?Android性能優(yōu)化之內(nèi)存優(yōu)化 => 優(yōu)化內(nèi)存空間?中講解過(guò)的一些常規(guī)的內(nèi)存優(yōu)化策略以外,在下面列舉了一些更深入的內(nèi)存優(yōu)化策略。
1、使 bitmap 資源在 native 中分配
對(duì)于 Android 2.x 系統(tǒng),使用反射將 BitmapFactory.Options 里面隱藏的 inNativeAlloc 打開(kāi)。
對(duì)于 Android 4.x 系統(tǒng),使用或借鑒 Fresco 將 bitmap 資源在 native 中分配的方式。
2、圖片加載時(shí)的降級(jí)處理
使用 Glide、Fresco 等圖片加載庫(kù),通過(guò)定制,在加載 bitmap 時(shí),若發(fā)生 OOM,則使用 try catch 將其捕獲,然后清除圖片 cache,嘗試降低 bitmap format(ARGB8888、RGB565、ARGB4444、ALPHA8)。
需要注意的是,OOM 是可以捕獲的,只要 OOM 是由 try 語(yǔ)句中的對(duì)象聲明所導(dǎo)致的,那么在 catch 語(yǔ)句中,是可以釋放掉這些對(duì)象,解決 OOM 的問(wèn)題的。
3、前臺(tái)每隔 3 分鐘去獲取當(dāng)前應(yīng)用內(nèi)存占最大內(nèi)存的比例,超過(guò)設(shè)定的危險(xiǎn)閾值(如80%)則主動(dòng)釋放應(yīng)用 cache(Bitmap 為大頭),并且顯示地除去應(yīng)用的 memory,以加速內(nèi)存收集的過(guò)程。
計(jì)算當(dāng)前應(yīng)用內(nèi)存占最大內(nèi)存的比例的代碼如下:
max = Runtime.getRuntime().maxMemory();
available = Runtime.getRuntime.totalMemory() - Runtime.getFreeMemory();
ratio = available / max;
顯示地除去應(yīng)用的 memory,以加速內(nèi)存收集過(guò)程的代碼如下所示:
WindowManagerGlobal.getInstance().startTrimMemory(TRIM_MEMORY_COMPLETE);
復(fù)制代碼
5、由于 webview 存在內(nèi)存系統(tǒng)泄漏,還有 圖庫(kù)占用內(nèi)存過(guò)多 的問(wèn)題,可以采用單獨(dú)的進(jìn)程。
6、當(dāng)UI隱藏時(shí)釋放內(nèi)存
當(dāng)用戶切換到其它應(yīng)用并且你的應(yīng)用 UI 不再可見(jiàn)時(shí),應(yīng)該釋放應(yīng)用 UI 所占用的所有內(nèi)存資源。這能夠顯著增加系統(tǒng)緩存進(jìn)程的能力,能夠提升用戶體驗(yàn)。
在所有 UI 組件都隱藏的時(shí)候會(huì)接收到 Activity 的 onTrimMemory() 回調(diào)并帶有參數(shù) TRIM_MEMORY_UI_HIDDEN。
7、Activity 的兜底內(nèi)存回收策略
在 Activity 的 onDestory 中遞歸釋放其引用到的 Bitmap、DrawingCache 等資源,以降低發(fā)生內(nèi)存泄漏時(shí)對(duì)應(yīng)用內(nèi)存的壓力。
8、使用類似 Hack 的方式修復(fù)系統(tǒng)內(nèi)存泄漏
LeakCanary 的 AndroidExcludeRefs 列出了一些由于系統(tǒng)原因?qū)е乱脽o(wú)法釋放的例子,可使用類似 Hack 的方式去修復(fù)。具體的實(shí)現(xiàn)代碼可以參考?Booster => 系統(tǒng)問(wèn)題修復(fù)。
9、應(yīng)用發(fā)生 OOM 時(shí),需要上傳更加詳細(xì)的內(nèi)存相關(guān)信息。
10、當(dāng)應(yīng)用使用的Service不再使用時(shí)應(yīng)該銷毀它,建議使用 IntentServcie。
11、謹(jǐn)慎使用第三方庫(kù),避免為了使用其中一兩個(gè)功能而導(dǎo)入一個(gè)大而全的解決方案。