我們在編寫android程式的時候經常要用到許多圖檔,不同圖檔總是會有不同的形狀、不同的大小,但在大多數情況下,這些圖檔都會大于我們程式所需要的大小。比如說系統圖檔庫裡展示的圖檔大都是用手機攝像頭拍出來的,這些圖檔的分辨率會比我們手機螢幕的分辨率高得多。大家應該知道,我們編寫的應用程式都是有一定記憶體限制的,程式占用了過高的記憶體就容易出現oom(outofmemory)異常。我們可以通過下面的代碼看出每個應用程式最高可用記憶體是多少。
int maxmemory = (int) (runtime.getruntime().maxmemory() / 1024);
log.d("tag", "max memory is " + maxmemory + "kb");
是以在展示高分辨率圖檔的時候,最好先将圖檔進行壓縮。壓縮後的圖檔大小應該和用來展示它的控件大小相近,在一個很小的imageview上顯示一張超大的圖檔不會帶來任何視覺上的好處,但卻會占用我們相當多寶貴的記憶體,而且在性能上還可能會帶來負面影響。下面我們就來看一看,如何對一張大圖檔進行适當的壓縮,讓它能夠以最佳大小顯示的同時,還能防止oom的出現。
bitmapfactory這個類提供了多個解析方法(decodebytearray, decodefile, decoderesource等)用于建立bitmap對象,我們應該根據圖檔的來源選擇合适的方法。比如sd卡中的圖檔可以使用decodefile方法,網絡上的圖檔可以使用decodestream方法,資源檔案中的圖檔可以使用decoderesource方法。這些方法會嘗試為已經建構的bitmap配置設定記憶體,這時就會很容易導緻oom出現。為此每一種解析方法都提供了一個可選的bitmapfactory.options參數,将這個參數的injustdecodebounds屬性設定為true就可以讓解析方法禁止為bitmap配置設定記憶體,傳回值也不再是一個bitmap對象,而是null。雖然bitmap是null了,但是bitmapfactory.options的outwidth、outheight和outmimetype屬性都會被指派。這個技巧讓我們可以在加載圖檔之前就擷取到圖檔的長寬值和mime類型,進而根據情況對圖檔進行壓縮。如下代碼所示:
bitmapfactory.options options = new bitmapfactory.options();
options.injustdecodebounds = true;
bitmapfactory.decoderesource(getresources(), r.id.myimage, options);
int imageheight = options.outheight;
int imagewidth = options.outwidth;
string imagetype = options.outmimetype;
為了避免oom異常,最好在解析每張圖檔的時候都先檢查一下圖檔的大小,除非你非常信任圖檔的來源,保證這些圖檔都不會超出你程式的可用記憶體。
現在圖檔的大小已經知道了,我們就可以決定是把整張圖檔加載到記憶體中還是加載一個壓縮版的圖檔到記憶體中。以下幾個因素是我們需要考慮的:
預估一下加載整張圖檔所需占用的記憶體。
為了加載這一張圖檔你所願意提供多少記憶體。
用于展示這張圖檔的控件的實際大小。
目前裝置的螢幕尺寸和分辨率。
比如,你的imageview隻有128*96像素的大小,隻是為了顯示一張縮略圖,這時候把一張1024*768像素的圖檔完全加載到記憶體中顯然是不值得的。
那我們怎樣才能對圖檔進行壓縮呢?通過設定bitmapfactory.options中insamplesize的值就可以實作。比如我們有一張2048*1536像素的圖檔,将insamplesize的值設定為4,就可以把這張圖檔壓縮成512*384像素。原本加載這張圖檔需要占用13m的記憶體,壓縮後就隻需要占用0.75m了(假設圖檔是argb_8888類型,即每個像素點占用4個位元組)。下面的方法可以根據傳入的寬和高,計算出合适的insamplesize值:
public static int calculateinsamplesize(bitmapfactory.options options,
int reqwidth, int reqheight) {
// 源圖檔的高度和寬度
final int height = options.outheight;
final int width = options.outwidth;
int insamplesize = 1;
if (height > reqheight || width > reqwidth) {
// 計算出實際寬高和目标寬高的比率
final int heightratio = math.round((float) height / (float) reqheight);
final int widthratio = math.round((float) width / (float) reqwidth);
// 選擇寬和高中最小的比率作為insamplesize的值,這樣可以保證最終圖檔的寬和高
// 一定都會大于等于目标的寬和高。
insamplesize = heightratio < widthratio ? heightratio : widthratio;
}
return insamplesize;
}
使用這個方法,首先你要将bitmapfactory.options的injustdecodebounds屬性設定為true,解析一次圖檔。然後将bitmapfactory.options連同期望的寬度和高度一起傳遞到到calculateinsamplesize方法中,就可以得到合适的insamplesize值了。之後再解析一次圖檔,使用新擷取到的insamplesize值,并把injustdecodebounds設定為false,就可以得到壓縮後的圖檔了。
public static bitmap decodesampledbitmapfromresource(resources res, int resid,
// 第一次解析将injustdecodebounds設定為true,來擷取圖檔大小
final bitmapfactory.options options = new bitmapfactory.options();
options.injustdecodebounds = true;
bitmapfactory.decoderesource(res, resid, options);
// 調用上面定義的方法計算insamplesize值
options.insamplesize = calculateinsamplesize(options, reqwidth, reqheight);
// 使用擷取到的insamplesize值再次解析圖檔
options.injustdecodebounds = false;
return bitmapfactory.decoderesource(res, resid, options);
下面的代碼非常簡單地将任意一張圖檔壓縮成100*100的縮略圖,并在imageview上展示。
mimageview.setimagebitmap(
decodesampledbitmapfromresource(getresources(), r.id.myimage, 100, 100));
在你應用程式的ui界面加載一張圖檔是一件很簡單的事情,但是當你需要在界面上加載一大堆圖檔的時候,情況就變得複雜起來。在很多情況下,(比如使用listview, gridview 或者 viewpager 這樣的元件),螢幕上顯示的圖檔可以通過滑動螢幕等事件不斷地增加,最終導緻oom。
為了保證記憶體的使用始終維持在一個合理的範圍,通常會把被移除螢幕的圖檔進行回收處理。此時垃圾回收器也會認為你不再持有這些圖檔的引用,進而對這些圖檔進行gc操作。用這種思路來解決問題是非常好的,可是為了能讓程式快速運作,在界面上迅速地加載圖檔,你又必須要考慮到某些圖檔被回收之後,使用者又将它重新滑入螢幕這種情況。這時重新去加載一遍剛剛加載過的圖檔無疑是性能的瓶頸,你需要想辦法去避免這個情況的發生。
這個時候,使用記憶體緩存技術可以很好的解決這個問題,它可以讓元件快速地重新加載和處理圖檔。下面我們就來看一看如何使用記憶體緩存技術來對圖檔進行緩存,進而讓你的應用程式在加載很多圖檔的時候可以提高響應速度和流暢性。
記憶體緩存技術對那些大量占用應用程式寶貴記憶體的圖檔提供了快速通路的方法。其中最核心的類是lrucache (此類在android-support-v4的包中提供) 。這個類非常适合用來緩存圖檔,它的主要算法原理是把最近使用的對象用強引用存儲在 linkedhashmap 中,并且把最近最少使用的對象在緩存值達到預設定值之前從記憶體中移除。
在過去,我們經常會使用一種非常流行的記憶體緩存技術的實作,即軟引用或弱引用 (softreference or weakreference)。但是現在已經不再推薦使用這種方式了,因為從 android 2.3 (api level 9)開始,垃圾回收器會更傾向于回收持有軟引用或弱引用的對象,這讓軟引用和弱引用變得不再可靠。另外,android 3.0 (api level 11)中,圖檔的資料會存儲在本地的記憶體當中,因而無法用一種可預見的方式将其釋放,這就有潛在的風險造成應用程式的記憶體溢出并崩潰。
為了能夠選擇一個合适的緩存大小給lrucache, 有以下多個因素應該放入考慮範圍内,例如:
你的裝置可以為每個應用程式配置設定多大的記憶體?
裝置螢幕上一次最多能顯示多少張圖檔?有多少圖檔需要進行預加載,因為有可能很快也會顯示在螢幕上?
你的裝置的螢幕大小和分辨率分别是多少?一個超高分辨率的裝置(例如 galaxy nexus) 比起一個較低分辨率的裝置(例如 nexus s),在持有相同數量圖檔的時候,需要更大的緩存空間。
圖檔的尺寸和大小,還有每張圖檔會占據多少記憶體空間。
圖檔被通路的頻率有多高?會不會有一些圖檔的通路頻率比其它圖檔要高?如果有的話,你也許應該讓一些圖檔常駐在記憶體當中,或者使用多個lrucache 對象來區分不同組的圖檔。
你能維持好數量和品質之間的平衡嗎?有些時候,存儲多個低像素的圖檔,而在背景去開線程加載高像素的圖檔會更加的有效。
并沒有一個指定的緩存大小可以滿足所有的應用程式,這是由你決定的。你應該去分析程式記憶體的使用情況,然後制定出一個合适的解決方案。一個太小的緩存空間,有可能造成圖檔頻繁地被釋放和重新加載,這并沒有好處。而一個太大的緩存空間,則有可能還是會引起 java.lang.outofmemory 的異常。
下面是一個使用 lrucache 來緩存圖檔的例子:
private lrucache<string, bitmap> mmemorycache;
@override
protected void oncreate(bundle savedinstancestate) {
// 擷取到可用記憶體的最大值,使用記憶體超出這個值會引起outofmemory異常。
// lrucache通過構造函數傳入緩存值,以kb為機關。
int maxmemory = (int) (runtime.getruntime().maxmemory() / 1024);
// 使用最大可用記憶體值的1/8作為緩存的大小。
int cachesize = maxmemory / 8;
mmemorycache = new lrucache<string, bitmap>(cachesize) {
@override
protected int sizeof(string key, bitmap bitmap) {
// 重寫此方法來衡量每張圖檔的大小,預設傳回圖檔數量。
return bitmap.getbytecount() / 1024;
}
};
public void addbitmaptomemorycache(string key, bitmap bitmap) {
if (getbitmapfrommemcache(key) == null) {
mmemorycache.put(key, bitmap);
public bitmap getbitmapfrommemcache(string key) {
return mmemorycache.get(key);
在這個例子當中,使用了系統配置設定給應用程式的八分之一記憶體來作為緩存大小。在中高配置的手機當中,這大概會有4兆(32/8)的緩存空間。一個全螢幕的 gridview 使用4張 800x480分辨率的圖檔來填充,則大概會占用1.5兆的空間(800*480*4)。是以,這個緩存大小可以存儲2.5頁的圖檔。
當向 imageview 中加載一張圖檔時,首先會在 lrucache 的緩存中進行檢查。如果找到了相應的鍵值,則會立刻更新imageview ,否則開啟一個背景線程來加載這張圖檔。
public void loadbitmap(int resid, imageview imageview) {
final string imagekey = string.valueof(resid);
final bitmap bitmap = getbitmapfrommemcache(imagekey);
if (bitmap != null) {
imageview.setimagebitmap(bitmap);
} else {
imageview.setimageresource(r.drawable.image_placeholder);
bitmapworkertask task = new bitmapworkertask(imageview);
task.execute(resid);
bitmapworkertask 還要把新加載的圖檔的鍵值對放到緩存中。
class bitmapworkertask extends asynctask<integer, void, bitmap> {
// 在背景加載圖檔。
@override
protected bitmap doinbackground(integer... params) {
final bitmap bitmap = decodesampledbitmapfromresource(
getresources(), params[0], 100, 100);
addbitmaptomemorycache(string.valueof(params[0]), bitmap);
return bitmap;