歡迎光臨
每天分享高質量文章

Android性能優化:關於 記憶體泄露 的知識都在這裡了!(文不如圖,圖不如表)

作者:_陳祥
鏈接:https://www.jianshu.com/p/e719e0c397e5

前言

在 Android 中,記憶體泄露的現象十分常見;而記憶體泄露導致的後果會使得應用Crash
本文 全面介紹了記憶體泄露的本質、原因 & 解決方案,最終提供一些常見的記憶體泄露分析工具,希望你們會喜歡。

目錄

1.png

1、簡介

即 ML (Memory Leak)
指 程式在申請記憶體後,當該記憶體不需再使用 但 卻無法被釋放 & 歸還給 程式的現象

2、對應用程式的影

容易使得應用程式發生記憶體上限溢位,即 OOM
記憶體上限溢位 簡介

1.png

3、發生記憶體泄露的本質原因

具體描述

1.jpg

  • 特別註意
    從機制上的角度來說,由於 Java存在垃圾回收機制(GC),理應不存在記憶體泄露;出現記憶體泄露的原因僅僅是外部人為原因 = 無意識地持有物件取用,使得 持有取用者的生命周期 > 被取用者的生命周期

4、儲備知識:Android 記憶體管理機制

4.1 簡介

 

1.png

下麵,將針對回收 行程、物件 、變數的記憶體分配 & 回收進行詳細講解

4.2 針對行程的記憶體策略


a. 記憶體分配策略
由 ActivityManagerService 集中管理 所有行程的記憶體分配


b. 記憶體回收策略


步驟1:Application Framework  決定回收的行程型別


Android中的行程 是托管的;當行程空間緊張時,會 按行程優先級低->>高的順序 自動回收行程
Android將行程分為5個優先等級,具體如下:

1.png

步驟2:Linux 內核真正回收具體行程
ActivityManagerService 對 所有行程進行評分(評分存放在變數adj中)
更新評分到Linux 內核
由Linux 內核完成真正的記憶體回收

此處僅總結流程,這其中的過程複雜,有興趣的讀者可研究系統原始碼ActivityManagerService.java

 

4.2 針對物件、變數的記憶體策略

 

  • Android的對於物件、變數的記憶體策略同 Java

  • 記憶體管理 = 物件 / 變數的記憶體分配 + 記憶體釋放

下麵,將詳細講解記憶體分配 & 記憶體釋放策略

a. 記憶體分配策略

  • 物件 / 變數的記憶體分配 由程式自動 負責

  • 共有3種:靜態分配、棧式分配、 & 堆式分配,分別面向靜態變數、區域性變數  & 物件實體
    具體介紹如下

1.png

註:用1個實體講解 記憶體分配

public class Sample {    
    int s1 = 0;
    Sample mSample1 = new Sample();   

    // 方法中的區域性變數s2、mSample2存放在 棧記憶體
    // 變數mSample2所指向的物件實體存放在 堆記憶體
      // 該實體的成員變數s1、mSample1也存放在棧中
    public void method() {        
        int s2 = 0;
        Sample mSample2 = new Sample();
    }
}
    // 變數mSample3所指向的物件實體存放在堆記憶體
    // 該實體的成員變數s1、mSample1也存放在堆記憶體中
    Sample mSample3 = new Sample();

b. 記憶體釋放策略

  • 物件 / 變數的記憶體釋放 由Java垃圾回收器(GC) / 幀棧 負責

  • 此處主要講解物件分配(即堆式分配)的記憶體釋放策略 = Java垃圾回收器(GC)

由於靜態分配不需釋放、棧式分配僅 通過幀棧自動出、入棧,較簡單,故不詳細描述

  • Java垃圾回收器(GC)的記憶體釋放 = 垃圾回收演算法,主要包括:

1.png

具體介紹如下

1.png

5、常見的記憶體泄露原因 & 解決方案

常見引發記憶體泄露原因主要有:

  1. 集合類

  2. Static關鍵字修飾的成員變數

  3. 非靜態內部類 / 匿名類

  4. 資源物件使用後未關閉

下麵,我將詳細介紹每個引發記憶體泄露的原因

5.1 集合類

 

  • 記憶體泄露原因
    集合類 添加元素後,仍取用著 集合元素物件,導致該集合元素物件不可被回收,從而 導致記憶體泄漏
    實體演示:

// 通過 迴圈申請Object 物件 & 將申請的物件逐個放入到集合List
List objectList = new ArrayList<>();        
       for (int i = 0; i 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
// 雖釋放了集合元素取用的本身:o=null)
// 但集合List 仍然取用該物件,故垃圾回收器GC 依然不可回收該物件
  • 解決方案
    集合類 添加集合元素物件 後,在使用後必須從集合中刪除

由於1個集合中有許多元素,故最簡單的方法 = 清空集合物件 & 設置為null

 // 釋放objectList
        objectList.clear();
        objectList=null;

 

5.2 Static 關鍵字修飾的成員變數

 

  • 儲備知識
    被 Static 關鍵字修飾的成員變數的生命周期 = 應用程式的生命周期

  • 泄露原因
    若使被 Static 關鍵字修飾的成員變數 取用耗費資源過多的實體(如Context),則容易出現該成員變數的生命周期 > 取用實體生命周期的情況,當取用實體需結束生命周期銷毀時,會因靜態變數的持有而無法被回收,從而出現記憶體泄露
    實體講解:

public class ClassName {
 // 定義1個靜態變數
 private static Context mContext;
 //...
// 取用的是Activity的context
 mContext = context; 

// 當Activity需銷毀時,由於mContext = 靜態 & 生命周期 = 應用程式的生命周期,故 Activity無法被回收,從而出現記憶體泄露
}
  • 解決方案

  1. 儘量避免 Static 成員變數取用資源耗費過多的實體(如 Context)

若需取用 Context,則儘量使用Applicaiton的Context

  1. 使用 弱取用(WeakReference) 代替 強取用 持有實體

註:靜態成員變數有個非常典型的例子 = 單例樣式

  • 儲備知識
    單例樣式 由於其靜態特性,其生命周期的長度 = 應用程式的生命周期

  • 泄露原因
    若1個物件已不需再使用 而單例物件還持有該物件的取用,那麼該物件將不能被正常回收 從而 導致記憶體泄漏

實體演示:

// 創建單例時,需傳入一個Context
// 若傳入的是Activity的Context,此時單例 則持有該Activity的取用
// 由於單例一直持有該Activity的取用(直到整個應用生命周期結束),即使該Activity退出,該Activity的記憶體也不會被回收
// 特別是一些龐大的Activity,此處非常容易導致OOM

public class SingleInstanceClass {    
    private static SingleInstanceClass instance;    
    private Context mContext;    
    private SingleInstanceClass(Context context) {        
        this.mContext = context; // 傳遞的是Activity的context
    }  

    public SingleInstanceClass getInstance(Context context) {        
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }        
        return instance;
    }
}
  • 解決方案
    單例樣式取用的物件的生命周期 = 應用的生命周期

如上述實體,應傳遞Application的Context,因Application的生命周期 = 整個應用的生命周期

public class SingleInstanceClass {    
    private static SingleInstanceClass instance;    
    private Context mContext;    
    private SingleInstanceClass(Context context) {        
        this.mContext = context.getApplicationContext(); // 傳遞的是Application 的context
    }    

    public SingleInstanceClass getInstance(Context context) {        
        if (instance == null) {
            instance = new SingleInstanceClass(context);
        }        
        return instance;
    }
}

 

5.3 非靜態內部類 / 匿名類

 

  • 儲備知識
    非靜態內部類 / 匿名類 預設持有 外部類的取用;而靜態內部類則不會
  • 常見情況
    3種,分別是:非靜態內部類的實體  = 靜態、多執行緒、訊息傳遞機制(Handler)

5.3.1 非靜態內部類的實體  = 靜態

  • 泄露原因
    若 非靜態內部類所創建的實體 = 靜態(其生命周期 = 應用的生命周期),會因 非靜態內部類預設持有外部類的取用 而導致外部類無法釋放,最終 造成記憶體泄露

即 外部類中 持有 非靜態內部類的靜態物件
實體演示:

// 背景:
   a. 在啟動頻繁的Activity中,為了避免重覆創建相同的資料資源,會在Activity內部創建一個非靜態內部類的單例
   b. 每次啟動Activity時都會使用該單例的資料

public class TestActivity extends AppCompatActivity {  

    // 非靜態內部類的實體的取用
    // 註:設置為靜態  
    public static InnerClass innerClass = null; 

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {        
        super.onCreate(savedInstanceState);   

        // 保證非靜態內部類的實體只有1個
        if (innerClass == null)
            innerClass = new InnerClass();
    }

    // 非靜態內部類的定義    
    private class InnerClass {        
        //...
    }
}

// 造成記憶體泄露的原因:
    // a. 當TestActivity銷毀時,因非靜態內部類單例的取用(innerClass)的生命周期 = 應用App的生命周期、持有外部類TestActivity的取用
    // b. 故 TestActivity無法被GC回收,從而導致記憶體泄漏
  • 解決方案

  1. 將非靜態內部類設置為:靜態內部類(靜態內部類預設不持有外部類的取用)

  2. 該內部類抽取出來封裝成一個單例

  3. 儘量 避免 非靜態內部類所創建的實體 = 靜態

若需使用Context,建議使用 Application 的 Context

5.3.2 多執行緒:AsyncTask、實現Runnable接口、繼承Thread類

  • 儲備知識
    多執行緒的使用方法 = 非靜態內部類 / 匿名類;即 執行緒類 屬於 非靜態內部類 / 匿名類

  • 泄露原因
    當 工作執行緒正在處理任務 & 外部類需銷毀時, 由於 工作執行緒實體 持有外部類取用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 記憶體泄露

多執行緒主要使用的是:AsyncTask、實現Runnable接口 & 繼承Thread類
前3者記憶體泄露的原理相同,此處主要以繼承Thread類 為例說明

實體演示

/** 
     * 方式1:新建Thread子類(內部類)
     */  
        public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            // 通過創建的內部類 實現多執行緒
            new MyThread().start();

        }
        // 自定義的Thread子類
        private class MyThread extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執行了多執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   /** 
     * 方式2:匿名Thread內部類
     */ 
     public class MainActivity extends AppCompatActivity {

    public static final String TAG = "carson:";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 通過匿名內部類 實現多執行緒
        new Thread() {
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執行了多執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }.start();
    }
}

/** 
  * 分析:記憶體泄露原因
  */ 
  // 工作執行緒Thread類屬於非靜態內部類 / 匿名內部類,運行時預設持有外部類的取用
  // 當工作執行緒運行時,若外部類MainActivity需銷毀
  // 由於此時工作執行緒類實體持有外部類的取用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 記憶體泄露
  • 解決方案
    從上面可看出,造成記憶體泄露的原因有2個關鍵條件:

  1. 存在 ”工作執行緒實體 持有外部類取用“ 的取用關係

  2. 工作執行緒實體的生命周期 > 外部類的生命周期,即工作執行緒仍在運行 而 外部類需銷毀
    解決方案的思路 = 使得上述任1條件不成立 即可。

// 共有2個解決方案:靜態內部類 & 當外部類結束生命周期時,強制結束執行緒
// 具體描述如下

   /** 
     * 解決方式1:靜態內部類
     * 原理:靜態內部類 不預設持有外部類的取用,從而使得 “工作執行緒實體 持有 外部類取用” 的取用關係 不復存在
     * 具體實現:將Thread的子類設置成 靜態內部類
     */  
        public class MainActivity extends AppCompatActivity {

        public static final String TAG = "carson:";
        @Override
        public void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);

            // 通過創建的內部類 實現多執行緒
            new MyThread().start();

        }
        // 分析1:自定義Thread子類
        // 設置為:靜態內部類
        private static class MyThread extends Thread{
            @Override
            public void run() {
                try {
                    Thread.sleep(5000);
                    Log.d(TAG, "執行了多執行緒");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

   /** 
     * 解決方案2:當外部類結束生命周期時,強制結束執行緒
     * 原理:使得 工作執行緒實體的生命周期 與 外部類的生命周期 同步
     * 具體實現:當 外部類(此處以Activity為例) 結束生命周期時(此時系統會呼叫onDestroy()),強制結束執行緒(呼叫stop())
     */ 
     @Override
    protected void onDestroy() {
        super.onDestroy();
        Thread.stop();
        // 外部類Activity生命周期結束時,強制結束執行緒
    }

5.3.3 訊息傳遞機制:Handler

Android 記憶體泄露:詳解 Handler 記憶體泄露的原因與解決方案
https://www.jianshu.com/p/031515d8a7ca

5.4 資源物件使用後未關閉

 

  • 泄露原因
    對於資源的使用(如 廣播BraodcastReceiver、檔案流File、資料庫游標Cursor、圖片資源Bitmap等),若在Activity銷毀時無及時關閉 / 註銷這些資源,則這些資源將不會被回收,從而造成記憶體泄漏

  • 解決方案
    在Activity銷毀時 及時關閉 / 註銷資源

// 對於 廣播BraodcastReceiver:註銷註冊
unregisterReceiver()

// 對於 檔案流File:關閉流
InputStream / OutputStream.close()

// 對於資料庫游標cursor:使用後關閉游標
cursor.close()

// 對於 圖片資源Bitmap:Android分配給圖片的記憶體只有8M,若1個Bitmap物件占記憶體較多,當它不再被使用時,應呼叫recycle()回收此物件的像素所占用的記憶體;最後再賦為null 
Bitmap.recycle();
Bitmap = null;

// 對於動畫(屬性動畫)
// 將動畫設置成無限迴圈播放repeatCount = “infinite”後
// 在Activity退出時記得停止動畫

 

5.5 其他使用

 

  • 除了上述4種常見情況,還有一些日常的使用會導致記憶體泄露

  • 主要包括:Context、WebView、Adapter,具體介紹如下

1.png

 

5.6 總結


下麵,我將用一張圖總結Android中記憶體泄露的原因 & 解決方案

1.png

6、輔助分析記憶體泄露的工具

    • 哪怕完全瞭解 記憶體泄露的原因,但難免還是會出現記憶體泄露的現象

    • 下麵將簡單介紹幾個主流的分析記憶體泄露的工具,分別是

 

  1. MAT(Memory Analysis Tools)

  2. Heap Viewer

  3. Allocation Tracker

  4. Android Studio 的 Memory Monitor

  5. LeakCanary

 

6.1 MAT(Memory Analysis Tools)

 

  • 定義:一個Eclipse的 Java Heap 記憶體分析工具 ->>下載地址

  • 作用:查看當前記憶體占用情況

通過分析 Java 行程的記憶體快照 HPROF 分析,快速計算出在記憶體中物件占用的大小,查看哪些物件不能被垃圾收集器回收 & 可通過視圖直觀地查看可能造成這種結果的物件

具體使用:MAT使用攻略

6.2 Heap Viewer

定義:一個的 Java Heap 記憶體分析工具
作用:查看當前記憶體快照

可查看 分別有哪些型別的資料在堆記憶體總 & 各種型別資料的占比情況

具體使用:Heap Viewer使用攻略

6.3 Allocation Tracker

簡介:一個記憶體追蹤分析工具
作用:追蹤記憶體分配信息,按順序排列
具體使用:Allocation Tracker使用攻略

6.4 Memory Monitor

簡介:一個 Android Studio 自帶 的圖形化檢測記憶體工具
作用:跟蹤系統 / 應用的記憶體使用情況。核心功能如下

1.png

具體使用:Android Studio 的 Memory Monitor使用攻略

6.5 LeakCanary

簡介:一個square出品的Android開源庫 ->>下載地址
作用:檢測記憶體泄露
具體使用:
https://www.liaohuqiu.net/cn/posts/leak-canary/

7、總結

本文 全面介紹了記憶體泄露的本質、原因 & 解決方案,希望大家在開發時儘量避免出現記憶體泄露

赞(0)

分享創造快樂