2021-09-24

Android深度性能優(yōu)化--內(nèi)存優(yōu)化

一、背景

在內(nèi)存管理上,JVM擁有垃圾內(nèi)存回收的機(jī)制,自身會在虛擬機(jī)層面自動分配和釋放內(nèi)存,因此不需要像使用C/C++一樣在代碼中分配和釋放某一塊內(nèi)存。Android系統(tǒng)的內(nèi)存管理類似于JVM,通過new關(guān)鍵字來為對象分配內(nèi)存,內(nèi)存的釋放由GC來回收。并且Android系統(tǒng)在內(nèi)存管理上有一個(gè)Generational Heap Memory模型,當(dāng)內(nèi)存達(dá)到某一個(gè)閾值時(shí),系統(tǒng)會根據(jù)不同的規(guī)則自動釋放可以釋放的內(nèi)存。即便有了內(nèi)存管理機(jī)制,但是,如果不合理地使用內(nèi)存,也會造成一系列的性能問題,比如內(nèi)存泄漏、內(nèi)存抖動、短時(shí)間內(nèi)分配大量的內(nèi)存對象等等。

二、優(yōu)化工具

2.1 Memory Profiler

Memory profiler是Android Studio自帶的一個(gè)內(nèi)存檢測工具,通過實(shí)時(shí)圖表的方式展示內(nèi)存信息,具有可以識別內(nèi)存泄露,內(nèi)存抖動等現(xiàn)象,并可以將捕獲到的內(nèi)存信息進(jìn)行堆轉(zhuǎn)儲、強(qiáng)制GC以及跟蹤內(nèi)存分配的能力。

Android Studio打開Profiler工具

1.jpg

觀察Memory曲線,比較平緩即為內(nèi)存分配正常,如果出現(xiàn)大的波動有可能發(fā)生了內(nèi)存泄露。

GC:可手動觸發(fā)GC

Dump:Dump出當(dāng)前Java Heap信息

Record:記錄一段時(shí)間內(nèi)的內(nèi)存信息

點(diǎn)擊Dump后

2.jpg

可查看當(dāng)前內(nèi)存分配對象

Allocations:分配對象個(gè)數(shù)

Native Size:Native內(nèi)存大小

Shallow Size:對象本身占用內(nèi)存的大小,不包含其引用的對象

Retained Size: 對象的Retained Size = 對象本身的Shallow Size + 對象能直接或間接訪問到的對象的Shallow Size,也就是說 Retained Size 就是該對象被 Gc 之后所能回收內(nèi)存的總和

點(diǎn)擊Bitmap Preview可以進(jìn)行預(yù)覽圖片,對查看圖片占用內(nèi)存情況比較有幫助

點(diǎn)擊Record后

3.jpg

可以記錄一段時(shí)間內(nèi)內(nèi)存分配情況,可查看各對象分配大小及調(diào)用棧、對象生成位置

2.2 Memory Analyzer(MAT)

比Memory Profiler更強(qiáng)大的Java Heap分析工具,可以準(zhǔn)確查找內(nèi)存泄露以及內(nèi)存占用情況,還可以生成整體報(bào)告,用來分析問題等。

MAT一般用來線下結(jié)合Memory Profiler分析問題使用,Memory Profiler可以直觀看出內(nèi)存抖動,然后生成的hdprof文件,通過MAT深入分析及定位內(nèi)存泄露問題。

具體使用下面會結(jié)合實(shí)例講解一下

2.3 LeakCannary

Leak Cannary是一個(gè)能自動監(jiān)測內(nèi)存泄露的線下監(jiān)測工具,具體原理可自行了解下。

github鏈接:https://github.com/square/leakcanary

三、內(nèi)存管理

3.1 內(nèi)存區(qū)域

Java內(nèi)存劃分為方法區(qū)、堆、程序計(jì)數(shù)器、本地方法棧、虛擬機(jī)棧五個(gè)區(qū)域;

線程維度分為線程共享區(qū)和線程隔離區(qū),方法區(qū)和堆是線程共享的,程序計(jì)數(shù)器、本地方法棧、虛擬機(jī)棧是線程隔離的,如下圖

4.jpg

方法區(qū)

  • 線程共享區(qū)域,用于存儲類信息、靜態(tài)變量、常量、即時(shí)編譯器編譯出來的代碼數(shù)據(jù)
  • 無法滿足內(nèi)存分配需求時(shí)會發(fā)生OOM

  • 線程共享區(qū)域,是JAVA虛擬機(jī)管理的內(nèi)存中最大的一塊,在虛擬機(jī)啟動時(shí)創(chuàng)建
  • 存放對象實(shí)例,幾乎所有的對象實(shí)例都在堆上分配,GC管理的主要區(qū)域

虛擬機(jī)棧

  • 線程私有區(qū)域,每個(gè)java方法在執(zhí)行的時(shí)候會創(chuàng)建一個(gè)棧幀用于存儲局部變量表、操作數(shù)棧、動態(tài)鏈接、方法出口等信息。方法從執(zhí)行開始到結(jié)束過程就是棧幀在虛擬機(jī)棧中入棧出棧過程
  • 局部變量表存放編譯期可知的基本數(shù)據(jù)類型、對象引用、returnAddress類型。所需的內(nèi)存空間會在編譯期間完成分配,進(jìn)入一個(gè)方法時(shí)在幀中局部變量表的空間是完全確定的,不需要運(yùn)行時(shí)改變
  • 若線程申請的棧深度大于虛擬機(jī)允許的最大深度,會拋出SatckOverFlowError錯(cuò)誤
  • 虛擬機(jī)動態(tài)擴(kuò)展時(shí),若無法申請到足夠內(nèi)存,會拋出OutOfMemoryError錯(cuò)誤

本地方法棧

  • 為虛擬機(jī)中Native方法服務(wù),對本地方法棧中使用的語言、數(shù)據(jù)結(jié)構(gòu)、使用方式?jīng)]有強(qiáng)制規(guī)定,虛擬機(jī)可自有實(shí)現(xiàn)
  • 占用的內(nèi)存區(qū)大小是不固定的,可根據(jù)需要?jiǎng)討B(tài)擴(kuò)展

程序計(jì)數(shù)器

  • 一塊較小的內(nèi)存空間,線程私有,存儲當(dāng)前線程執(zhí)行的字節(jié)碼行號指示器
  • 字節(jié)碼解釋器通過改變這個(gè)計(jì)數(shù)器的值來選取下一條需要執(zhí)行的字節(jié)碼指令:分支、循環(huán)、跳轉(zhuǎn)等
  • 每個(gè)線程都有一個(gè)獨(dú)立的程序計(jì)數(shù)器
  • 唯一一個(gè)在java虛擬機(jī)中不會OOM的區(qū)域

3.2 對象存活判斷

引用計(jì)數(shù)法

  • 給對象添加引用計(jì)數(shù)器,每當(dāng)一個(gè)地方引用時(shí),計(jì)數(shù)器加1,引用失效時(shí)計(jì)數(shù)器減1;當(dāng)引用計(jì)數(shù)器為0時(shí)即為對象不可用
  • 實(shí)現(xiàn)簡單,效率高,但是無法解決相互引用問題,主流虛擬機(jī)一般不使用此方法判斷對象是否存活

可達(dá)性分析法

  • 從一些稱為"GC Roots"的對象作為起點(diǎn),向下搜索,搜索走過的路徑稱為引用鏈,當(dāng)一個(gè)對象到GC Roots沒有任何引用鏈時(shí)即為對象不可用,可被回收的
  • 可被稱為GC Roots的對象:虛擬機(jī)棧中引用的對象、方法區(qū)中類靜態(tài)屬性引用的對象、方法區(qū)中常量引用的對象、本地方法棧中引用的對象

GC Root有以下幾種:

  • Class-由系統(tǒng)ClassLoader加載的對象
  • Thread-活著的線程
  • Stack Local-Java方法的local變量或參數(shù)
  • JNI Local - JNI方法的local變量或參數(shù)
  • JNI Global - 全局JNI引用
  • Monitor Used - 用于同步的監(jiān)控對象

3.3 垃圾回收算法

標(biāo)記清除算法

標(biāo)記清除算法有兩個(gè)階段,首先標(biāo)記出需要回收的對象,在標(biāo)記完成后統(tǒng)一回收所有標(biāo)記的對象;

缺點(diǎn):

  • 效率問題:標(biāo)記和清除兩個(gè)過程效率都不高
  • 空間問題:標(biāo)記清除之后會導(dǎo)致很多不連續(xù)的內(nèi)存碎片,會導(dǎo)致需要分配大對象時(shí)無法找到足夠的連續(xù)空間而不得不觸發(fā)GC的問題

復(fù)制算法

將可用內(nèi)存按空間分為大小相同的兩小塊,每次只使用其中的一塊,等這塊內(nèi)存使用完了將還存活的對象復(fù)制到另一塊內(nèi)存上,然后將這塊內(nèi)存區(qū)域?qū)ο笳w清除掉。每次對整個(gè)半?yún)^(qū)進(jìn)行內(nèi)存回收,不會導(dǎo)致碎片問題,實(shí)現(xiàn)簡單高效。

缺點(diǎn):

  • 需要將內(nèi)存縮小為原來的一半,空間代價(jià)太高

標(biāo)記整理算法

標(biāo)記整理算法標(biāo)記過程和標(biāo)記清除算法一樣,但清除過程并不是對可回收對象直接清理,而是將所有存活對象像一端移動,然后集中清理到端邊界以外的內(nèi)存。

分代收集算法

當(dāng)代虛擬機(jī)垃圾回收算法都采用分代收集算法來收集,根據(jù)對象存活周期不同將內(nèi)存劃分為新生代和老年代,再根據(jù)每個(gè)年代的特點(diǎn)采用最合適的算法。

  • 新生代存活對象較少,每次垃圾回收都有大量對象死去,一般采用復(fù)制算法,只需要付出復(fù)制少量存活對象的成本就可以實(shí)現(xiàn)垃圾回收;
  • 老年代存活對象較多,沒有額外空間進(jìn)行分配擔(dān)保,就必須采用標(biāo)記清除算法和標(biāo)記整理算法進(jìn)行回收;

四、內(nèi)存抖動

內(nèi)存頻繁分配和回收導(dǎo)致內(nèi)存不穩(wěn)定

  • 頻繁GC,內(nèi)存曲線呈現(xiàn)鋸齒狀,會導(dǎo)致卡頓
  • 頻繁的創(chuàng)建對象會導(dǎo)致內(nèi)存不足及碎片
  • 不連續(xù)的內(nèi)存碎片無法被釋放,導(dǎo)致OOM

4.1 模擬內(nèi)存抖動

執(zhí)行此段代碼

private static Handler mShakeHandler = new Handler() {
    @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
        // 頻繁創(chuàng)建對象,模擬內(nèi)存抖動
        for(int index = 0;index <= 100;index ++) {
            String strArray[] = new String[100000];
        }

        mShakeHandler.sendEmptyMessageDelayed(0,30);
    }
};

4.2 分析并定位

利用Memory Profiler工具查看內(nèi)存信息

5.jpg

發(fā)現(xiàn)內(nèi)存曲線由原來的平穩(wěn)曲線變成鋸齒狀

6.jpg

點(diǎn)擊record記錄內(nèi)存信息,查找發(fā)生內(nèi)存抖動位置,發(fā)現(xiàn)String對象ShallowSize非常異常,可直接通過Jump to Source定位到代碼位置

7.jpg

五、內(nèi)存泄露

定義:內(nèi)存中存在已經(jīng)沒有用確無法回收的對象

現(xiàn)象:會導(dǎo)致內(nèi)存抖動,可用內(nèi)存減少,進(jìn)而導(dǎo)致GC頻繁、卡頓、OOM

5.1 模擬內(nèi)存泄露

模擬內(nèi)存泄露代碼,反復(fù)進(jìn)入退出該Activity

/**
 * 模擬內(nèi)存泄露的Activity
 */
public class MemoryLeakActivity extends AppCompatActivity implements CallBack{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_memoryleak);
        ImageView imageView = findViewById(R.id.iv_memoryleak);
        Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.splash);
        imageView.setImageBitmap(bitmap);

        // 添加靜態(tài)類引用
        CallBackManager.addCallBack(this);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
//        CallBackManager.removeCallBack(this);
    }

    @Override
    public void dpOperate() {
        // do sth
    }

5.2 分析并定位

通過Memory Profiler工具查看內(nèi)存曲線,發(fā)現(xiàn)內(nèi)存在不斷的上升

8.jpg

如果想分析定位具體發(fā)生內(nèi)存泄露位置需要借助MAT工具

首先生成hprof文件

點(diǎn)擊dump將當(dāng)前內(nèi)存信息轉(zhuǎn)成hprof文件,需要對生成的文件轉(zhuǎn)換成MAT可讀取文件

執(zhí)行一下轉(zhuǎn)換命令(Android/sdk/platorm-tools路徑下)

hprof-conv 剛剛生成的hprof文件 memory-mat.hprof

使用mat打開剛剛轉(zhuǎn)換的hprof文件

9.jpg

點(diǎn)擊Historygram,搜索MemoryLeakActivity

10.jpg

可以看到有8個(gè)MemoryLeakActivity未釋放

11.jpg

查看所有引用對象

12.jpg

查看到GC Roots的引用鏈

13.jpg

可以看到GC Roots是CallBackManager

14.jpg

解決問題,當(dāng)Activity銷毀時(shí)將當(dāng)前引用移除

@Override
protected void onDestroy() {
    super.onDestroy();
    CallBackManager.removeCallBack(this);
}

六、MAT分析工具

Overview

當(dāng)前內(nèi)存整體信息

[圖片上傳失敗...(image-c0bad7-1632471126749)]

Histogram

列舉對象所有的實(shí)例及實(shí)例所占大小,可按package排序

[圖片上傳失敗...(image-83fb18-1632471126749)]

可以查看應(yīng)用包名下Activity存在實(shí)例個(gè)數(shù),可以查看是否存在內(nèi)存泄露,這里發(fā)現(xiàn)內(nèi)存中有8個(gè)Activity實(shí)例未釋放

[圖片上傳失敗...(image-d501f9-1632471126749)]

查看未被釋放的Activity的引用鏈

[圖片上傳失敗...(image-4347e3-1632471126749)]

Dominator_tree

當(dāng)前所有實(shí)例的支配樹,和Histogram區(qū)別時(shí)Histogram是類維度,dominator_tree是實(shí)例維度,可以查看所有實(shí)例的所占百分比和引用鏈

[圖片上傳失敗...(image-34cb1a-1632471126749)]

SQL

通過sql語句查詢相關(guān)類信息

[圖片上傳失敗...(image-c55ac1-1632471126749)]

Thread_overview

查看當(dāng)前所有線程信息

[圖片上傳失敗...(image-f62dea-1632471126749)]

Top Consumers

通過圖形方式展示占用內(nèi)存較高的對象,對降低內(nèi)存棧優(yōu)化可用內(nèi)存比較有幫助

[圖片上傳失敗...(image-d91270-1632471126749)]

[圖片上傳失敗...(image-7c14ca-1632471126749)]

Leak Suspects

內(nèi)存泄露分析頁面

[圖片上傳失敗...(image-e1c2e4-1632471126749)]

直接定位到內(nèi)存泄露位置

[圖片上傳失敗...(image-90bec3-1632471126749)]

七、通過ARTHook檢測不合理圖片

7.1 獲取Bitmap占用內(nèi)存

  • 通過getByteCount方法,但是需要在運(yùn)行時(shí)獲取
  • width * height * 一個(gè)像素所占內(nèi)存 * 圖片所在資源目錄壓縮比

7.2 檢測大圖

當(dāng)圖片控件load圖片大小超過控件自身大小時(shí)會造成內(nèi)存浪費(fèi),所以檢測出不合理圖片對內(nèi)存優(yōu)化是很重要的。

ARTHook方式檢測不合理圖片

通過ARTHook方法可以優(yōu)雅的獲取不合理圖片,侵入性低,但是因?yàn)榧嫒菪詥栴}一般在線下使用。

引入epic開源庫

implementation 'me.weishu:epic:0.3.6'

實(shí)現(xiàn)Hook方法

public class CheckBitmapHook extends XC_MethodHook {

    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);

        ImageView imageView = (ImageView)param.thisObject;
        checkBitmap(imageView,imageView.getDrawable());
    }

    private static void checkBitmap(Object o,Drawable drawable) {
        if(drawable instanceof BitmapDrawable && o instanceof View) {
            final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
            if(bitmap != null) {
                final View view = (View)o;
                int width = view.getWidth();
                int height = view.getHeight();
                if(width > 0 && height > 0) {
                    if(bitmap.getWidth() > (width <<1) && bitmap.getHeight() > (height << 1)) {
                        warn(bitmap.getWidth(),bitmap.getHeight(),width,height,
                                new RuntimeException("Bitmap size is too large"));
                    }
                } else {
                    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 = new StringBuilder("Bitmap size too large: ")
                .append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
                .append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
                .append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
                .toString();

        LogUtils.i(warnInfo);

Application初始化時(shí)注入Hook

DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
    @Override protected void afterHookedMethod(MethodHookParam param) throws Throwable {
        super.afterHookedMethod(param);
        DexposedBridge.findAndHookMethod(ImageView.class,"setImageBitmap", Bitmap.class,
                new CheckBitmapHook());
    }
});

八、線上內(nèi)存監(jiān)控

8.1 常規(guī)方案

常規(guī)方案一

在特定場景中獲取當(dāng)前占用內(nèi)存大小,如果當(dāng)前內(nèi)存大小超過系統(tǒng)最大內(nèi)存80%,對當(dāng)前內(nèi)存進(jìn)行一次Dump(Debug.dumpHprofData()),選擇合適時(shí)間將hprof文件進(jìn)行上傳,然后通過MAT工具手動分析該文件。

缺點(diǎn):

  • Dump文件比較大,和用戶使用時(shí)間、對象樹正相關(guān)
  • 文件較大導(dǎo)致上傳失敗率較高,分析困難

常規(guī)方案二

將LeakCannary帶到線上,添加預(yù)設(shè)懷疑點(diǎn),對懷疑點(diǎn)進(jìn)行內(nèi)存泄露監(jiān)控,發(fā)現(xiàn)內(nèi)存泄露回傳到server。

缺點(diǎn):

  • 通用性較低,需要預(yù)設(shè)懷疑點(diǎn),對沒有預(yù)設(shè)懷疑點(diǎn)的地方監(jiān)控不到
  • LeakCanary分析比較耗時(shí)、耗內(nèi)存,有可能會發(fā)生OOM

8.2 LeakCannary定制改造

  1. 將需要預(yù)設(shè)懷疑點(diǎn)改為自動尋找懷疑點(diǎn),自動將前內(nèi)存中所占內(nèi)存較大的對象類中設(shè)置懷疑點(diǎn)。
  2. LeakCanary分析泄露鏈路比較慢,改造為只分析Retain size大的對象。
  3. 分析過程會OOM,是因?yàn)長eakCannary分析時(shí)會將分析對象全部加載到內(nèi)存當(dāng)中,我們可以記錄下分析對象的個(gè)數(shù)和占用大小,對分析對象進(jìn)行裁剪,不全部加載到內(nèi)存當(dāng)中。

8.3 完整方案

  1. 監(jiān)控常規(guī)指標(biāo):待機(jī)內(nèi)存、重點(diǎn)模塊占用內(nèi)存、OOM率
  2. 監(jiān)控APP一個(gè)生命周期內(nèi)和重點(diǎn)模塊界面的生命周期內(nèi)的GC次數(shù)、GC時(shí)間等
  3. 將定制的LeakCanary帶到線上,自動化分析線上的內(nèi)存泄露
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

  • RunLoop 與線程的關(guān)系[#RunLoop] RunLoop 對外的接口[#RunLoop-1] RunLoo...
    我是wLiang閱讀 254評論 0 0
  • 三重:代碼、底層內(nèi)存、源碼 第一階段:開發(fā)常用JavaSE基礎(chǔ)、IDE、Maven、Gradle、SVN、Git、...
    guodd369閱讀 17,523評論 1 44
  • Android 內(nèi)存泄漏總結(jié) 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。內(nèi)存泄漏...
    神奇的小蘑菇閱讀 574評論 0 0
  • Android 內(nèi)存管理的目的 內(nèi)存管理的目的就是讓我們在開發(fā)中怎么有效的避免我們的應(yīng)用出現(xiàn)內(nèi)存泄漏的問題。簡單粗...
    晨光光閱讀 1,374評論 1 4
  • 設(shè)計(jì)模式 一.六大設(shè)計(jì)原則 1.開閉原則:針對擴(kuò)展開放,修改關(guān)閉; 2.里氏替換原則:任何父類出現(xiàn)的地方都可由其子...
    說好的蔚藍(lán)天空呢閱讀 663評論 0 0

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