Android開發中應該避免的記憶體洩露
一、背景和目的:
目前許多開發人員在Android開發過程中,較少關注實現細節和記憶體使用,容易會造成記憶體洩露,導致程式OOM。
本文會通過程式碼向大家介紹在Android開發過程中常見的記憶體洩露。
二、常見的記憶體洩露程式碼
1、使用Handler****造成的記憶體問題
在Android開發過程中,Handler是比較常用的,通過Handler傳送Message與主執行緒進行通訊,Message傳送之後是儲存在MessageQueue中的,有些Message並不是馬上被處理的,在Message中存在一個Target,是Handler的一個引用,如果Message在Handler中的存在時間過長,會導致Handler無法被回收。如果Handler非靜態,則會導致相關引用的Activity或者Service不會回收,所以在處理Hanlder之類的內部類的時候,應該要將Handler定義為靜態內部類,同樣在使用HandlerThread的時候也需要注意,我們來看看程式碼:
public class MainActivity extends AppCompatActivity{ @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HandlerThread mThread = new HandlerThread("threadTask",Process.THREAD_PROIORITY_BACKGROUND); mThread.start(); Handler mHander = new Handler(mThread.getLooper()); //TODO... } }
這個程式碼存在洩漏問題,因為HandlerThread內部會不斷的迴圈執行,它不會自己結束,執行緒的生命週期超過了activity生命週期,當橫豎屏切換,HandlerThread執行緒的數量會隨著activity重建次數的增加而增加。
我們應該在onDestroy時將執行緒停止掉:mThread.getLooper().quit();
另外,對於不是HandlerThread的執行緒,也應該確保activity消耗後,執行緒已經終止,可以這樣做:在onDestroy時呼叫mThread.join();
2、使用非靜態內部類的靜態例項
public class MainActivity extends AppCompatActivity{ public class OtherClass{ } private static OtherClass sInstance = null; @Override protected void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); if(sInstance == null){ sInstance = new OtherClass(); } } }
上面的程式碼中的sInstance例項型別為靜態例項,在第一個MainActivityact例項建立時,sInstance會獲得並一直持有activity的引用。當MainAcitivity銷燬後重建,因為sInstance持有activity的引用,所以activity是無法被GC回收的,程序中會存在2個MainActivity例項(activity和重建後的MainActivity例項),這個activity物件就是一個無用的但一直佔用記憶體的物件,即無法回收的垃圾物件。所以,對於lauchMode不是singleInstance的Activity,應該避免在activity裡面例項化其非靜態內部類的靜態例項。
3、在Activity****中使用靜態成員
public class MainActivity extends AppCompatActivity{
private static Drawable sBackground = null;
@Override
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TextView label = new TextView(this);
if(sBackground == null){
sBackground = getDrawable(R.mipmap.ic_launcher);
}
label.setBackgroundDrawable(sBackground);
}
}
由於用靜態成員sBackground 快取了drawable物件,所以activity載入速度會加快,但是這樣做是錯誤的。因為它會導致activity銷燬後無法被系統回收。label.setBackgroundDrawable函式呼叫會將label賦值給sBackground的成員變數。上面程式碼意味著:sBackground(GC Root)會持有TextView物件,而TextView持有Activiy物件。所以導致Activity物件無法被系統回收。
以上2個例子的記憶體洩漏都是因為Activity的引用的生命週期超越了activity物件的生命週期。也就是常說的Context洩漏,想要避免context相關的記憶體洩漏,需要注意以下幾點:
l 不要對activity的context長期引用(activity的引用的生存週期應該和activity的生命週期相同)
l 在可以使用application的context的情況下,儘可能使用application的context來替代和activity相關的context
l 如果一個acitivity的非靜態內部類的生命週期不受控制,那麼我們就應該避免這樣使用。
4、註冊某個物件後未登出
註冊廣播接收器、註冊觀察者等等,比如: 在呼叫registerReceiver後,若未呼叫unregisterReceiver,它會導致BroadcastReceiver不會被unregister而導致記憶體洩露,我們經常會看到類似下面的程式碼:
registerReceiver(new BroadcastReceiver(){
@Override
public void onReceive(Context context,Intent intent){
//TODO...
}
},filter);
5、集合中物件沒清理造成的記憶體洩露
我們通常把一些物件的引用加入到了集合中,當我們不需要該物件時,如果沒有把它的引用從集合中清理掉,這樣這個集合就會越來越大。如果這個集合是static的話,如果物件不斷增大,達到一定的值的時候程式就會OOM
6、資源物件沒關閉造成的記憶體洩露
資源性物件比如(Cursor,File檔案等)往往都用了一些緩衝,我們在不使用的時候,應該及時關閉它們,以便它們的緩衝及時回收記憶體。它們的緩衝不僅存在於Java虛擬機器內,還存在於Java虛擬機器外。如果我們僅僅是把它的引用設定為null,而不關閉它們,往往會造成記憶體洩露。因為有些資源性物件,比如SQLiteCursor(在解構函式finalize(),如果我們沒有關閉它,它自己會調close()關閉),如果我們沒有關閉它,系統在回收它時也會關閉它,但是這樣的效率太低了。因此對於資源性物件在不使用的時候,應該立即呼叫它的close()函式,將其關閉掉,然後再置為null.在我們的程式退出時一定要確保我們的資源性物件已經關閉。
程式中經常會進行查詢資料庫的操作,但是經常會有使用完畢Cursor後沒有關閉的情況。如果我們的查詢結果集比較小,對記憶體的消耗不容易被發現,只有在長時間大量操作的情況下才會復現記憶體問題,這樣就會給以後的測試和問題排查帶來困難和風險。
寫程式碼時,經常會有人忘記呼叫close, 或者因為程式碼邏輯問題狀況導致close未被呼叫。
錯誤的程式碼:
try{
Cursor c = queryCursor();
int a = c.getInt(2);
c.close();
}catch(Exception ex){
}
修正後的程式碼:
Cursor c = null;
try{
c = queryCursor();
int a = c.getInt(2);
c.close();
}catch(Exception ex){
}finally{
if(c!=null){
c.close();
}
}
7、一些不良程式碼成記憶體壓力
有些程式碼並不造成記憶體洩露,但是它們或是對沒使用的記憶體沒進行有效及時的釋放,或是沒有有效的利用已有的物件而是頻繁的申請新記憶體,對記憶體的回收和分配造成很大影響的,容易迫使虛擬機器不得不給該應用程序分配更多的記憶體,增加vm的負擔,造成不必要的記憶體開支。
7.1 Bitmap 使用不當
一、需要及時的銷燬。
雖然,系統能夠確認Bitmap分配的記憶體最終會被銷燬,但是由於它佔用的記憶體過多,所以很可能會超過Java堆的限制。因此,在用完Bitmap時,要及時的recycle掉。recycle並不能確定立即就會將Bitmap釋放掉,但是會給虛擬機器一個暗示:“該圖片可以釋放了”。
二、需要設定一定的取樣率。
有時候,我們要顯示的區域很小,沒有必要將整個圖片都加載出來,而只需要記載一個縮小過的圖片,這時候可以設定一定的取樣率,那麼就可以大大減小佔用的記憶體。如下面的程式碼:
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2; //圖片寬高都為原來的二分之一,圖片為原來的額四分之一
Bitmap bitmap = BitmapFactory.decodeStream(is,null,options);
三、巧妙的運用軟引用(SoftRefrence)
有些時候,我們使用Bitmap後沒有保留對它的引用,因此就無法呼叫Recycle函式。這時候巧妙的運用軟引用,可以使Bitmap在記憶體不足時得到有效的釋放。如下:
SoftReference<Bitmap> bitmap_ref = new SoftReference<Bitmap>(BitmapFactory.decodeStream(is));
if(bitmap_ref.get()!=null){
bitmap_ref.get().recycle();
}
7.2 使用Adapter時,沒有使用快取的 ConvertView
以構造ListView的BaseAdapter為例,在BaseAdapter中提共了方法:
public View getView(int position,View convertView,ViewGroup parent){}
來向ListView提供每一個item所需要的view物件。初始時ListView會從BaseAdapter中根據當前的屏幕布局例項化一定數量的view物件,同時ListView會將這些view物件快取起來。當向上滾動ListView時,原先位於最上面的list item的view物件會被回收,然後被用來構造新出現的最下面的list item。這個構造過程就是由getView()方法完成的,getView()的第二個形參 View convertView就是被快取起來的list item的view物件(初始化時快取中沒有view物件則convertView是null)。
由此可以看出,如果我們不去使用convertView,而是每次都在getView()中重新例項化一個View物件的話,即浪費時間,也造成記憶體垃圾,給垃圾回收增加壓力,如果垃圾回收來不及的話,虛擬機器將不得不給該應用程序分配更多的記憶體,造成不必要的記憶體開支。ListView回收list item的view物件的過程可以檢視:
android.widget.AbsListView.Java
public addScrapView (View scrap ,int position)
錯誤的程式碼:
public View getView (int position , View convertView,ViewGroup parent){
View view = new TextView(this);
return view;
}
修正示例程式碼:
public View getView (int position , View convertView,ViewGroup parent){
View view = null;
if(convertView !=null){
view =(View)convertView.getTag();
} else {
view = new TextView(this);
convertView.setTag(view);
}
return view;
}
7.3適當的使用物件池
不要在經常呼叫的方法中建立物件,每次new之後都丟棄,尤其是忌諱在迴圈中建立物件。在android support v4包中包含Pools類,其實就是物件池,使用方法也比較簡單,具體可以參考下面的MyPools這個類。
public class MyPools{
private static final Pools.SynchronizedPool<MyPools> sPool = new Pools.SynchronizedPool<MyPools>(10);
public static MyPools obtain(){
MyPools instance = sPool.acquire();
return (instance != null)?instance :new MyPools();
}
public void recycle(){
sPool.release(this);
}
}