Android效能優化:關於 記憶體洩露 的知識都在這裡了!(文不如圖,圖不如表)
前言
在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. 常見的記憶體洩露原因 & 解決方案
常見引發記憶體洩露原因主要有:
- 集合類
- Static關鍵字修飾的成員變數
- 非靜態內部類 / 匿名類
- 資源物件使用後未關閉
下面,我將詳細介紹每個引發記憶體洩露的原因
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個集合中有許多元素,故最簡單的方法 = 清空集合物件 & 設定為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無法被回收,從而出現記憶體洩露 }
- 解決方案
- 儘量避免 Static 成員變數引用資源耗費過多的例項(如 Context)
若需引用 Context,則儘量使用Applicaiton的Context
- 使用 弱引用(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回收,從而導致記憶體洩漏
- 解決方案
- 將非靜態內部類設定為:靜態內部類(靜態內部類預設不持有外部類的引用)
- 該內部類抽取出來封裝成一個單例
- 儘量 避免 非靜態內部類所建立的例項 = 靜態
若需使用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:靜態內部類 * 原理:靜態內部類 不預設持有外部類的引用,從而使得 “工作執行緒例項 持有 外部類引用” 的引用關係 不復存在 * 具體實現:將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 記憶體洩露的原因與解決方案
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. 輔助分析記憶體洩露的工具
- 哪怕完全瞭解 記憶體洩露的原因,但難免還是會出現記憶體洩露的現象
- 下面將簡單介紹幾個主流的分析記憶體洩露的工具,分別是
- MAT(Memory Analysis Tools)
- Heap Viewer
- Allocation Tracker
- Android Studio 的 Memory Monitor
- 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. 總結
本文 全面介紹了記憶體洩露的本質、原因 & 解決方案,希望大家在開發時儘量避免出現記憶體洩露