Android中常見的記憶體洩漏之上下文物件
阿新 • • 發佈:2019-02-04
雖然現在手機的記憶體不斷增大,但Android為了實現不同應用間執行隔離,不至於相互影響,所以對單個應用最大可使用的記憶體做出了限制。限制大小在不同手機裝置和ROM上都可能不一樣。如Android界的第一款手機HTC G1是16MB,後來的Nexus One是32MB。所以即使手機記憶體不斷變大,但你開發的應用可使用的記憶體空間並沒有增大很多,這也需要你開發時多注意注意記憶體問題,遵從最少使用記憶體的原則,避免記憶體洩漏的發生,這樣不但能讓你的應用避免被系統無故殺死,還能讓使用者使用更加流暢。
記憶體洩漏的產生
Android的虛擬機器機制模仿JVM,所以也有垃圾回收機制。Android虛擬機器中把記憶體分為兩部分,一部分為棧空間,儲存一些全域性引用和靜態變數等值,該空間的分配與回收由系統機制決定,垃圾回收不作用在這塊區域;另一部分為堆空間,裡面儲存是物件的例項,需要開發者主動建立,垃圾回收主要作用在這部分,回收的一個主要策略是檢測堆中的物件在棧空間有無對應的引用。如果沒有引用指向它,則會被優先回收,如果有引用指向則不會被回收。所以如果開發者沒有在適當的時間把一個物件的引用設定為null,則就會可能會產生記憶體洩漏。在Android中最常見的一個記憶體洩漏問題就是長時間持有Context。Context在Android中有非常大的作用,比如用來獲取資源,所以基本上所有的檢視都需要獲得Context才能被建立。使用不當則很可能造成記憶體洩漏。
Android中記憶體洩漏表現
你開發了一個應用,剛開始使用起來還挺流暢,但隨著使用時間變長,應用就變得越來越慢,最後導致使用者不得重啟應用才能繼續使用。這就很可能出現了記憶體洩漏。就像上面提到的,如果說一個靜態變數持有了一個Activity的引用,使用者開啟該Activity,會建立一個Activity的例項,此時即使你關閉該Activity,雖然它不再顯示,但它的例項一直會在記憶體中存在,因為有一個靜態變數一直指向它,導致它的記憶體空間就不會被當做垃圾回收。想想這個Activity中可能包含很多屬性,很多檢視的資訊,它未被釋放,會浪費很多記憶體空間。下面我們從兩個個例子入手,講解下記憶體洩漏和解決辦法。
舉一個例子
以上使用一個靜態變數來儲存一個drawable。從上分析可以看到,一個TextView的區域性變數持有了本Activity的引用,因為label是區域性變數,所以並不會引起記憶體洩漏。但緊接著下面,使用了label.setBackgroundDrawable(sBackground); 有人可能就會想,這也沒啥問題啊,即使sBackground作為一個靜態變數,持有了一個drawable,這塊記憶體不會被釋放,但這塊記憶體畢竟沒有持有整個Activity的引用。但實際上你錯了。我們來看下View.java中的setBackgroundDrawable原始碼,原始碼位置在 (frameworks/base/core/java/android/view/View.java)
其中有一個background.setCallback(this);,所以這就導致這個靜態變數指向的物件又持有了TextView這個物件的引用。這樣,因為是靜態變數,像我上一小節所說的,靜態變數的生命週期基本和應用同週期,它持有了TextView物件引用,所以TextView不會被回收,然後TextView又持有了整個Activity的引用,所以最後就導致整個Activity在關閉後也不會被系統回收。 當然解決此種問題的方法非常簡單,就是把sBackground換成非靜態變數就行,這樣當Activity關閉後,回收機制就能判斷,這個Activity的空間不會被使用到了,所以就啟動GC。 另一個例子
private static Drawable sBackground; @Override protected void onCreate(Bundle state) { super.onCreate(state); TextView label = new TextView(this); label.setText("Leaks are bad"); if (sBackground == null) { sBackground = getDrawable(R.drawable.large_bitmap); } label.setBackgroundDrawable(sBackground); setContentView(label); }
以上使用一個靜態變數來儲存一個drawable。從上分析可以看到,一個TextView的區域性變數持有了本Activity的引用,因為label是區域性變數,所以並不會引起記憶體洩漏。但緊接著下面,使用了label.setBackgroundDrawable(sBackground); 有人可能就會想,這也沒啥問題啊,即使sBackground作為一個靜態變數,持有了一個drawable,這塊記憶體不會被釋放,但這塊記憶體畢竟沒有持有整個Activity的引用。但實際上你錯了。我們來看下View.java中的setBackgroundDrawable原始碼,原始碼位置在 (frameworks/base/core/java/android/view/View.java)
public void setBackgroundDrawable(Drawable background) {
...
if (background != null) {
...
background.setCallback(this);
...
} else {
...
}
...
}
其中有一個background.setCallback(this);,所以這就導致這個靜態變數指向的物件又持有了TextView這個物件的引用。這樣,因為是靜態變數,像我上一小節所說的,靜態變數的生命週期基本和應用同週期,它持有了TextView物件引用,所以TextView不會被回收,然後TextView又持有了整個Activity的引用,所以最後就導致整個Activity在關閉後也不會被系統回收。 當然解決此種問題的方法非常簡單,就是把sBackground換成非靜態變數就行,這樣當Activity關閉後,回收機制就能判斷,這個Activity的空間不會被使用到了,所以就啟動GC。 另一個例子
下面我們再舉一個非常常見的例子,Android開發者很喜歡用單例模式,但有些開發者不注意就可能導致記憶體洩漏,如下:
private static DaVinci sDaVinci = null;
public static DaVinci with(Context context) {
if ( sDaVinci == null ) {
sDaVinci = new DaVinci(context);
}
return sDaVinci;
}
大家可能一時覺得這沒啥問題啊,但這並不是一個好的寫法,因為這可能讓使用者在使用時把一個Activity的Context傳入,導致讓一個單例持有了這個Activity的Context引用,造成記憶體洩漏。一個比較好的寫法是使用
sDaVinci = new DaVinci(context.getApplicationContext());。因為Application的生命週期本來就是貫穿整個應用的,所以即使被持有也沒關係。
建議
1,儘量不要用一個生命週期長於Activity的物件來持有Activity的引用。
2,在需要傳入Context的時候儘量考慮使用Application的Context,而不是Activity的。
3,在Activity中儘量避免使用生命週期不受控制的非靜態型別的內部類,可以使用靜態型別的內部類加上弱引用的方式實現。