Android深度效能優化--記憶體優化(一篇就夠)

nameyuxiang發表於2020-03-05

本文整理自網路課程 

一、背景

在記憶體管理上,JVM擁有垃圾記憶體回收的機制,自身會在虛擬機器層面自動分配和釋放記憶體,因此不需要像使用C/C++一樣在程式碼中分配和釋放某一塊記憶體。Android系統的記憶體管理類似於JVM,通過new關鍵字來為物件分配記憶體,記憶體的釋放由GC來回收。並且Android系統在記憶體管理上有一個Generational Heap Memory模型,當記憶體達到某一個閾值時,系統會根據不同的規則自動釋放可以釋放的記憶體。即便有了記憶體管理機制,但是,如果不合理地使用記憶體,也會造成一系列的效能問題,比如記憶體洩漏、記憶體抖動、短時間內分配大量的記憶體物件等等。

二、優化工具

1、Memory Profiler

Memory profiler是Android Studio自帶的一個記憶體檢測工具,通過實時圖表的方式展示記憶體資訊,具有可以識別記憶體洩露,記憶體抖動等現象,並可以將捕獲到的記憶體資訊進行堆轉儲、強制GC以及跟蹤記憶體分配的能力。

Android Studio開啟Profiler工具

Android深度效能優化--記憶體優化(一篇就夠)

觀察Memory曲線,比較平緩即為記憶體分配正常,如果出現大的波動有可能發生了記憶體洩露。

GC:可手動觸發GC

Dump:Dump出當前Java Heap資訊

Record:記錄一段時間內的記憶體資訊

點選Dump後

Android深度效能優化--記憶體優化(一篇就夠)

可檢視當前記憶體分配物件

Allocations:分配物件個數

Native Size:Native記憶體大小

Shallow Size:物件本身佔用記憶體的大小,不包含其引用的物件

Retained Size: 物件的Retained Size = 物件本身的Shallow Size + 物件能直接或間接訪問到的物件的Shallow Size,也就是說 Retained Size 就是該物件被 Gc 之後所能回收記憶體的總和

點選Bitmap Preview可以進行預覽圖片,對檢視圖片佔用記憶體情況比較有幫助

點選Record後

Android深度效能優化--記憶體優化(一篇就夠)

可以記錄一段時間內記憶體分配情況,可檢視各物件分配大小及呼叫棧、物件生成位置

2、Memory Analyzer(MAT)

比Memory Profiler更強大的Java Heap分析工具,可以準確查詢記憶體洩露以及記憶體佔用情況,還可以生成整體報告,用來分析問題等。

MAT一般用來線下結合Memory Profiler分析問題使用,Memory Profiler可以直觀看出記憶體抖動,然後生成的hdprof檔案,通過MAT深入分析及定位記憶體洩露問題。

具體使用下面會結合例項講解一下

3、LeakCannary

Leak Cannary是一個能自動監測記憶體洩露的線下監測工具,具體原理可自行了解下。

github連結:github.com/square/leak…

三、記憶體管理

3.1 記憶體區域

Java記憶體劃分為方法區、堆、程式計數器、本地方法棧、虛擬機器棧五個區域;

執行緒維度分為執行緒共享區和執行緒隔離區,方法區和堆是執行緒共享的,程式計數器、本地方法棧、虛擬機器棧是執行緒隔離的,如下圖

Android深度效能優化--記憶體優化(一篇就夠)

方法區

  • 執行緒共享區域,用於儲存類資訊、靜態變數、常量、即時編譯器編譯出來的程式碼資料
  • 無法滿足記憶體分配需求時會發生OOM

  • 執行緒共享區域,是JAVA虛擬機器管理的記憶體中最大的一塊,在虛擬機器啟動時建立
  • 存放物件例項,幾乎所有的物件例項都在堆上分配,GC管理的主要區域

虛擬機器棧

  • 執行緒私有區域,每個java方法在執行的時候會建立一個棧幀用於儲存區域性變數表、運算元棧、動態連結、方法出口等資訊。方法從執行開始到結束過程就是棧幀在虛擬機器棧中入棧出棧過程
  • 區域性變數表存放編譯期可知的基本資料型別、物件引用、returnAddress型別。所需的記憶體空間會在編譯期間完成分配,進入一個方法時在幀中區域性變數表的空間是完全確定的,不需要執行時改變
  • 若執行緒申請的棧深度大於虛擬機器允許的最大深度,會丟擲SatckOverFlowError錯誤
  • 虛擬機器動態擴充套件時,若無法申請到足夠記憶體,會丟擲OutOfMemoryError錯誤

本地方法棧

  • 為虛擬機器中Native方法服務,對本地方法棧中使用的語言、資料結構、使用方式沒有強制規定,虛擬機器可自有實現
  • 佔用的記憶體區大小是不固定的,可根據需要動態擴充套件

程式計數器

  • 一塊較小的記憶體空間,執行緒私有,儲存當前執行緒執行的位元組碼行號指示器
  • 位元組碼直譯器通過改變這個計數器的值來選取下一條需要執行的位元組碼指令:分支、迴圈、跳轉等
  • 每個執行緒都有一個獨立的程式計數器
  • 唯一一個在java虛擬機器中不會OOM的區域

3.2 物件存活判斷

引用計數法

  • 給物件新增引用計數器,每當一個地方引用時,計數器加1,引用失效時計數器減1;當引用計數器為0時即為物件不可用
  • 實現簡單,效率高,但是無法解決相互引用問題,主流虛擬機器一般不使用此方法判斷物件是否存活

可達性分析法

  • 從一些稱為"GC Roots"的物件作為起點,向下搜尋,搜尋走過的路徑稱為引用鏈,當一個物件到GC Roots沒有任何引用鏈時即為物件不可用,可被回收的
  • 可被稱為GC Roots的物件:虛擬機器棧中引用的物件、方法區中類靜態屬性引用的物件、方法區中常量引用的物件、本地方法棧中引用的物件

GC Root有以下幾種:

  1. Class-由系統ClassLoader載入的物件
  2. Thread-活著的執行緒
  3. Stack Local-Java方法的local變數或引數
  4. JNI Local - JNI方法的local變數或引數
  5. JNI Global - 全域性JNI引用
  6. Monitor Used - 用於同步的監控物件

3.3 垃圾回收演算法

標記清除演算法

標記清除演算法有兩個階段,首先標記出需要回收的物件,在標記完成後統一回收所有標記的物件;

缺點:

  • 效率問題:標記和清除兩個過程效率都不高
  • 空間問題:標記清除之後會導致很多不連續的記憶體碎片,會導致需要分配大物件時無法找到足夠的連續空間而不得不觸發GC的問題

複製演算法

將可用記憶體按空間分為大小相同的兩小塊,每次只使用其中的一塊,等這塊記憶體使用完了將還存活的物件複製到另一塊記憶體上,然後將這塊記憶體區域物件整體清除掉。每次對整個半區進行記憶體回收,不會導致碎片問題,實現簡單高效。

缺點:

  • 需要將記憶體縮小為原來的一半,空間代價太高

標記整理演算法

標記整理演算法標記過程和標記清除演算法一樣,但清除過程並不是對可回收物件直接清理,而是將所有存活物件像一端移動,然後集中清理到端邊界以外的記憶體。

分代收集演算法

當代虛擬機器垃圾回收演算法都採用分代收集演算法來收集,根據物件存活週期不同將記憶體劃分為新生代和老年代,再根據每個年代的特點採用最合適的演算法。

  • 新生代存活物件較少,每次垃圾回收都有大量物件死去,一般採用複製演算法,只需要付出複製少量存活物件的成本就可以實現垃圾回收;
  • 老年代存活物件較多,沒有額外空間進行分配擔保,就必須採用標記清除演算法和標記整理演算法進行回收;

四、記憶體抖動

記憶體頻繁分配和回收導致記憶體不穩定

  • 頻繁GC,記憶體曲線呈現鋸齒狀,會導致卡頓
  • 頻繁的建立物件會導致記憶體不足及碎片
  • 不連續的記憶體碎片無法被釋放,導致OOM

4.1 模擬記憶體抖動

執行此段程式碼

private static Handler mShakeHandler = new Handler() {
    @Override public void handleMessage(Message msg) {
        super.handleMessage(msg);
        // 頻繁建立物件,模擬記憶體抖動
        for(int index = 0;index <= 100;index ++) {
            String strArray[] = new String[100000];
        }


        mShakeHandler.sendEmptyMessageDelayed(0,30);
    }
};
複製程式碼

4.2 分析並定位

利用Memory Profiler工具檢視記憶體資訊

Android深度效能優化--記憶體優化(一篇就夠)

發現記憶體曲線由原來的平穩曲線變成鋸齒狀

Android深度效能優化--記憶體優化(一篇就夠)

點選record記錄記憶體資訊,查詢發生記憶體抖動位置,發現String物件ShallowSize非常異常,可直接通過Jump to Source定位到程式碼位置

Android深度效能優化--記憶體優化(一篇就夠)

五、記憶體洩露

定義:記憶體中存在已經沒有用確無法回收的物件

現象:會導致記憶體抖動,可用記憶體減少,進而導致GC頻繁、卡頓、OOM

5.1 模擬記憶體洩露

模擬記憶體洩露程式碼,反覆進入退出該Activity

/**
 * 模擬記憶體洩露的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);
        
        // 新增靜態類引用
        CallBackManager.addCallBack(this);
    }


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


    @Override
    public void dpOperate() {
        // do sth
    }
複製程式碼

5.2 分析並定位

通過Memory Profiler工具檢視記憶體曲線,發現記憶體在不斷的上升

Android深度效能優化--記憶體優化(一篇就夠)

如果想分析定位具體發生記憶體洩露位置需要藉助MAT工具

首先生成hprof檔案

點選dump將當前記憶體資訊轉成hprof檔案,需要對生成的檔案轉換成MAT可讀取檔案

執行一下轉換命令(Android/sdk/platorm-tools路徑下)

hprof-conv 剛剛生成的hprof檔案 memory-mat.hprof複製程式碼

使用mat開啟剛剛轉換的hprof檔案

Android深度效能優化--記憶體優化(一篇就夠)

點選Historygram,搜尋MemoryLeakActivity

Android深度效能優化--記憶體優化(一篇就夠)

可以看到有8個MemoryLeakActivity未釋放

Android深度效能優化--記憶體優化(一篇就夠)

檢視所有引用物件

Android深度效能優化--記憶體優化(一篇就夠)

檢視到GC Roots的引用鏈

Android深度效能優化--記憶體優化(一篇就夠)

可以看到GC Roots是CallBackManager

Android深度效能優化--記憶體優化(一篇就夠)

解決問題,當Activity銷燬時將當前引用移除

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

複製程式碼

六、MAT分析工具

Overview

當前記憶體整體資訊

Android深度效能優化--記憶體優化(一篇就夠)

Histogram

列舉物件所有的例項及例項所佔大小,可按package排序

Android深度效能優化--記憶體優化(一篇就夠)

可以檢視應用包名下Activity存在例項個數,可以檢視是否存在記憶體洩露,這裡發現記憶體中有8個Activity例項未釋放

Android深度效能優化--記憶體優化(一篇就夠)

檢視未被釋放的Activity的引用鏈

Android深度效能優化--記憶體優化(一篇就夠)

Dominator_tree

當前所有例項的支配樹,和Histogram區別時Histogram是類維度,dominator_tree是例項維度,可以檢視所有例項的所佔百分比和引用鏈

Android深度效能優化--記憶體優化(一篇就夠)

SQL

通過sql語句查詢相關類資訊

Android深度效能優化--記憶體優化(一篇就夠)

Thread_overview

檢視當前所有執行緒資訊

Android深度效能優化--記憶體優化(一篇就夠)

Top Consumers

通過圖形方式展示佔用記憶體較高的物件,對降低記憶體棧優化可用記憶體比較有幫助

Android深度效能優化--記憶體優化(一篇就夠)

Android深度效能優化--記憶體優化(一篇就夠)

Leak Suspects

記憶體洩露分析頁面

Android深度效能優化--記憶體優化(一篇就夠)

直接定位到記憶體洩露位置

Android深度效能優化--記憶體優化(一篇就夠)

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

7.1 獲取Bitmap佔用記憶體

  • 通過getByteCount方法,但是需要在執行時獲取
  • width * height * 一個畫素所佔記憶體 * 圖片所在資源目錄壓縮比

7.2 檢測大圖

當圖片控制元件load圖片大小超過控制元件自身大小時會造成記憶體浪費,所以檢測出不合理圖片對記憶體優化是很重要的。

ARTHook方式檢測不合理圖片

通過ARTHook方法可以優雅的獲取不合理圖片,侵入性低,但是因為相容性問題一般線上下使用。

引入epic開源庫

implementation 'me.weishu:epic:0.3.6'複製程式碼

實現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初始化時注入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());
    }
});

複製程式碼

八、線上記憶體監控

8.1 常規方案

常規方案一

在特定場景中獲取當前佔用記憶體大小,如果當前記憶體大小超過系統最大記憶體80%,對當前記憶體進行一次Dump(Debug.dumpHprofData()),選擇合適時間將hprof檔案進行上傳,然後通過MAT工具手動分析該檔案。

缺點:

  • Dump檔案比較大,和使用者使用時間、物件樹正相關
  • 檔案較大導致上傳失敗率較高,分析困難

常規方案二

將LeakCannary帶到線上,新增預設懷疑點,對懷疑點進行記憶體洩露監控,發現記憶體洩露回傳到server。

缺點:

  • 通用性較低,需要預設懷疑點,對沒有預設懷疑點的地方監控不到
  • LeakCanary分析比較耗時、耗記憶體,有可能會發生OOM

8.2 LeakCannary定製改造

  1. 將需要預設懷疑點改為自動尋找懷疑點,自動將前記憶體中所佔記憶體較大的物件類中設定懷疑點。
  2. LeakCanary分析洩露鏈路比較慢,改造為只分析Retain size大的物件。
  3. 分析過程會OOM,是因為LeakCannary分析時會將分析物件全部載入到記憶體當中,我們可以記錄下分析物件的個數和佔用大小,對分析物件進行裁剪,不全部載入到記憶體當中。

8.3 完整方案

  1. 監控常規指標:待機記憶體、重點模組佔用記憶體、OOM率
  2. 監控APP一個生命週期內和重點模組介面的生命週期內的GC次數、GC時間等
  3. 將定製的LeakCanary帶到線上,自動化分析線上的記憶體洩露



系列筆記
《Android深度效能優化--APP啟動優化》
《Android深度效能優化--APP記憶體優化》
《Android深度效能優化--APP佈局優化》
《Android深度效能優化--APP卡頓優化》
《Android深度效能優化--APP執行緒優化》
《Android深度效能優化--APP網路優化》
《Android深度效能優化--APP電量優化》
《Android深度效能優化--APP Crash優化》

持續關注新增微信公眾號:玉祥筆記

Android深度效能優化--記憶體優化(一篇就夠)


相關文章