Android性能優化:手把手帶你全面瞭解 內存泄露 & 解決方案

原文地址:http://blog.csdn.net/carson_ho/article/details/79407707

前言

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

目錄

示意圖


1. 簡介

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

2. 對應用程序的影響

  • 容易使得應用程序發生內存溢出,即 OOM 

    內存溢出 簡介: 
    示意圖


3. 發生內存泄露的本質原因

  • 具體描述

示意圖

  • 特別注意 
    從機制上的角度來說,由於 Java存在垃圾回收機制(GC),理應不存在內存泄露;出現內存泄露的原因僅僅是外部人爲原因 = 無意識地持有對象引用,使得 持有引用者的生命週期 > 被引用者的生命週期

4. 儲備知識:Android 內存管理機制

4.1 簡介

示意圖

下面,將針對回收 進程、對象 、變量的內存分配 & 回收進行詳細講解

4.2 針對進程的內存策略

a. 內存分配策略

由 ActivityManagerService 集中管理 所有進程的內存分配

b. 內存回收策略

  • 步驟1:Application Framework 決定回收的進程類型 
    Android中的進程 是託管的;當進程空間緊張時,會 按進程優先級低->>高的順序 自動回收進程 

    Android將進程分爲5個優先等級,具體如下:


示意圖
  • 步驟2:Linux 內核真正回收具體進程 
    1. ActivityManagerService 對 所有進程進行評分(評分存放在變量adj中)
    2. 更新評分到Linux 內核
    3. Linux 內核完成真正的內存回收 
      此處僅總結流程,這其中的過程複雜,有興趣的讀者可研究系統源碼ActivityManagerService.java

4.2 針對對象、變量的內存策略

  • Android的對於對象、變量的內存策略同 Java
  • 內存管理 = 對象 / 變量的內存分配 + 內存釋放

下面,將詳細講解內存分配 & 內存釋放策略

a. 內存分配策略

  • 對象 / 變量的內存分配 由程序自動 負責
  • 共有3種:靜態分配、棧式分配、 & 堆式分配,分別面向靜態變量、局部變量 & 對象實例
  • 具體介紹如下

示意圖

注:用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();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

b. 內存釋放策略

  • 對象 / 變量的內存釋放 由Java垃圾回收器(GC) / 幀棧 負責
  • 此處主要講解對象分配(即堆式分配)的內存釋放策略 = Java垃圾回收器(GC

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

  • Java垃圾回收器(GC)的內存釋放 = 垃圾回收算法,主要包括:

垃圾收集算法類型

  • 具體介紹如下

總結


5. 常見的內存泄露原因 & 解決方案

  • 常見引發內存泄露原因主要有:

    1. 集合類
    2. Static關鍵字修飾的成員變量
    3. 非靜態內部類 / 匿名類
    4. 資源對象使用後未關閉
  • 下面,我將詳細介紹每個引發內存泄露的原因

5.1 集合類

  • 內存泄露原因 
    集合類 添加元素後,仍引用着 集合元素對象,導致該集合元素對象不可被回收,從而 導致內存泄漏

  • 實例演示

// 通過 循環申請Object 對象 & 將申請的對象逐個放入到集合List
List<Object> objectList = new ArrayList<>();        
       for (int i = 0; i < 10; i++) {
            Object o = new Object();
            objectList.add(o);
            o = null;
        }
// 雖釋放了集合元素引用的本身:o=null)
// 但集合List 仍然引用該對象,故垃圾回收器GC 依然不可回收該對象
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 解決方案 
    集合類 添加集合元素對象 後,在使用後必須從集合中刪除 
    由於1個集合中有許多元素,故最簡單的方法 = 清空集合對象 & 設置爲null
 // 釋放objectList
        objectList.clear();
        objectList=null;
  • 1
  • 2
  • 3

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

  • 儲備知識 
    被 Static 關鍵字修飾的成員變量的生命週期 = 應用程序的生命週期
  • 泄露原因 
    若使被 Static 關鍵字修飾的成員變量 引用耗費資源過多的實例(如Context),則容易出現該成員變量的生命週期 > 引用實例生命週期的情況,當引用實例需結束生命週期銷燬時,會因靜態變量的持有而無法被回收,從而出現內存泄露

  • 實例講解

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

// 當Activity需銷燬時,由於mContext = 靜態 & 生命週期 = 應用程序的生命週期,故 Activity無法被回收,從而出現內存泄露

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 解決方案

    1. 儘量避免 Static 成員變量引用資源耗費過多的實例(如 Context

      若需引用 Context,則儘量使用ApplicaitonContext

    2. 使用 弱引用(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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 解決方案 
    單例模式引用的對象的生命週期 = 應用的生命週期 
    如上述實例,應傳遞ApplicationContext,因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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

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
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 解決方案 
    1. 將非靜態內部類設置爲:靜態內部類(靜態內部類默認不持有外部類的引用)
    2. 該內部類抽取出來封裝成一個單例
    3. 儘量 避免 非靜態內部類所創建的實例 = 靜態 
      若需使用Context,建議使用 Application 的 Context

5.3.2 多線程:AsyncTask、實現Runnable接口、繼承Thread類

  • 儲備知識 
    多線程的使用方法 = 非靜態內部類 / 匿名類;即 線程類 屬於 非靜態內部類 / 匿名類
  • 泄露原因 
    當 工作線程正在處理任務 & 外部類需銷燬時, 由於 工作線程實例 持有外部類引用,將使得外部類無法被垃圾回收器(GC)回收,從而造成 內存泄露

    1. 多線程主要使用的是:AsyncTask、實現Runnable接口 & 繼承Thread
    2. 前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)回收,從而造成 內存泄露
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 解決方案 
    從上面可看出,造成內存泄露的原因有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生命週期結束時,強制結束線程
    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46

5.3.3 消息傳遞機制:Handler

具體請看文章:Android 內存泄露:詳解 Handler 內存泄露的原因

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退出時記得停止動畫
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

5.5 其他使用

  • 除了上述4種常見情況,還有一些日常的使用會導致內存泄露
  • 主要包括:ContextWebViewAdapter,具體介紹如下

示意圖

5.6 總結

下面,我將用一張圖總結Android中內存泄露的原因 & 解決方案

示意圖


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

6.4 Memory Monitor

6.5 LeakCanary


7. 總結

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

示意圖


請幫頂 / 評論點贊!因爲你的鼓勵是我寫作的最大動力!


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章