Android的bitmap和優化

內存管理是個永恒的話題!

1.在Android應用裡,最耗費內存的就是圖片資源。而且在Android系統中,讀取位圖Bitmap時,分給虛擬機中的圖片的堆棧大小隻有 8M,如果超出瞭,就會出現OutOfMemory異常。
1) 要及時回收Bitmap的內存
Bitmap類有一個方法recycle(),從方法名可以看出意思是回收。這裡就有疑問瞭,Android系統有自己的垃圾回收機制,可以不定期的回收掉不使用的內存空間,當然也包括Bitmap的空間。那為什麼還需要這個方法呢?
Bitmap類的構造方法都是私有的,所以開發者不能直接new出一個Bitmap對象,隻能通過BitmapFactory類的各種靜態方法來實例化一個Bitmap。仔細查看BitmapFactory的源代碼可以看到,生成Bitmap對象最終都是通過JNI調用方式實現的。所以,加載Bitmap到內存裡以後,是包含兩部分內存區域的。簡單的說,一部分是Java部分的,一部分是C部分的。這個Bitmap對象是由Java部分分配的,不用的時候系統就會自動回收瞭,但是那個對應的C可用的內存區域,虛擬機是不能直接回收的,這個隻能調用底層的功能釋放。所以需要調用recycle()方法來釋放C部分的內存。從Bitmap類的源代碼也可以看到,recycle()方法裡也的確是調用瞭JNI方法瞭。
那如果不調用recycle(),是否就一定存在內存泄露呢?也不是的。Android的每個應用都運行在獨立的進程裡,有著獨立的內存,如果整個進程被應用本身或者系統殺死瞭,內存也就都被釋放掉瞭,當然也包括C部分的內存。
Android對於進程的管理是非常復雜的。簡單的說,Android系統的進程分為幾個級別,系統會在內存不足的情況下殺死一些低優先級的進程,以提供給其它進程充足的內存空間。在實際項目開發過程中,有的開發者會在退出程序的時候使用Process.killProcess(Process.myPid())的方式將自己的進程殺死,但是有的應用僅僅會使用調用Activity.finish()方法的方式關閉掉所有的Activity。

經驗分享:
Android手機的用戶,根據習慣不同,可能會有兩種方式退出整個應用程序:一種是按Home鍵直接退到桌面;另一種是從應用程序的退出按鈕或者按Back鍵退出程序。那麼從系統的角度來說,這兩種方式有什麼區別呢?按Home鍵,應用程序並沒有被關閉,而是成為瞭後臺應用程序。按Back鍵,一般來說,應用程序關閉瞭,但是進程並沒有被殺死,而是成為瞭空進程(程序本身對退出做瞭特殊處理的不考慮在內)。
Android系統已經做瞭大量進程管理的工作,這些已經可以滿足用戶的需求。個人建議,應用程序在退出應用的時候不需要手動殺死自己所在的進程。對於應用程序本身的進程管理,交給Android系統來處理就可以瞭。應用程序需要做的,是盡量做好程序本身的內存管理工作。

一般來說,如果能夠獲得Bitmap對象的引用,就需要及時的調用Bitmap的recycle()方法來釋放Bitmap占用的內存空間,而不要等Android系統來進行釋放。
下面是釋放Bitmap的示例代碼片段。
// 先判斷是否已經回收
if(bitmap != null && !bitmap.isRecycled()){
// 回收並且置為null
bitmap.recycle();
bitmap = null;
}
System.gc();

從上面的代碼可以看到,bitmap.recycle()方法用於回收該Bitmap所占用的內存,接著將bitmap置空,最後使用System.gc()調用一下系統的垃圾回收器進行回收,可以通知垃圾回收器盡快進行回收。這裡需要註意的是,調用System.gc()並不能保證立即開始進行回收過程,而隻是為瞭加快回收的到來。
如何調用recycle()方法進行回收已經瞭解瞭,那什麼時候釋放Bitmap的內存比較合適呢?一般來說,如果代碼已經不再需要使用Bitmap對象瞭,就可以釋放瞭。釋放內存以後,就不能再使用該Bitmap對象瞭,如果再次使用,就會拋出異常。所以一定要保證不再使用的時候釋放。比如,如果是在某個Activity中使用Bitmap,就可以在Activity的onStop()或者onDestroy()方法中進行回收。

2) 捕獲異常
因為Bitmap是吃內存大戶,為瞭避免應用在分配Bitmap內存的時候出現OutOfMemory異常以後Crash掉,需要特別註意實例化Bitmap部分的代碼。通常,在實例化Bitmap的代碼中,一定要對OutOfMemory異常進行捕獲。
以下是代碼示例。
Bitmap bitmap = null;
try {
// 實例化Bitmap
bitmap = BitmapFactory.decodeFile(path);
} catch (OutOfMemoryError e) {
//
}
if (bitmap == null) {
// 如果實例化失敗 返回默認的Bitmap對象
return defaultBitmapMap;
}

這裡對初始化Bitmap對象過程中可能發生的OutOfMemory異常進行瞭捕獲。如果發生瞭OutOfMemory異常,應用不會崩潰,而是得到瞭一個默認的Bitmap圖。

經驗分享:
很多開發者會習慣性的在代碼中直接捕獲Exception。但是對於OutOfMemoryError來說,這樣做是捕獲不到的。因為OutOfMemoryError是一種Error,而不是Exception。在此僅僅做一下提醒,避免寫錯代碼而捕獲不到OutOfMemoryError。

3) 緩存通用的Bitmap對象
有時候,可能需要在一個Activity裡多次用到同一張圖片。比如一個Activity會展示一些用戶的頭像列表,而如果用戶沒有設置頭像的話,則會顯示一個默認頭像,而這個頭像是位於應用程序本身的資源文件中的。
如果有類似上面的場景,就可以對同一Bitmap進行緩存。如果不進行緩存,盡管看到的是同一張圖片文件,但是使用BitmapFactory類的方法來實例化出來的Bitmap,是不同的Bitmap對象。緩存可以避免新建多個Bitmap對象,避免內存的浪費。

經驗分享:
Web開發者對於緩存技術是很熟悉的。其實在Android應用開發過程中,也會經常使用緩存的技術。這裡所說的緩存有兩個級別,一個是硬盤緩存,一個是內存緩存。比如說,在開發網絡應用過程中,可以將一些從網絡上獲取的數據保存到SD卡中,下次直接從SD卡讀取,而不從網絡中讀取,從而節省網絡流量。這種方式就是硬盤緩存。再比如,應用程序經常會使用同一對象,也可以放到內存中緩存起來,需要的時候直接從內存中讀取。這種方式就是內存緩存。

4) 壓縮圖片
如果圖片像素過大,使用BitmapFactory類的方法實例化Bitmap的過程中,需要大於8M的內存空間,就必定會發生OutOfMemory異常。這個時候該如何處理呢?如果有這種情況,則可以將圖片縮小,以減少載入圖片過程中的內存的使用,避免異常發生。
使用BitmapFactory.Options設置inSampleSize就可以縮小圖片。屬性值inSampleSize表示縮略圖大小為原始圖片大小的幾分之一。即如果這個值為2,則取出的縮略圖的寬和高都是原始圖片的1/2,圖片的大小就為原始大小的1/4。
如果知道圖片的像素過大,就可以對其進行縮小。那麼如何才知道圖片過大呢?
使用BitmapFactory.Options設置inJustDecodeBounds為true後,再使用decodeFile()等方法,並不會真正的分配空間,即解碼出來的Bitmap為null,但是可計算出原始圖片的寬度和高度,即options.outWidth和options.outHeight。通過這兩個值,就可以知道圖片是否過大瞭。
BitmapFactory.Options opts = new BitmapFactory.Options();
// 設置inJustDecodeBounds為true
opts.inJustDecodeBounds = true;
// 使用decodeFile方法得到圖片的寬和高
BitmapFactory.decodeFile(path, opts);
// 打印出圖片的寬和高
Log.d(“example”, opts.outWidth + “,” + opts.outHeight);

在實際項目中,可以利用上面的代碼,先獲取圖片真實的寬度和高度,然後判斷是否需要跑縮小。如果不需要縮小,設置inSampleSize的值為1。如果需要縮小,則動態計算並設置inSampleSize的值,對圖片進行縮小。需要註意的是,在下次使用BitmapFactory的decodeFile()等方法實例化Bitmap對象前,別忘記將opts.inJustDecodeBound設置回false。否則獲取的bitmap對象還是null。

經驗分享:
如果程序的圖片的來源都是程序包中的資源,或者是自己服務器上的圖片,圖片的大小是開發者可以調整的,那麼一般來說,就隻需要註意使用的圖片不要過大,並且註意代碼的質量,及時回收Bitmap對象,就能避免OutOfMemory異常的發生。
如果程序的圖片來自外界,這個時候就特別需要註意OutOfMemory的發生。一個是如果載入的圖片比較大,就需要先縮小;另一個是一定要捕獲異常,避免程序Crash。

2.一般來說,優秀的程序員在寫完代碼之後都會不斷的對代碼進行重構。重構的好處有很多,其中一點,就是對代碼進行優化,提高軟件的性能。下面我們就從幾個方面來瞭解Android開發過程中的代碼優化。

1)靜態變量引起內存泄露
在代碼優化的過程中,我們需要對代碼中的靜態變量特別留意。靜態變量是類相關的變量,它的生命周期是從這個類被聲明,到這個類徹底被垃圾回收器回收才會被銷毀。所以,一般情況下,靜態變量從所在的類被使用開始就要一直占用著內存空間,直到程序退出。如果不註意,靜態變量引用瞭占用大量內存的資源,造成垃圾回收器無法對內存進行回收,就可能造成內存的浪費。
先來看一段代碼,這段代碼定義瞭一個Activity。
private static Resources mResources;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
if (mResources == null) {
mResources = this.getResources();
}
}

這段代碼中有一個靜態的Resources對象。代碼片段mResources = this.getResources()對Resources對象進行瞭初始化。這時Resources對象擁有瞭當前Activity對象的引用,Activity又引用瞭整個頁面中所有的對象。如果當前的Activity被重新創建(比如橫豎屏切換,默認情況下整個Activity會被重新創建),由於Resources引用瞭第一次創建的Activity,就會導致第一次創建的Activity不能被垃圾回收器回收,從而導致第一次創建的Activity中的所有對象都不能被回收。這個時候,一部分內存就浪費掉瞭。

經驗分享:
在實際項目中,我們經常會把一些對象的引用加入到集合中,如果這個集合是靜態的話,就需要特別註意瞭。當不需要某對象時,務必及時把它的引用從集合中清理掉。或者可以為集合提供一種更新策略,及時更新整個集合,這樣可以保證集合的大小不超過某值,避免內存空間的浪費。

2)使用Application的Context
在Android中,Application Context的生命周期和應用的生命周期一樣長,而不是取決於某個Activity的生命周期。如果想保持一個長期生命的對象,並且這個對象需要一個Context,就可以使用Application對象。可以通過調用Context.getApplicationContext()方法或者Activity.getApplication()方法來獲得Application對象。
依然拿上面的代碼作為例子。可以將代碼修改成下面的樣子。
private static Resources mResources;
@Override
protected void onCreate(Bundle state) {
super.onCreate(state);
if (mResources == null) {
// mResources = this.getResources();
mResources = this.getApplication().getResources();
}
}
在這裡將this.getResources()修改為this.getApplication().getResources()。修改以後,Resources對象擁有的是Application對象的引用。如果Activity被重新創建,第一次創建的Activity就可以被回收瞭。
3)及時關閉資源
Cursor是Android查詢數據後得到的一個管理數據集合的類。正常情況下,如果我們沒有關閉它,系統會在回收它時進行關閉,但是這樣的效率特別低。如果查詢得到的數據量較小時還好,如果Cursor的數據量非常大,特別是如果裡面有
Blob信息時,就可能出現內存問題。所以一定要及時關閉Cursor。
下面給出一個通用的使用Cursor的代碼片段。
Cursor cursor = null;
try{
cursor = mContext.getContentResolver().query(uri,null,null,null,null);
if (cursor != null) {
cursor.moveToFirst();
// 處理數據
}
} catch (Exception e){
e.printStatckTrace();
} finally {
if (cursor != null){
cursor.close();
}
}

即對異常進行捕獲,並且在finally中將cursor關閉。同樣的,在使用文件的時候,也要及時關閉。
4)使用Bitmap及時調用recycle()
前面的章節講過,在不使用Bitmap對象時,需要調用recycle()釋放內存,然後將它設置為null。雖然調用recycle()並不能保證立即釋放占用的內存,但是可以加速Bitmap的內存的釋放。在代碼優化的過程中,如果發現某個Activity用到瞭Bitmap對象,卻沒有顯式的調用recycle()釋放內存,則需要分析代碼邏輯,增加相關代碼,在不再使用Bitmap以後調用recycle()釋放內存。

5)對Adapter進行優化
下面以構造ListView的BaseAdapter為例說明如何對Adapter進行優化。
在BaseAdapter類中提供瞭如下方法:
public View getView(int position, View convertView, ViewGroup parent)

當ListView列表裡的每一項顯示時,都會調用Adapter的getView方法返回一個View,
來向ListView提供所需要的View對象。
下面是一個完整的getView()方法的代碼示例。
public View getView(int position, View convertView, ViewGroup parent) {
  ViewHolder holder;
if (convertView == null) {
   convertView = mInflater.inflate(R.layout.list_item, null);
   holder = new ViewHolder();
   holder.text = (TextView) convertView.findViewById(R.id.text);
   convertView.setTag(holder);
  } else {
   holder = (ViewHolder) convertView.getTag();
  }
  holder.text.setText(“line” + position);
  return convertView;
}

private class ViewHolder {
  TextView text;
}

當向上滾動ListView時,getView()方法會被反復調用。getView()的第二個參數convertView是被緩存起來的List條目中的View對象。當ListView滑動的時候,getView可能會直接返回舊的convertView。這裡使用瞭convertView和ViewHolder,可以充分利用緩存,避免反復創建View對象和TextView對象。如果ListView的條目隻有幾個,這種技巧並不能帶來多少性能的提升。但是如果條目有幾百甚至幾千個,使用這種技巧隻會創建幾個convertView和ViewHolder(取決於當前界面能夠顯示的條目數),性能的差別就非常非常大瞭。

6)代碼“微優化”
當今時代已經進入瞭“微時代”。這裡的“微優化”指的是代碼層面的細節優化,即不改動代碼整體結構,不改變程序原有的邏輯。盡管Android使用的是Dalvik虛擬機,但是傳統的Java方面的代碼優化技巧在Android開發中也都是適用的。
還有其他:
創建新的對象都需要額外的內存空間,要盡量減少創建新的對象。
將類、變量、方法等等的可見性修改為最小。
針對字符串的拼接,使用StringBuffer替代String。
不要在循環當中聲明臨時變量,不要在循環中捕獲異常。
如果對於線程安全沒有要求,盡量使用線程不安全的集合對象。
使用集合對象,如果事先知道其大小,則可以在構造方法中設置初始大小。
文件讀取操作需要使用緩存類,及時關閉文件。
慎用異常,使用異常會導致性能降低。
如果程序會頻繁創建線程,則可以考慮使用線程池。

經驗分享:
代碼的微優化有很多很多東西可以講,小到一個變量的聲明,大到一段算法。尤其在代碼Review的過程中,可能會反復審查代碼是否可以優化。不過我認為,代碼的微優化是非常耗費時間的,沒有必要從頭到尾將所有代碼都優化一遍。開發者應該根據具體的業務邏輯去專門針對某部分代碼做優化。比如應用中可能有一些方法會被反復調用,那麼這部分代碼就值得專門做優化。其它的代碼,需要開發者在寫代碼過程中去註意。

發佈留言