Android效能優化:手把手帶你全面瞭解 記憶體洩露 & 解決方案
前言
- 在
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
核心真正回收具體程式ActivityManagerService
對 所有程式進行評分(評分存放在變數adj
中)- 更新評分到
Linux
核心 - 由
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. 常見的記憶體洩露原因 & 解決方案
常見引發記憶體洩露原因主要有:
- 集合類
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
- 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
解決方案
儘量避免
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 解決方案
單例模式引用的物件的生命週期 = 應用的生命週期如上述例項,應傳遞
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;
}
}
- 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
- 解決方案
- 將非靜態內部類設定為:靜態內部類(靜態內部類預設不持有外部類的引用)
- 該內部類抽取出來封裝成一個單例
- 儘量 避免 非靜態內部類所建立的例項 = 靜態
若需使用
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)回收,從而造成 記憶體洩露
- 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:靜態內部類
* 原理:靜態內部類 不預設持有外部類的引用,從而使得 “工作執行緒例項 持有 外部類引用” 的引用關係 不復存在
* 具體實現:將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種常見情況,還有一些日常的使用會導致記憶體洩露
- 主要包括:
Context
、WebView
、Adapter
,具體介紹如下
5.6 總結
下面,我將用一張圖總結Android
中記憶體洩露的原因 & 解決方案
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
自帶 的圖形化檢測記憶體工具 作用:跟蹤系統 / 應用的記憶體使用情況。核心功能如下
6.5 LeakCanary
- 簡介:一個
square
出品的Android
開源庫 ->>下載地址 - 作用:檢測記憶體洩露
- 具體使用:https://www.liaohuqiu.net/cn/posts/leak-canary/
7. 總結
- 本文 全面介紹了記憶體洩露的本質、原因 & 解決方案,希望大家在開發時儘量避免出現記憶體洩露
- 下一篇文章我將對講解
Android
效能優化的相關知識,有興趣可以繼續關注Carson_Ho的安卓開發筆記
請幫頂 / 評論點贊!因為你的鼓勵是我寫作的最大動力!
相關文章
- Android 效能優化:手把手帶你全面瞭解記憶體洩露Android優化記憶體洩露
- Android效能優化:手把手帶你全面實現記憶體優化Android優化記憶體
- Android 效能優化 - 徹底解決記憶體洩漏Android優化記憶體
- Android 記憶體洩露詳解Android記憶體洩露
- Android 關於記憶體洩露,你必須瞭解的東西Android記憶體洩露
- Android 記憶體洩露優化處理Android記憶體洩露優化
- react 記憶體洩露常見問題解決方案React記憶體洩露
- Android效能最佳化之記憶體洩露Android記憶體洩露
- 解決git記憶體洩露問題Git記憶體洩露
- Android記憶體優化——記憶體洩露檢測分析方法Android優化記憶體洩露
- Android效能優化篇之記憶體優化--記憶體洩漏Android優化記憶體
- 二、Android效能優化之記憶體洩露分析及工具使用Android優化記憶體洩露
- Android效能優化之記憶體洩漏Android優化記憶體
- JAVA記憶體洩露的原因及解決Java記憶體洩露
- Android效能優化你瞭解多少Android優化
- JAVA 記憶體洩露詳解(原因、例子及解決)Java記憶體洩露
- Android 效能優化 - 徹底解決記憶體抖動Android優化記憶體
- 記憶體洩露例項分析 -- Android記憶體優化第四彈記憶體洩露Android優化
- 安卓效能優化之被忽視的記憶體洩露安卓優化記憶體洩露
- .Net記憶體洩露原因及解決辦法記憶體洩露
- 難解決的記憶體洩露(OutOfMemory)問題!!!記憶體洩露
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(上)Android優化記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(下)Android優化記憶體
- Android 效能優化之記憶體洩漏檢測以及記憶體優化(中)Android優化記憶體
- 一篇文章帶你瞭解 Java 自動記憶體管理機制及效能優化Java記憶體優化
- 淺談Android開發中記憶體洩露與優化Android記憶體洩露優化
- Java 記憶體洩露的理解與解決過程Java記憶體洩露
- 手把手教你解決 Flutter engine 記憶體洩漏Flutter記憶體
- Handler記憶體洩漏原因及解決方案記憶體
- 防範JAVA記憶體洩漏解決方案Java記憶體
- 深入瞭解 JavaScript 記憶體洩漏JavaScript記憶體
- Android 輕鬆解決記憶體洩漏Android記憶體
- Android效能優化 - 記憶體優化Android優化記憶體
- android,記憶體優化詳解Android記憶體優化
- Android效能優化之常見的記憶體洩漏Android優化記憶體
- php常駐程式記憶體洩露的簡單解決PHP記憶體洩露
- 小題大做 | Handler記憶體洩露全面分析記憶體洩露
- Android 檢測記憶體洩露Android記憶體洩露