理解Android中的引用類型

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

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

引用

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

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

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

來看一段代碼:

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

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

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

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關鍵字去新建一個對象的時候,創建的就是強引用。

比如:

MyObject object = new MyObject();

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

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

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

下面,我們舉一個例子:

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來表示。弱引用并不能阻止垃圾回收。如果使用一個強引用的話,只要該引用存在,那么被引用的對象是不能被回收的。弱引用則沒有這個問題。在垃圾回收器運行的時候,如果對一個對象的所有引用都是弱引用的話,該對象會被回收。

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

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的引用換成了:

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的內存,通過這種方式可以使得程序所消耗的內存維持在一個相對較低且穩定的水平。

虛引用的使用demo可以參考這篇文章:How to use PhantomReference


Refers:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380

推薦閱讀更多精彩內容