寫在前面
雖然現(xiàn)在手機的內(nèi)存不斷增大,但Android為了實現(xiàn)不同應(yīng)用間運行隔離,不至于相互影響,所以對單個應(yīng)用最大可使用的內(nèi)存做出了限制。限制大小在不同手機設(shè)備和ROM上都可能不一樣。如Android界的第一款手機HTC G1是16MB,后來的Nexus One是32MB。所以即使手機內(nèi)存不斷變大,但你開發(fā)的應(yīng)用可使用的內(nèi)存空間并沒有增大很多,這也需要你開發(fā)時多注意注意內(nèi)存問題,遵從最少使用內(nèi)存的原則,避免內(nèi)存泄漏的發(fā)生,這樣不但能讓你的應(yīng)用避免被系統(tǒng)無故殺死,還能讓用戶使用更加流暢。
如果想查看自己應(yīng)用可以使用的最大內(nèi)存空間,可以參考:《Detect application heap size in Android》
如果你實在需要增大自己應(yīng)用的內(nèi)存使用大小,可以參考這篇文章:《How to increase heap size of an android application》
內(nèi)存泄漏的產(chǎn)生
Android的虛擬機機制模仿JVM,所以也有垃圾回收機制。Android虛擬機中把內(nèi)存分為兩部分,一部分為??臻g,存儲一些全局引用和靜態(tài)變量等值,該空間的分配與回收由系統(tǒng)機制決定,垃圾回收不作用在這塊區(qū)域;另一部分為堆空間,里面存儲是對象的實例,需要開發(fā)者主動創(chuàng)建,垃圾回收主要作用在這部分,回收的一個主要策略是檢測堆中的對象在棧空間有無對應(yīng)的引用。如果沒有引用指向它,則會被優(yōu)先回收,如果有引用指向則不會被回收。所以如果開發(fā)者沒有在適當(dāng)?shù)臅r間把一個對象的引用設(shè)置為null,則就會可能會產(chǎn)生內(nèi)存泄漏。在Android中最常見的一個內(nèi)存泄漏問題就是長時間持有Context。Context在Android中有非常大的作用,比如用來獲取資源,所以基本上所有的視圖都需要獲得Context才能被創(chuàng)建。使用不當(dāng)則很可能造成內(nèi)存泄漏。
Android中內(nèi)存泄漏表現(xiàn)
你開發(fā)了一個應(yīng)用,剛開始使用起來還挺流暢,但隨著使用時間變長,應(yīng)用就變得越來越慢,最后導(dǎo)致用戶不得重啟應(yīng)用才能繼續(xù)使用。這就很可能出現(xiàn)了內(nèi)存泄漏。就像上面提到的,如果說一個靜態(tài)變量持有了一個Activity的引用,用戶打開該Activity,會創(chuàng)建一個Activity的實例,此時即使你關(guān)閉該Activity,雖然它不再顯示,但它的實例一直會在內(nèi)存中存在,因為有一個靜態(tài)變量一直指向它,導(dǎo)致它的內(nèi)存空間就不會被當(dāng)做垃圾回收。想想這個Activity中可能包含很多屬性,很多視圖的信息,它未被釋放,會浪費很多內(nèi)存空間。下面我們從兩個個例子入手,講解下內(nèi)存泄漏和解決辦法。
一個例子
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);
}
以上使用一個靜態(tài)變量來保存一個drawable。從上分析可以看到,一個TextView的局部變量持有了本Activity的引用,因為label是局部變量,所以并不會引起內(nèi)存泄漏。但緊接著下面,使用了label.setBackgroundDrawable(sBackground); 有人可能就會想,這也沒啥問題啊,即使sBackground作為一個靜態(tài)變量,持有了一個drawable,這塊內(nèi)存不會被釋放,但這塊內(nèi)存畢竟沒有持有整個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);,所以這就導(dǎo)致這個靜態(tài)變量指向的對象又持有了TextView這個對象的引用。這樣,因為是靜態(tài)變量,像我上一小節(jié)所說的,靜態(tài)變量的生命周期基本和應(yīng)用同周期,它持有了TextView對象引用,所以TextView不會被回收,然后TextView又持有了整個Activity的引用,所以最后就導(dǎo)致整個Activity在關(guān)閉后也不會被系統(tǒng)回收。
當(dāng)然解決此種問題的方法非常簡單,就是把sBackground換成非靜態(tài)變量就行,這樣當(dāng)Activity關(guān)閉后,回收機制就能判斷,這個Activity的空間不會被使用到了,所以就啟動GC。
另一個例子
下面我們再舉一個非常常見的例子,Android開發(fā)者很喜歡用單例模式,但有些開發(fā)者不注意就可能導(dǎo)致內(nèi)存泄漏,如下:
private static DaVinci sDaVinci = null;
public static DaVinci with(Context context) {
if ( sDaVinci == null ) {
sDaVinci = new DaVinci(context);
}
return sDaVinci;
}
大家可能一時覺得這沒啥問題啊,但這并不是一個好的寫法,因為這可能讓用戶在使用時把一個Activity的Context傳入,導(dǎo)致讓一個單例持有了這個Activity的Context引用,造成內(nèi)存泄漏。一個比較好的寫法是使用
sDaVinci = new DaVinci(context.getApplicationContext());。因為Application的生命周期本來就是貫穿整個應(yīng)用的,所以即使被持有也沒關(guān)系。
幾點建議
1,盡量不要用一個生命周期長于Activity的對象來持有Activity的引用。
2,在需要傳入Context的時候盡量考慮使用Application的Context,而不是Activity的。
3,在Activity中盡量避免使用生命周期不受控制的非靜態(tài)類型的內(nèi)部類,可以使用靜態(tài)類型的內(nèi)部類加上弱引用的方式實現(xiàn)。
作者簡介
彭濤(@彭濤me) 致力于讓技術(shù)變得易懂且有趣
個人博客:http://pengtao.me, GitHub地址:https://github.com/CPPAlien