Android 中記憶體洩漏的原因和解決方案
前言
之前研究過一段時間關於 Android 記憶體洩漏的知識,大致瞭解了導致記憶體洩漏的一些原因,但是沒有深入去探究,很多細節也理解的不夠透徹,基本上處於一種似懂非懂的狀態,最近又研究了一波,發現有很多新的收穫,遂在此記錄一些心得體會。
首先引用一下開源專案 LearningNotes 中關於 Java 記憶體分配策略和 Java 是如何管理記憶體的說明。
Java 記憶體分配策略
Java 程式執行時的記憶體分配策略有三種,分別是靜態分配,棧式分配,和堆式分配,對應的,三種儲存策略使用的記憶體空間主要分別是靜態儲存區(也稱方法區)、棧區和堆區。
● 靜態儲存區(方法區):主要存放靜態資料、全域性 static 資料和常量。這塊記憶體在程式編譯時就已經分配好,並且在程式整個執行期間都存在。
● 棧區 :當方法被執行時,方法體內的區域性變數(其中包括基礎資料型別、物件的引用)都在棧上建立,並在方法執行結束時這些區域性變數所持有的記憶體將會自動被釋放。因為棧記憶體分配運算內置於處理器的指令集中,效率很高,但是分配的記憶體容量有限。
● 堆區 : 又稱動態記憶體分配,通常就是指在程式執行時直接 new 出來的記憶體,也就是物件的例項。這部分記憶體在不使用時將會由 Java 垃圾回收器來負責回收。
棧與堆的區別
在方法體內定義的(區域性變數)一些基本型別的變數和物件的引用變數都是在方法的棧記憶體中分配的。當在一段方法塊中定義一個變數時,Java 就會在棧中為該變數分配記憶體空間,當超過該變數的作用域後,該變數也就無效了,分配給它的記憶體空間也將被釋放掉,該記憶體空間可以被重新使用。
堆記憶體用來存放所有由 new 建立的物件(包括該物件其中的所有成員變數)和陣列。在堆中分配的記憶體,將由 Java 垃圾回收器來自動管理。在堆中產生了一個數組或者物件後,還可以在棧中定義一個特殊的變數,這個變數的取值等於陣列或者物件在堆記憶體中的首地址,這個特殊的變數就是我們上面說的引用變數。我們可以通過這個引用變數來訪問堆中的物件或者陣列。
舉個例子:
public class Sample {
int s1 = 0;
Sample mSample1 = new Sample();
public void method() {
int s2 = 1;
Sample mSample2 = new Sample();
}
}
Sample mSample3 = new Sample();
Sample 類的區域性變數 s2 和引用變數 mSample2 都是存在於棧中,但 mSample2 指向的物件是存在於堆上的。 mSample3 指向的物件實體存放在堆上,包括這個物件的所有成員變數 s1 和 mSample1,而它自己存在於棧中。
結論:
區域性變數的基本資料型別和引用儲存於棧中,引用的物件實體儲存於堆中。—— 因為它們屬於方法中的變數,生命週期隨方法而結束。
成員變數全部儲存與堆中(包括基本資料型別,引用和引用的物件實體)—— 因為它們屬於類,類物件終究是要被new出來使用的。
Java是如何管理記憶體
Java的記憶體管理就是物件的分配和釋放問題。在 Java 中,程式設計師需要通過關鍵字 new 為每個物件申請記憶體空間 (基本型別除外),所有的物件都在堆 (Heap)中分配空間。另外,物件的釋放是由 GC 決定和執行的。在 Java 中,記憶體的分配是由程式完成的,而記憶體的釋放是由 GC 完成的,這種收支兩條線的方法確實簡化了程式設計師的工作。但同時,它也加重了JVM的工作。這也是 Java 程式執行速度較慢的原因之一。因為,GC 為了能夠正確釋放物件,GC 必須監控每一個物件的執行狀態,包括物件的申請、引用、被引用、賦值等,GC 都需要進行監控。
監視物件狀態是為了更加準確地、及時地釋放物件,而釋放物件的根本原則就是該物件不再被引用。
為了更好理解 GC 的工作原理,我們可以將物件考慮為有向圖的頂點,將引用關係考慮為圖的有向邊,有向邊從引用者指向被引物件。另外,每個執行緒物件可以作為一個圖的起始頂點,例如大多程式從 main 程序開始執行,那麼該圖就是以 main 程序頂點開始的一棵根樹。在這個有向圖中,根頂點可達的物件都是有效物件,GC將不回收這些物件。如果某個物件 (連通子圖)與這個根頂點不可達(注意,該圖為有向圖),那麼我們認為這個(這些)物件不再被引用,可以被 GC 回收。 以下,我們舉一個例子說明如何用有向圖表示記憶體管理。對於程式的每一個時刻,我們都有一個有向圖表示JVM的記憶體分配情況。以下右圖,就是左邊程式執行到第6行的示意圖。
Java使用有向圖的方式進行記憶體管理,可以消除引用迴圈的問題,例如有三個物件,相互引用,只要它們和根程序不可達的,那麼GC也是可以回收它們的。這種方式的優點是管理記憶體的精度很高,但是效率較低。另外一種常用的記憶體管理技術是使用計數器,例如COM模型採用計數器方式管理構件,它與有向圖相比,精度行低(很難處理迴圈引用的問題),但執行效率很高。
什麼是Java中的記憶體洩漏
在Java中,記憶體洩漏就是存在一些被分配的物件,這些物件有下面兩個特點,首先,這些物件是可達的,即在有向圖中,存在通路可以與其相連;其次,這些物件是無用的,即程式以後不會再使用這些物件。如果物件滿足這兩個條件,這些物件就可以判定為Java中的記憶體洩漏,這些物件不會被GC所回收,然而它卻佔用記憶體。
(以上內容引用自開源專案LearningNotes)
Android 中記憶體洩漏的原因
在 Android 中記憶體洩漏的原因其實和在 Java 中是一樣的,即某個物件已經不需要再用了,但是它卻沒有被系統所回收,一直在記憶體中佔用著空間,而導致它無法被回收的原因大多是由於它被一個生命週期更長的物件所引用。其實要分析 Android 中的記憶體洩漏的原因非常簡單,只要理解一句話,那就是 生命週期較長的物件持有生命週期較短的物件的引用。
舉個例子,如果一個 Activity 被一個單例物件所引用,那麼當退出這個 Activity 時,由於單例的物件依然存在(單例物件的生命週期跟整個 App 的生命週期一致),而單例物件又持有 Activity 的引用,這就導致了此 Activity 無法被回收,從而造成記憶體洩漏。
知道了記憶體洩漏的根本原因,再分析為什麼會出現記憶體洩漏就很簡單了,下面就針對一些常見的記憶體洩漏進行分析。
單例造成的記憶體洩漏
剛才已經分析過了,假設有一個單例是這樣的
public class SingleTon {
private static SingleTon singleTon;
private Context context;
private SingleTon(Context context) {
this.context = context;
}
public static SingleTon getInstance(Context context) {
if (singleTon == null) {
synchronized (SingleTon.class) {
if (singleTon == null) {
singleTon = new SingleTon(context);
}
}
}
return singleTon;
}
}
這是單例模式餓漢式的雙重校驗鎖的寫法,這裡的 singleTon 持有 Context 物件,如果 Activity 中呼叫 getInstance 方法並傳入 this 時,singleTon 就持有了此 Activity 的引用,當退出 Activity 時,Activity 就無法回收,造成記憶體洩漏,所以應該修改它的構造方法
private SingleTon(Context context) {
this.context = context.getApplicationContext();
}
通過 getApplicationContext 來獲取 Application 的 Context,讓它被單例持有,這樣退出 Activity 時,Activity 物件就能正常被回收了,而 Application 的 Context 的生命週期和單例的生命週期是一致的,所有再整個 App 執行過程中都不會造成記憶體洩漏。
非靜態內部類造成的記憶體洩漏
我們知道,非靜態內部類會持有外部類的引用,如果這個非靜態的內部類的生命週期比它的外部類的生命週期長,那麼當銷燬外部類的時候,它無法被回收,就會造成記憶體洩漏。
外部類中持有非靜態內部類的靜態物件
假設 Activity 的程式碼是這樣的
public class MainActivity extends AppCompatActivity {
private static Test test;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
if (test == null) {
test = new Test();
}
}
private class Test {
}
}
這個其實和單例的原理是一樣的,由於靜態物件 test 的生命週期和整個應用的生命週期一致,而非靜態內部類 Test 持有外部類 MainActivity 的引用,導致 MainActivity 退出的時候不能被回收,從而造成記憶體洩漏,解決的方法也很簡單,把 test 改成非靜態,這樣 test 的生命週期和 MainActivity 是一樣的了,就避免了記憶體洩漏。或者也可以把 Test 改成靜態內部類,讓 test 不持有 MainActivity 的引用,不過一般沒有這種操作。
Handler 或 Runnable 作為非靜態內部類
handler 和 runnable 都有定時器的功能,當它們作為非靜態內部類的時候,同樣會持有外部類的引用,如果它們的內部有延遲操作,在延遲操作還沒有發生的時候,銷燬了外部類,那麼外部類物件無法回收,從而造成記憶體洩漏,假設 Activity 的程式碼如下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
}
}, 10 * 1000);
}
}
上面的程式碼中,Handler 和 Runnable 作為匿名內部類,都會持有 MainActivity 的引用,而它們內部有一個 10 秒鐘的定時器,如果在開啟 MainActivity 的 10 秒內關閉了 MainActivity,那麼由於 Handler 和 Runnable 的生命週期比 MainActivity 長,會導致 MainActivity 無法被回收,從而造成記憶體洩漏。
那麼應該如何避免記憶體洩漏呢?這裡的一般套路就是把 Handler 和 Runnable 定義為靜態內部類,這樣它們就不再持有 MainActivity 的引用了,從而避免了記憶體洩漏
public class MainActivity extends AppCompatActivity {
private Handler handler;
private Runnable runnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new TestHandler();
runnable = new TestRunnable();
handler.postDelayed(runnable, 10 * 1000);
}
private static class TestHandler extends Handler {
}
private static class TestRunnable implements Runnable {
@Override
public void run() {
Log.d(TAG, "run: ");
}
}
private static final String TAG = "MainActivity";
}
最好再在 onDestory 呼叫 handler 的 removeCallbacks 方法來移除 Message,這樣不但能避免記憶體洩漏,而且在退出 Activity 時取消了定時器,保證 10 秒以後也不會執行 run 方法
@Override
protected void onDestroy() {
super.onDestroy();
handler.removeCallbacks(runnable);
}
還有一種特殊情況,如果 Handler 或者 Runnable 中持有 Context 物件,那麼即使使用靜態內部類,還是會發生記憶體洩漏
public class MainActivity extends AppCompatActivity {
private Handler handler;
private Runnable runnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new TestHandler(this);
runnable = new TestRunnable();
handler.postDelayed(runnable, 10 * 1000);
}
private static class TestHandler extends Handler {
private Context context;
private TestHandler(Context context) {
this.context = context;
}
}
private static class TestRunnable implements Runnable {
@Override
public void run() {
Log.d(TAG, "run: ");
}
}
private static final String TAG = "MainActivity";
}
上面的程式碼,使用 https://github.com/square/leakcanary leakcanary 工具會發現依然會發生記憶體洩漏,而且造成記憶體洩漏的原因和之前用非靜態內部類是一樣的,那麼為什麼會出現這種情況呢?
這是由於在 Handler 中持有 Context 物件,而這個 Context 物件是通過 TestHandler 的構造方法傳入的,它是一個 MainActivity 物件,也就是說,雖然 TestHandler 作為靜態內部類不會持有外部類 MainActivity 的引用,但是我們在呼叫它的構造方法時,自己傳入了 MainActivity 的物件,從而 handler 物件持有了 MainActivity 的引用,handler 的生命週期比 MainActivity 的生命週期長,因此會造成記憶體洩漏,這種情況可以使用弱引用的方式來引用 Context 來避免記憶體洩漏,程式碼如下
public class MainActivity extends AppCompatActivity {
private Handler handler;
private Runnable runnable;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new TestHandler(new WeakReference<Context>(this));
runnable = new TestRunnable();
handler.postDelayed(runnable, 10 * 1000);
}
private static class TestHandler extends Handler {
private Context context;
private TestHandler(WeakReference<Context> weakContext) {
context = weakContext.get();
}
}
private static class TestRunnable implements Runnable {
@Override
public void run() {
Log.d(TAG, "run: ");
}
}
private static final String TAG = "MainActivity";
}
其他的記憶體洩漏情況
還有一些其他的會導致記憶體洩漏的情況,比如 BraodcastReceiver 未取消註冊,InputStream 未關閉等,這類記憶體洩漏非常簡單,只要在平時寫程式碼時多多注意即可避免。
總結
通過前面的分析我們可以發現,造成 Android 記憶體洩漏的最根本原因就是 生命週期較長的物件持有生命週期較短的物件的引用 ,只要理解了這一點,記憶體洩漏問題就可迎刃而解了。
應用
在 MVP 的架構中,通常 Presenter 要同時持有 View 和 Model 的引用,如果在 Activity 退出的時候,Presenter 正在進行一個耗時操作,那麼 Presenter 的生命週期會比 Activity 長,導致 Activity 無法回收,造成記憶體洩漏
public class MainActivity extends AppCompatActivity implements TestView {
private Presenter presenter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
presenter = new Presenter(this);
presenter.request();
}
}
public class Presenter {
private TestView view;
private Model model;
public Presenter(TestView view) {
this.view = view;
model = new Model();
}
public void request() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10 * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(TAG, "run: ");
}
}).start();
}
private static final String TAG = "Presenter";
}
上面的程式碼中,假設 request 是一個需要耗時 10 秒的操作,那麼在 10 秒之內如果退出 Activity 就會記憶體洩漏。
解決方法也很簡單,在 onDestory 方法中把 presenter 中的 view 物件置為空就可以了
@Override
protected void onDestroy() {
super.onDestroy();
presenter.detachView();
}
public void detachView() {
view = null;
}
也就是在退出 Activity 的時候,讓 Presenter 不再持有 Activity 的引用,避免了記憶體洩漏。
原文釋出時間為:2018-11-21
本文作者:Zackratos
本文來自雲棲社群合作伙伴“ ofollow,noindex">安卓巴士Android開發者門戶 ”,瞭解相關資訊可以關注“ 安卓巴士Android開發者門戶 ”。