節省你的內存

我剛開始接觸手機開發的時候,一昧地認爲讓程序跑的越快就越好,完全忽略內存是否夠不夠用,而事實上,手機的CPU速度總是比我們想象的要快,而內存的容量總是比我們想象的要少。忽略內存的使用情況有時候好比一條貪吃的蟒蛇,企圖一次吞下大象;有時候好比消化系統出現了問題,只進不出,俗稱“BM”。

 

在 android 開發中這兩種情況都有可能發生,當我們批量加載圖片時,有可能因爲同時寫入內存的字節過多,導致內存不夠用,出現OOM(Out OfMemeory)——蟒蛇把肚皮撐破了;當我們進行數據庫查詢返回 Cursor 時,有可能因爲忘記使用完 Cursor 後將其關閉釋放資源而導致內存泄漏——消化不良了。

 

以下分別對這兩種情況進行說明並提出一些需要注意的地方以避免發生OOM。


1 小心特殊指針Cursor——在大倉庫尋找繡花針的煩惱

做 C/C++ 開發的童鞋經常苦惱於內存的管理,尤其是指針,它給我們帶來高效率的同時也帶來了不小的麻煩,所謂“花無百日紅”,無論你多麼仔細,忘記釋放指針幾乎是難免的事,而尋找這些隱患有時就如同大海撈針一般。


雖然在 Java 中省去了這些所謂的麻煩,但是在 android 開發中又引入了一個遊標機制——Cursor,它抓住內存資源不放的行徑就如同 C/C++ 指針一般厚顏無恥,忘記釋放Cursor 在數據庫查詢中經常出現,要想避免這樣的情況出現沒有別的捷徑,我們只能時刻提醒自己:

 

誰產生了Cursor誰就要負責釋放它

 

這條原則是如此的眼熟,簡直像極了C/C++ 中的指針管理以及 Object-c 中的 retain count 管理。

 

記住一些Cursor管理的經驗:

 

  • 在 finally 中釋放 Curosr——“It’s safe now”,引人入勝的電影情節總不會讓這句臺詞得逞

一旦你能全程操控 Cursor 的生命週期,請記住:一定要在 finally 中釋放 cursor,請看下面的僞代碼塊:


public void foo() {
	Cursor c = query to get a cursor;


	//use the cursor


	if (something occured) {
		return or throw exception;
	}


	//in the end you thought
	if (c != null) {
		c.close();
	}


}



在 foo 方法中,你原本以爲在函數的結尾釋放了 cursor,就此平安無事了,可未曾料到前面的語句中有返回或者異常的出現,而此時 cursor 將被忘記釋放。也許你認爲這樣的錯誤太過低級,稍微留意就不會出現,這裏只是簡化了實例,一旦程序邏輯複雜到一定程度,則這樣的低級錯誤往往很難避免,解決的辦法就是,只要你全程管理了一個 cursor,就將其植入 try{}finally塊中,並在 finally 中釋放 cursor:


public void foo() {
	Cursor c = query to get a cursor;
	
	if (c != null) {
		try {
			//use the cursor
			if (something occured) {
				return or throw exception;
			}
			
		} finally {
			c.close();
		}
	}
}


這樣,無論你的 try 中有多少可能的返回或異常出現,finally 中的語句總會在最後執行,從而也可避免在 try 中的每一處退出程序的地方都進行cursor的檢查釋放,省去很多麻煩和隱患。

 

  • CursorAdapter 之殤——孩子不能依賴父母太甚

當我們用 CursorAdapter 爲列表產生數據時,我們無法全程對 Cursor 進行管理,於是無法引用前一條規則,事實上CursorAdapter 動態實現了對老 cursor 的釋放:

 

當我們對 Cursor 進行更新後都需要通過 CursorAdapter.changeCursor(c); 方法將新的 cursor 傳入CursorAdapter 中以便重新生成新的數據並更新列表。而在 changeCursor() 方法中就實現了對前一個 cursor 的釋放(請參考 CursorAdapter 源代碼:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2.2_r1/android/widget/CursorAdapter.java#CursorAdapter.changeCursor%28android.database.Cursor%29)。

 

這樣看起來好像 CursorAdapter 已經幫我們安排好了一切,它對 Cursor 的管理是如此的完美,就好比富二代的父母安排孩子上學、工作,讓孩子誤認爲一切都有父母,一切依賴父母,一旦失去依靠則無法生存。

 

你肯定發現了,CursorAdapter 雖然幫我們釋放了所有老的 cursor,可最後一個 cursor呢?我們往往記得有更新時需要調用 changeCursor(),讓我們產生 changeCursor() 只在我們需要更新的時候才需要用到的錯覺。

 

CursorAdapter第一原則:用完CursorAdapter 之後調用CursorAdapter.changeCursor(null); 釋放最後一個 cursor。

 

第一原則適用於 CursorAdapter 本身及其所有子類,如:ResourceCursorAdapter。

 

此外,還需要注意 cursor 的釋放時機:

 

CursorAdapter 第二原則:調用CursorAdapter.changeCursor(null); 的次數和 多次查詢產生的 Cursor個數相等。

 

千萬不要忘記在你的 Activity 中反覆執行了多少次同樣的查詢,每次查詢都會重新生成一個新的 Cursor,那麼釋放次數也應和生成的 Cursor 次數相等。這些反覆查詢有可能是你自己進行的,也有可能是 API 內部進行的,總之,要小心地檢查,不要遺漏。

 

  • 避免過早地釋放 Cursor——嘿,服務員,我還沒吃完呢

在餐廳你可能會遇到這樣的尷尬,飯吃到一半,出去接了個電話或者上了趟WC,回來一看,碗筷被收走了……

 

這裏還要補充一下 cursor 的釋放時機,我們知道滯後釋放會引起內存泄漏,那麼過早釋放就會引起空引用的致命異常,就好比前面提到的餐廳事件。

 

記住以下兩條經驗:

 

1.   釋放Cursor 之前一定要確保它在當前線程中永遠不會再被使用到

 

2.   儘量不用一個 Cursor 變量引用多個 Cursor 生成來源,否則會讓 cursor 的管理變得混亂,很容易導致錯誤釋放。


 

2 小心載入Bitmap——貪吃蟒蛇的悲劇

       在應用中載入圖片是 android 開發中的普遍需求,無論是網絡下載的圖片還是從外存加載的圖片,小則幾K,大則幾M,若一次加載圖片不多,幾乎無需考慮圖片對內存的佔用問題,然而這樣就安然無事了嗎?考慮以下幾種情況:

 

1.    沒錯,你一次只需加載一張圖片,而某時你的程序突然加載了一張超過10M的大圖(系統給一個進程分配的內存上限是10M);

 

2.    你加載的每一張圖片確實很小,但是你需要批量加載,比如在 GridView中,每次加載的圖片大概在三屏60張左右,假設平均一張圖片超過200K,請算算需要多少內存?

 

看看我們的圖片是不是像極了一條貪吃的大蟒蛇,如果不加提防,一旦它飢餓起來會瘋狂吞噬你的內存!那如何解決呢?兩個原則:

 

1.   縮小圖片,控制圖片大小;

2.   手動分配內存使其不超過上限,重複利用分配的內存給圖片——需要時給,不需要就收回。

 

2.1 控制單張圖片的大小——一起來瘦身吧

第一種情況相對好解決,只需控制圖片大小,具體來說加載圖片之前先加載其寬高值並計算出圖片字節大小,然後縮小其規格,使其字節大小不超過甚至遠小於內存上限即可。圖片字節大小的計算公式如下:

 

BitSize = width X height X BPP

 

width和 height 分別表示圖片的寬和高,單位爲像素,BPP(bit per pix)表示單位像素所佔的字節位數。以下是一個生成圖片縮略圖的例子:


static final int MAX_SIZE = 1024 * 1024 * 8; //每張加載圖片所佔內存限制 1MB

BitmapFactory.Options options = new BitmapFatory.Options();
options.InJustDecodeBounds = true; //只加載邊界,無需佔用內存
BitmapFatory.decodeFile(path, options);

//android默認採用ARGB_8888顏色模式,即BPP = 32位
int srcSize = options.outHeight * options.outWidth * 32;
if (srcSize > MAX_SIZE) {
	int scale = Math.ceil((double)MAX_SIZE / (double)srcSize);
	options. InJustDecodeBounds = false;

	//嚴格來說應該是scale的二次開方,但我們儘量縮小
	options.inSampleSize = scale; 
	//如果對顏色質量要求不高可選擇此項,能將BBP 置爲更小的16位每像素
	options.inPreferredConfig = Bitmap.Config.ARGB_4444;
	
	bitmap = BitmapFatory.decodeFile(path, options); //重新解碼加載圖片
}


幾點注意:

1.    一般來說在不太苛刻的情況下,控制好你所需要的圖片寬高值就行了,無需計算出圖片的字節數;

 

2.    使用BitmapFactory解碼圖片時儘量不要使用 decodeResource 方法,而要使用 decodeStream 或者 decodeFile 方法,decodeFile 其實也是調用了 decodeStream,因爲 decodeStream 調用了底層的 native 方法進行解碼,需要更少的額外內存;

 

3.    如果對圖片顏色質量有要求,則不要改變 inPreferredConfig選項。

 

2.2 合理分配可用內存——不要佔着茅坑不…

對於大批量加載圖片的情況,僅僅依靠對單張圖片瘦身還是不夠的,就算圖片再小,理論上當加載總量超過一個閾值時還是會出現內存不夠用的情況,這樣的情況很容易出現在列表類View中快速加載圖片的時候。

 

比如在 GridView 中加載圖片時,爲了避免多次生成圖片,通常需要將已經生成的圖片放入 HashMap 緩存中以便重複利用,如果對緩存不加限制,那麼緩存容量將直線上升,相反可用內存容量也將直線下降,直至內存耗盡。

 

那麼,如何避免內存被耗盡呢?簡言之,就是——需要時就給,不需要時及時收回。對操作系統原理很熟悉的童鞋一定馬上想到了操作系統的內存分配策略,那我們就在應用層次模仿系統層次的內存管理吧。

 

這裏以類FIFO策略和LRU策略分別加以說明。

 

  • 類FIFO內存分配策略——嗯,不要自欺欺人,其實我們都喜新厭舊

FIFO的概念我想不用多說,關鍵是如何構建一個這樣的有序列表,讓我們存入其中的bitmap 是按照生成的先後順序進行排列的?也許你覺得這有什麼難的,順序加入 List 容器不就 OK 了嗎?的確,可是別忘了,這樣一來複用的時候我怎麼知道取哪一個呢?沒錯,別忘了每一個bitmap 的唯一標識:URI(也許是文件路徑,也許是URL)。

 

看來還得麻煩 HashMap,而且必須是有序的 HashMap,java util 中給我們提供了這樣的HashMap——LinkedHashMap。

 

首先,讓我們來看看類FIFO策略的具體運作流程吧,如下圖所示:




圖中的 Hard Cache 可以用LinkedHashMap實現,以“path - bitmap”元素對存入。之所以叫類FIFO策略是因爲該策略不是嚴格的FIFO,從圖中可以看出,新的 bitmap 進入有序緩存(Hard Cache)是按照時間順序排列的,但是出去的bitmap並不總是最老的那個,爲了讓 Hard Cache 時刻保持最新,我們將複用過一次的 bitmap 都丟到 Soft Cache 中,此外, Hard Cache 的容量是有限的,當超過容量上限則將最老的一個 bitmap 也丟進 Soft Cache。

 

Soft Cache 是什麼呢?我們可以將其視爲二級緩存,一般用可同時讀寫的ConcurrentHashMap 實現,也以“path - bitmap”元素對存入,只不過其中的 bigmap 採用 SoftReference 弱引用,這些優先級最低的 bitmap 將隨時被系統回收,同時它還起到備用作用,當從 Hard Cache 中取不到所要的 bitmap 時,可以再次從 Soft Cache 中遍歷到,如果該 bitmap 存在且未被回收的話。若兩者沒有,則重新生成(下載或從外存中獲取)。

 

下圖描述了從緩存中讀取 bitmap 的過程:

 


總結一下,類FIFO策略的關鍵是:新進入的比後進入的優先級高,複用過的和最後進入的可以被系統回收

 

該種策略的實現方法請參考 Tim Bray 的安卓開發博客(需要翻牆):http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html 

或者看我轉載的博文:Multithreading For Performance

  • LRU內存分配策略——家裏最近不用的雜物太多,又佔地方,當廢品賣掉吧

往下閱讀之前,請大家抽空複習下操作系統的LRU內存分配策略,雖然在這裏不需要大家手動實現它,但熟悉一下總是好的,能讓你明白該種策略的價值所在。

 

LRU的核心是對於最近最少使用到的內存塊將其釋放,以便可以讓新進事務利用。AndroidAPI 提供了一個實現了 LRU 算法的 HashMap——LruCache,實際上是 LruCache 引用了一個 LinkedHashMap 的實例,並通過 LRU 算法實現對 LinkedHashMap 的緩存操作。大家在 bitmap 的緩存中可以直接採用 LruCache 來實現。

 

最簡單的LruCache 緩存 bitmap 的方式就是只需要一個獨立的 LruCache,分配給固定的內存空間,使其按照 LRU 策略對緩存的 bitmap 實施淘汰,如下圖所示:

 



當然,如果想有效緩存更多的圖片,你還可以選擇採用一級LruCache 和二級類FIFO Cache 相結合的方法,或者 一級LruCache 與 二級Soft Cache 相結合的方法,如下圖所示:

 

總結一下,LRU 策略的關鍵是:採用LRU 策略對 bitmap 進行緩存淘汰,LRU 策略採用 android API 中的 LruCache 實現

 

具體的實現方法請參考 LruCache 的谷歌官方文檔:http://developer.android.com/reference/android/util/LruCache.html。

你也可以看我轉載的博文:LruCache 結合 FIFO 策略實現bitmap緩存

 

最後總結一下,無論是類 FIFO 還是 LRU,一個總的原則是:禁止永久緩存你的 bitmap,這裏永久的意思是在一個線程生命週期內。

 

現在安全了嗎?NO!

 

  • 控制 bitmap 的生成時機——百米衝刺,你能持續做有氧呼吸麼?

難道對緩存進行了合理的控制還不夠?對的,還不夠,靜態地控制緩存的容量算是解決了一部分問題,這是顯而易見的,然而隱藏在深處的動態產生的內存呢?

 

好比說我們要批量加載 bitmap,一般是在 UI 線程以外的線程中進行的,通常是一個 bitmap 由一個 AyncTask 來產生,當你快速滑動一個GridView的時候,大批量的 bitmap 加載將生成大量的 AyncTask,同時也將產生大量的bitmap,這些bitmap 被快速地丟入緩存又從緩存中丟棄,然而這些被丟棄的 bitmap 不會馬上被釋放掉,無論它們是 SoftReference 還是被 recycle(),都只是告訴垃圾回收器,這些是可以被系統回收的,但我們知道垃圾回收器是不會馬上將其回收的,這個回收時機我們無法控制,只能由 VM 來決定。

 

於是當這些等待被回收但還沒有馬上被回收的 bitmap 瞬間達到一個峯值時,噢噢,OOM了……

 

所以,我們要控制 bitmap 的生成時機,注意到當我們在 Fling GridView 的時候,我們更在意列表的滑動流暢度,至於圖像的加載則期望在 GridView 停止時馬上完成,因爲此時目光的注意力最集中。所以一種解決方案是,在列表快速滑動時無需生成 AyncTask 加載 bitmap,而只有當滑動停下時才快速加載。就好比運動員在百米衝刺時機體只需做無氧呼吸,而停下後則盡情地享受空氣。


具體實現請看我的博文:在列表中控制 AsyncTask 加載 bitmap 的時機 


一種更爲理想的方案是,根據列表的滑動速度來判斷是否加載 bitmap,當速度慢到一個閾值時則加載,否則不加載。實際上第一種方案是此方案的一種特殊情況,即滑動速度爲0時加載。

 

3 其它常見內存泄漏——互聯網大拿們,你們怎麼看?

1.   Context 泄漏,這種內存泄漏通常發生的非常隱祕,它通常是由於你在 Activity 的控制範圍之外(比如靜態區)長期引用了Activity 所致,Romain Guy 在他的博文中對此有詳細說明:

 

Romain Guy 的博文(原文,需翻牆):

http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

 

該文的山寨版翻譯鏈接:

http://blog.csdn.net/sunchaoenter/article/details/7209635

 

2.   Alert Dialog 泄漏,通常我們在程序中需要創建 Alert Dialog 來發出警告信息,但是往往在Activity 銷燬或者重新加載元素(旋轉)之前忘記 dismiss,從而引起 Dialog 資源的泄漏,詳細請看 Justin Schultz 的博文:

 

http://publicstaticdroidmain.com/2012/01/avoiding-android-memory-leaks-part-1/

 

3.   register receiver 泄漏,registerReceiver 之後忘記 unregisterReceiver

4.   InputStream/OutputStream 泄漏,忘記關閉輸入輸出流

 

最後兩點來自博文:http://www.linuxidc.com/Linux/2011-10/44785.htm

 


發佈了34 篇原創文章 · 獲贊 7 · 訪問量 11萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章