Android效能最佳化:手把手帶你全面瞭解 記憶體泄露 & 解決方案

來源:互聯網
上載者:User

標籤:inter   等級   內建   div   排列   return   情況   private   簡單   

. 簡介

即 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(www.famenjie.com);

}
// 自訂的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(www.huayyule.com) {
super.onDestroy();
Thread.stop(www.baqist.cn/);
// 外部類Activity生命週期結束時,強制結束線程
5.3.3 訊息傳遞機制:Handler

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

5.4 資來源物件使用後未關閉

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

解決方案
在Activity銷毀時 及時關閉 / 登出資源

// 對於 廣播BraodcastReceiver:登出註冊
unregisterReceiver(www.dongfan178.com )

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

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

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

// 對於動畫(屬性動畫)
// 將動畫設定成無限迴圈播放repeatCount = “infinite”後
// 在Activity退出時記得停止動畫
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 內建 的圖形化檢測記憶體工具
作用:跟蹤系統 / 應用的記憶體使用量情況。核心功能如下

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

6.5 LeakCanary

簡介:一個square出品的Android開源庫 ->>
作用:檢測記憶體泄露
具體使用:https:/wwww.dashuju178.com /cn/posts/leak-canary/
7. 總結

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

下一篇文章我將對講解Android 效能最佳化的相關知識,有興趣可以繼續關注Carson_Ho的安卓開發筆記
請幫頂 / 評論點贊!因為你的鼓勵是我寫作的最大動力!

Android效能最佳化:手把手帶你全面瞭解 記憶體泄露 & 解決方案

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.