究極深入Android內(nèi)存優(yōu)化(二)

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 github 地址

使用步驟

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)存分析

項(xiàng)目地址請(qǐng)點(diǎn)擊此處

在配置的時(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è)大而全的解決方案。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容