理解Android中的引用類型

理解Android中的引用類型

Android中的對象有着4種引用類型,垃圾回收器對於不同的引用類型有着不同的處理方式,瞭解這些處理方式有助於我們避免寫出會導致內存泄露的代碼。

出處: Allen’s Zone
作者: Allen Feng

引用

首先我們要理解:什麼是引用(reference)?

在Java中,一切都被視爲對象,引用則是用來操縱對象的途徑。

對象和引用之間的關係可以用遙控器(引用)來操縱電視機(對象)這個場景來理解。只要手持這個遙控器,就能保持與電視機的連接。當我們想要改變頻道或者音量時,實際操控的是遙控器(引用),再由遙控器(引用)來調控電視機(對象),達到操控的目的。

來看一段代碼:

1
2
Car myCar = new Car();
myCar.run();

上面這句話的意思是,創建一個Car的對象,並將這個新建的對象的引用存儲在myCar中,此時myCar就是用來操作這個對象的引用。當我們獲得myCar,就可以使用這個引用去操作對象中的方法或者字段了。

注意,當我們嘗試在一個未指向任何對象的引用上去操作對象時,就會遇到經典的空指針異常(NullPointerException)。可以理解成我們手持遙控器,房間裏卻沒有電視機可與之對象(沒有可以用來操控的對象)。

1
2
Car myCar;
myCar.run();

GC與內存泄露

Java的一個重要優點就是通過垃圾收集器(Garbage Collection,GC)自動管理內存的回收,開發者不需要通過調用函數來釋放內存。在Java中,內存的分配是由程序分配的,而內存的回收是由GC來完成。
GC爲了能夠正確釋放對象,會監控每一個對象的運行狀態,包括對象的申請、引用、被引用、賦值等,GC都需要進行監控。監視對象狀態是爲了更加準確地、及時地釋放對象,而釋放對象的根本原則就是該對象不再被引用

Android中採用了標註與清理(Mark and Sweep)回收算法:

從”GC Roots”集合開始,將內存整個遍歷一次,保留所有可以被GC Roots直接或間接引用到的對象,而剩下的對象都當作垃圾對待並回收。

Android內存的回收管理策略可以用下面的過程來展示:

圖自Google I/O: Memory Management for Android Apps

上面三張圖片描述了GC的遍歷過程。
每個圓形節點代表一個對象(內存資源),箭頭表示對象引用的路徑(可達路徑),黃色表示遍歷後的當前對象與GC Roots存在可達路徑。當圓形節點與GC Roots存在可達路徑的時候,表示當前對象正在被使用,GC不會將其回收。反之,若圓形節點與GC Roots不存在可達路徑,意味着這個對象不再被程序引用,GC可以將之回收。

在Android中,每一個應用程序對應有一個單獨的Dalvik虛擬機實例,而每一個Dalvik虛擬機的大小是固定的(如32M,可以通過ActivityManager.getMemoryClass()獲得)。這意味着我們可以使用的內存不是無節制的。所以即使有着GC幫助我們回收無用內存,還是需要在開發過程中注意對內存的引用。否則,就會導致內存泄露。

結合上文所述,內存泄露指的是:

我們不再需要的對象資源仍然與GC Roots存在可達路徑,導致該資源無法被GC回收。

Android中的對象有着4種引用類型,垃圾回收器對於不同的引用類型有着不同的處理方式,瞭解這些處理方式有助於我們避免寫出會導致內存泄露的代碼。

Strong reference(強引用)

強引用我們最常用的一種引用類型。當我們使用new關鍵字去新建一個對象的時候,創建的就是強引用。

比如:

1
MyObject object = new MyObject();

這段代碼的意思是:一個新的MyObject對象被創建了,並且一個指向它的強引用被存儲在object中。

當一個對象具有強引用,那麼垃圾回收器是絕對不會的回收和銷燬它的。對象的強引用可以在程序中到處傳遞。很多情況下,會同時有多個引用指向同一個對象。

強引用的存在限制了對象在內存中的存活時間。假如對象A中包含了一個對象B的強引用,那麼一般情況下,對象B的存活時間就不會短於對象A。如果對象A沒有顯式的把對象B的引用設爲null的話,就只有當對象A被垃圾回收之後,對象B纔不再有引用指向它,纔可能獲得被垃圾回收的機會。

下面,我們舉一個例子:

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
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask(this).execute();
}
private class MyAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] params) {
// 模擬耗時任務
try {
Thread.sleep(60000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return doSomeStuff();
}
private Object doSomeStuff() {
return new Object();
}
@Override
protected void onPostExecute(Object object) {
super.onPostExecute(object);
// 更新UI
}
}
}

這段代碼裏,MyAsyncTask會跟隨Activity的onCreate去創建並開始執行一個長時間的耗時任務,並在耗時任務完成後去更新MainActivity中的UI。這是一個很常見的使用場景,卻會導致內存泄露問題:

在Java中,非靜態內部類會在其整個生命週期中持有對它外部類的強引用

MainActivity被銷燬時,MyAsyncTask中的耗時任務可能仍沒有執行完成,所以MyAsyncTask會一直存活。此時,由於MyAsyncTask持有着其外部類,即MainActivity的引用,將導致MainActivity不能被垃圾回收。如果MainActivity中還持有着Bitmap等大對象,反覆進出這個頁面幾次可能就會出現OOM Crash了。

那麼我們如何避免這樣的問題出現呢?請看下文。

WeakReference (弱引用)

弱引用通過類WeakReference來表示。弱引用並不能阻止垃圾回收。如果使用一個強引用的話,只要該引用存在,那麼被引用的對象是不能被回收的。弱引用則沒有這個問題。在垃圾回收器運行的時候,如果對一個對象的所有引用都是弱引用的話,該對象會被回收。

我們調整一下上面例子中的代碼,使用弱引用去避免內存泄露:

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
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyAsyncTask(this).execute();
}
private static class MyAsyncTask extends AsyncTask {
private WeakReference<MainActivity> mainActivity;
public MyAsyncTask(MainActivity mainActivity) {
this.mainActivity = new WeakReference<>(mainActivity);
}
@Override
protected Object doInBackground(Object[] params) {
// 模擬耗時任務
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return doSomeStuff();
}
private Object doSomeStuff() {
//do something to get result
return new Object();
}
@Override
protected void onPostExecute(Object object) {
super.onPostExecute(object);
if (mainActivity.get() != null){
// 更新UI
}
}
}
}

大家可以注意到,主要的不同點在於,我們把MyAsyncTask改爲了靜態內部類,並且其對外部類MainActivity的引用換成了:

1
private WeakReference<MainActivity> mainActivity;

修改之後,當MainActivity destroy的時候,由於MyAsyncTask是通過弱引用的方式持有MainActivity,所以並不會阻止MainActivity被垃圾回收器回收,也就不會有內存泄露產生了。

SoftReference(軟引用)

我們可以把軟引用理解成一種稍強的弱引用。使用類SoftReference來表示。

很多人可能會把弱引用和軟引用搞混,注意他們的區別在於:如果一個對象只具有軟引用,若內存空間足夠,垃圾回收器就不會回收它;如果內存空間不足了,纔會回收這些對象的內存。

而只具有弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它所管轄的內存區域的過程中,一旦發現了只具有弱引用的對象,不管當前內存空間足夠與否,都會回收它的內存。

所以從引用的強度來講: 強引用 > 軟引用 > 弱引用。

表面上看來,軟引用非常適合於創建緩存。當系統內存不足的時候,緩存中的內容是可以被釋放的。

但是,在實踐中,使用軟引用作爲緩存時效率是比較低的,系統並不知道哪些軟引用指向的對象應該被回收,哪些應該被保留。過早被回收的對象會導致不必要的工作,比如Bitmap要重新從SdCard或者網絡上加載到內存。

所以使用軟引用去緩存對象,雖然確實可以避免OOM問題,卻不適用於某些場景。在Android開發中,一種更好的選擇是使用LruCache,LRU是Least Recently Used的縮寫,即“最近最少使用”,它的內部會維護一個固定大小的內存,當內存不足的時候,會根據策略把最近最少使用的數據移除,讓出內存給最新的數據。具體實現有興趣的同學可以自行研究。

PhantomReference(虛引用)

一個只被虛引用持有的對象可能會在任何時候被GC回收。虛引用對對象的生存週期完全沒有影響,也無法通過虛引用來獲取對象實例,僅僅能在對象被回收時,得到一個系統通知(只能通過是否被加入到ReferenceQueue來判斷是否被GC,這也是唯一判斷對象是否被GC的途徑)。

我們都知道,java的Object類裏面有個finalize方法,它的工作原理是這樣的:一旦垃圾回收器準備好釋放對象佔用的內存空間,將首先調用其finalize方法,並且在下一次垃圾回收動作發生時,纔會真正回收對象佔用的內存。但是,問題在於,虛擬機不能保證finalize何時被調用,因爲GC的運行時間是不固定的。

使用虛引用就可以解決這個問題,虛引用主要用來跟蹤對象被垃圾回收的活動,主要用來實現比較精細的內存使用控制,這對於Android設備來說是很有意義的。比如,我們可以在確定一個Bitmap被回收後,再去申請另外一個Bitmap的內存,通過這種方式可以使得程序所消耗的內存維持在一個相對較低且穩定的水平。

發佈了0 篇原創文章 · 獲贊 6 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章