參考資料《Android開(kāi)發(fā)藝術(shù)探索》
如何高效的加載一個(gè)Bitmap?由于Bitmap的特殊性以及Android對(duì)單個(gè)應(yīng)用所施加的內(nèi)存限制,比如16M,這導(dǎo)致加載Bitmap時(shí)很容易出現(xiàn)內(nèi)存溢出。下面這個(gè)異常信息在開(kāi)發(fā)中應(yīng)該時(shí)常遇到:
java.lang.OutOfMemoryError: bitmap size exceeds VM budget
因此如何高效的加載Bitmap是一個(gè)很重要也很容易被開(kāi)發(fā)者忽視的問(wèn)題。
接著介紹Android中常用的緩存策略,緩存策略是一個(gè)通用的思想,可以用在很多場(chǎng)景中,但是實(shí)際開(kāi)發(fā)中經(jīng)常需要用Bitmap做緩存。通過(guò)緩存策略,我們不需要每次都從網(wǎng)絡(luò)上請(qǐng)求圖片或者從存儲(chǔ)設(shè)備中加載圖片,這樣就極大的提高了圖片的加載效率以及產(chǎn)品的用戶體驗(yàn)。目前比較常用的緩存策略是LruCache和DiskLruCache,其中LruCache常被用作內(nèi)存緩存,而DiskLruCache常被用作存儲(chǔ)緩存。Lru是Least Recently Used的縮寫(xiě),即最近最少使用算法,這種算法的核心思想為:當(dāng)緩存快滿時(shí),會(huì)淘汰近期最少使用的緩存目標(biāo),很顯然Lru算法的思想是很容易被接受的。
Bitmap的高效加載
在介紹Bitmap的高效加載之前,先說(shuō)一下如何加載一個(gè)Bitmap,Bitmap在Android中指的是一張圖片,可以是png格式也可以是jpg等其他常見(jiàn)的圖片格式。那么如何加載一個(gè)圖片呢?BitmapFactory類(lèi)提供了四種方法:decodeFile,decodeResource,decodeStream和decodeByteArray,分別用于支持從文件系統(tǒng),資源,輸入流以及字節(jié)數(shù)組中加載出一個(gè)Bitmap對(duì)象,其中decodeFile和decodeResource又間接調(diào)用了decodeStream方法,這四類(lèi)方法最終是在Android底層實(shí)現(xiàn)的,對(duì)應(yīng)著B(niǎo)itmapFactory類(lèi)的幾個(gè)native方法。
如何高效的加載Bitmap呢?其實(shí)核心思想也很簡(jiǎn)單,那就是采用BitmapFactory.Option來(lái)加載所需尺寸的圖片。這里假設(shè)通過(guò)ImageView來(lái)顯示圖片,很多時(shí)候ImageView并沒(méi)有圖片的原始尺寸那么大,這個(gè)時(shí)候把整個(gè)圖片加載進(jìn)來(lái)后在設(shè)給ImageView,這顯然是沒(méi)有必要的,因?yàn)镮mageView并沒(méi)有辦法顯示原始的圖片。通過(guò)BitmapFactory.Option就可以按一定的采樣率來(lái)加載縮小后的圖片,將縮小后的圖片在ImageView中顯示,這樣就會(huì)降低內(nèi)存占用從而在一定程度上避免OOM,提高了Bitmap加載時(shí)的性能。BitmapFactory提供的加載圖片的四類(lèi)方法都支持BitmapFactory.Option參數(shù),通過(guò)他們就可以很方便地對(duì)一個(gè)圖片進(jìn)行采樣縮放。
通過(guò)BitmapFactory.Option來(lái)縮放圖片,主要是用到了它的inSampleSize參數(shù),即采樣率。當(dāng)inSampleSize為1時(shí),采樣后的圖片大小為圖片的原始大?。划?dāng)inSampleSize大于1時(shí),比如為2,那么采樣后的圖片其寬/高均為原圖大小的1/2,而像素?cái)?shù)為原圖的1/4,其占有的內(nèi)存大小也為原圖的1/4??梢园l(fā)現(xiàn)inSampleSize必須是大于1的整數(shù)圖片才會(huì)有縮小的效果,并且采樣率同時(shí)作用于寬/高,這將導(dǎo)致縮放后的圖片大小以采樣率的2次方形式遞減。當(dāng)inSampleSize小于1時(shí),其作用相當(dāng)于1,即無(wú)縮放效果。另外最新的官方文檔中指出,inSampleSize的取值應(yīng)該總是為2的指數(shù),比如1,2,4,8,16,等等。如果外界傳遞給系統(tǒng)的inSampleSize補(bǔ)位2 的指數(shù),那么系統(tǒng)會(huì)向下取整并選擇最接近的2的指數(shù)來(lái)代替,比如3,系統(tǒng)會(huì)選擇2來(lái)代替,但是經(jīng)過(guò)驗(yàn)證發(fā)現(xiàn)這個(gè)結(jié)論并非在所有的Android版本上都成立,因此把它當(dāng)成一個(gè)開(kāi)發(fā)建議即可。
考慮到以下的實(shí)際情況,比如ImageView的大小是100100像素,而圖片的原始大小為200200,那么只需將采樣率inSampleSize設(shè)為2即可。但是圖片的大小為200300呢?這個(gè)時(shí)候采樣率還應(yīng)該選擇2,這樣縮放后的圖片大小為100150像素,仍然是適合ImageView的,如果采樣率是3,那么縮放后的圖片大小就會(huì)小于ImageView所期望的大小,這樣圖片就會(huì)被拉抻從而導(dǎo)致模糊。
通過(guò)采樣率即可有效地加載圖片,那么到底如何獲取采樣路呢?獲取采樣率也很簡(jiǎn)單,遵循如下流程:
(1)將BitmapFactory.Option的inJustDecodeBounds參數(shù)設(shè)置為true并加載圖片。
(2)從BitmapFactory.Option中取出圖片的原始寬高信息,它們對(duì)應(yīng)于outWidth和outHeight參數(shù)。
(3)根據(jù)采樣率的規(guī)則并結(jié)合目標(biāo)View的所需大小計(jì)算出采樣率inSampleSize。
(4)將BitmapFactory.Option的inJustDecodeBounds參數(shù)設(shè)置為false,然后重新加載圖片。
經(jīng)過(guò)上面的4個(gè)步驟,加載出的圖片就是最終縮放后的圖片,當(dāng)然也有可能不需要縮放。這里說(shuō)明一下inJustDecodeBounds參數(shù),當(dāng)此參數(shù)設(shè)置為true時(shí),BitmapFactory只會(huì)解析圖片的原始寬/高信息,并不會(huì)真正地加載圖片,所以這個(gè)操作是輕量級(jí)的。另外需要注意的是,這個(gè)時(shí)候BitmapFactory獲取的圖片寬/高信息和圖片的位置以及程序運(yùn)行的設(shè)備有關(guān),比如同一張圖片放在不同的drawable目錄下或者程序運(yùn)行在不同屏幕密度的設(shè)備上,這都可能導(dǎo)致BitmapFactory獲取到不同的結(jié)果,之所以會(huì)出現(xiàn)這個(gè)現(xiàn)象,這和Android的資源加載機(jī)制有關(guān)。
Android中的緩存策略
緩存策略在Android中有著廣泛的使用場(chǎng)景,尤其在圖片加載這個(gè)場(chǎng)景下,緩存策略就變得更為重要??紤]一種場(chǎng)景:有一批網(wǎng)絡(luò)圖片,需要下載后在用戶界面上予以顯示,這個(gè)場(chǎng)景在pc環(huán)境下是很簡(jiǎn)單的,直接把所有的圖片下載到本地在顯示即可,但是放到移動(dòng)設(shè)備上就不一樣了。不管是Android還是ios設(shè)備,流量對(duì)于用戶來(lái)說(shuō)都是一種寶貴的資源,由于流量是收費(fèi)的,所以在應(yīng)用開(kāi)發(fā)中并不能過(guò)多地消耗用戶的流量。
如何避免過(guò)多的流量消耗呢?那就是本節(jié)要討論的主題:緩存。當(dāng)程序第一次從網(wǎng)絡(luò)加載圖片后,就將其緩存到存儲(chǔ)設(shè)備上,這樣下次使用這張圖片就不用再?gòu)木W(wǎng)絡(luò)上獲取了,這樣就節(jié)省了用戶的流量。很多時(shí)候?yàn)榱颂岣邞?yīng)用的用戶體驗(yàn),往往還會(huì)把圖片在內(nèi)存中在緩存一份,這樣當(dāng)應(yīng)用打算從網(wǎng)絡(luò)山請(qǐng)求一張圖片時(shí),程序會(huì)首先從內(nèi)存中去獲取,如果內(nèi)存中沒(méi)有那就從存儲(chǔ)設(shè)備中去獲取,如果存儲(chǔ)設(shè)備中也沒(méi)有,那就從網(wǎng)絡(luò)上下載這張圖片。因?yàn)閺膬?nèi)存中加載圖片比從存儲(chǔ)設(shè)備中加載圖片要快,所以這樣即提高了程序的效率又為用戶節(jié)約了不必要的流量開(kāi)銷(xiāo)。上述的緩存策略不僅僅適用于圖片,也適用于其他的文件類(lèi)型。
說(shuō)到緩存策略,其實(shí)并沒(méi)有統(tǒng)一的標(biāo)準(zhǔn)。一般來(lái)說(shuō),緩存策略主要包含緩存的添加,獲取和刪除這三類(lèi)操作。如何添加和獲取緩存這個(gè)比較好理解,那為什么還要?jiǎng)h除緩存呢?這是因?yàn)椴还苁莾?nèi)存緩存還是存儲(chǔ)設(shè)備緩存,它們的緩存大小都是有限制的,因?yàn)閮?nèi)存和諸如sd卡之類(lèi)的存儲(chǔ)設(shè)備都是有容量限制的,因此在使用緩存時(shí)總是要為緩存指定一個(gè)最大的容量。如果緩存滿了,但是程序還是要向其添加緩存,這個(gè)時(shí)候就需要?jiǎng)h除一些舊的緩存并添加新的緩存,如何定義緩存的新舊這就是一種策略,不同的策略就應(yīng)對(duì)著不同的緩存算法,比如可以簡(jiǎn)單的根據(jù)文件的最后修改時(shí)間來(lái)定義緩存的新舊,當(dāng)緩存滿時(shí)就將最后修改時(shí)間較早的緩存移除,這就是一種緩存算法,但是這種算法并不算完美。
目前常用的一種緩存算法是LRU(Least Recently Used),LRU是近期最少使用算法,它的核心思想是當(dāng)緩存滿時(shí),會(huì)優(yōu)先淘汰那些近期最少使用的緩存對(duì)象。采用LRU算法的緩存有兩種:LruCache和DisLruCache,LruCache用于實(shí)現(xiàn)內(nèi)存緩存,而DisLruCache則充當(dāng)了存儲(chǔ)設(shè)備緩存,通過(guò)這二者的完美結(jié)合,就可以很方便的實(shí)現(xiàn)一個(gè)具有很高實(shí)用價(jià)值的ImageLoader。
LruCache
LruCache是一個(gè)泛型類(lèi),它的內(nèi)部采用一個(gè)LinkedHashMap以強(qiáng)引用的方式存儲(chǔ)外界的緩存對(duì)象,其提供了get和put方法來(lái)完成緩存的獲取和添加操作,當(dāng)緩存滿時(shí),LruCache會(huì)移除較早使用的緩存對(duì)象,然后再添加新的緩存對(duì)象。首先要明白強(qiáng)引用,軟引用和弱引用的區(qū)別。
強(qiáng)引用:直接的對(duì)象引用;
軟引用:當(dāng)一個(gè)對(duì)象只有軟引用存在時(shí),系統(tǒng)內(nèi)存不足時(shí)此對(duì)象會(huì)被gc回收;
弱引用:當(dāng)一個(gè)對(duì)象只有弱引用存在時(shí),此對(duì)象會(huì)隨時(shí)被gc回收;
另外,LruCache是線程安全的,下面是LruCache的定義:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
}
LruCache的實(shí)現(xiàn)比較簡(jiǎn)單,下面代碼展示LruCache的典型的初始化過(guò)程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
LruCache<String, Bitmap> mMemoryCahce = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
};
在上面代碼中,只需要提供緩存的總?cè)萘看笮〔⒅貙?xiě)sizeOf方法即可。sizeOf方法的作用是計(jì)算緩存對(duì)象的大小,這里大小的單位需要和總?cè)萘康膯挝灰恢?。?duì)于上面的示例代碼來(lái)說(shuō),總?cè)萘康拇笮楫?dāng)前進(jìn)程的可用內(nèi)存的1/8,單位為kb,而sizeOf方法則完成了Bitmap對(duì)象的大小計(jì)算。很明顯,之所以除以1024也是為了將其單位轉(zhuǎn)化為kb。一些特殊情況下還需要重寫(xiě)LruCache的entryRemove方法,LruCache移除舊緩存時(shí)會(huì)調(diào)用到entryRemove方法,因此可在entryRemove中完成一些資源回收工作。
除了LruCache的創(chuàng)建以外,還有緩存的獲取和添加,這也很簡(jiǎn)單,從LruCache中獲取一個(gè)緩存對(duì)象,如下所示
mMemoryCahce.get(key)
向LruCache中添加一個(gè)緩存對(duì)象,如下所示
mMemoryCahce.pet(key, bitmap)
LruCache還支持刪除操作,通過(guò)remove方法即可刪除一個(gè)指定的緩存對(duì)象??梢钥吹絃ruCache的實(shí)現(xiàn)以及使用都非常簡(jiǎn)單,雖然簡(jiǎn)單,但是仍然不影響它具有強(qiáng)大的功能。
DiskLruCache
DiskLruCache用于實(shí)現(xiàn)存儲(chǔ)設(shè)備緩存,即磁盤(pán)緩存,它通過(guò)將緩存對(duì)象寫(xiě)入文件系統(tǒng)從而實(shí)現(xiàn)緩存的效果。
DiskLruCache的創(chuàng)建
DiskLruCache并不能通過(guò)構(gòu)造方法來(lái)創(chuàng)建,它提供了open方法用于創(chuàng)建自身,如下所示
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
open方法有四個(gè)參數(shù),其中第一個(gè)參數(shù)表示磁盤(pán)緩存在文件系統(tǒng)中的存儲(chǔ)路徑。緩存路徑可以選擇SD卡上的緩存目錄。
第二個(gè)參數(shù)表示應(yīng)用的版本號(hào),一般設(shè)為1即可。
第三個(gè)參數(shù)表示但個(gè)節(jié)點(diǎn)所對(duì)應(yīng)的數(shù)據(jù)的個(gè)數(shù),一般設(shè)為1即可。
第四個(gè)參數(shù)表述緩存的總大小,比如50M,當(dāng)緩存大小超出這個(gè)設(shè)定值后,DiskLruCache會(huì)清除一些緩存從而保證總大小不大于這個(gè)設(shè)定值。
DiskLruCache的緩存添加
DiskLruCache的緩存添加的操作是通過(guò)Editor完成的,Editor表示一個(gè)緩存對(duì)象的編輯對(duì)象。這里仍然以圖片緩存舉例,首先需要獲取圖片url所對(duì)應(yīng)的key,然后根據(jù)key就可以通過(guò)edit()來(lái)獲取Editor對(duì)象,如果這個(gè)緩存正在被編輯,那么edit()會(huì)返回null,即DiskLruCache不允許同時(shí)編輯一個(gè)緩存對(duì)象。之所以要把url轉(zhuǎn)換成key,是因?yàn)閳D片的url中很可能有特殊字符,這將影響url在Android中直接使用,一般采用url的md5值作為key。
ImageLoader的實(shí)現(xiàn)
一般來(lái)說(shuō),一個(gè)優(yōu)秀的ImageLoader應(yīng)該具備如下功能
1.圖片的同步加載
2.圖片的異步加載
3.圖片壓縮
4.內(nèi)存緩存
5.磁盤(pán)緩存
6.網(wǎng)絡(luò)拉取
圖片的同步加載是指能夠以同步的方式向調(diào)用者提供所加載的圖片,這個(gè)圖片可能是從內(nèi)存緩存中讀取的,也可能是從磁盤(pán)緩存中讀取的,還可能是從網(wǎng)絡(luò)拉取的。圖片的異步加載是一個(gè)很有用的功能,很多時(shí)候調(diào)用者不想在單獨(dú)的線程中以同步的方式來(lái)獲取圖片,這個(gè)時(shí)候ImageLoader內(nèi)部需要自己在線程中加載圖片并將圖片設(shè)置給所需的ImageView。圖片壓縮的作用更毋庸置疑了,這是降低OOM概率的有效手段,ImageLoader必須合適地處理圖片的壓縮問(wèn)題。
內(nèi)存緩存和磁盤(pán)緩存是ImageLoader的核心,也是ImageLoader的意義之所在,通過(guò)這兩級(jí)緩存極大地提高了程序的效率并且有效地降低了對(duì)用戶所造成的流量消耗,只有當(dāng)這兩級(jí)緩存都不可用時(shí)才需要從網(wǎng)絡(luò)中拉取圖片。
除此之外,ImageLoader還需要處理一些特殊的情況,比如在ListvView或者GridView中,View復(fù)用即是它們的優(yōu)點(diǎn)也是它們的缺點(diǎn)。例如,在ListvView或者GridView中,假設(shè)一個(gè)item A正在從網(wǎng)絡(luò)加載圖片,它對(duì)應(yīng)的ImageView為A,這個(gè)時(shí)候用戶快速向下滑動(dòng)列表,很可能item B復(fù)用了ImageView A,然后等一會(huì)之前的圖片下載完畢了。如果直接給ImageView A設(shè)置圖片,由于這時(shí)候ImageView A被item B所復(fù)用,但是item B要顯示的圖片顯然不是item A剛剛下載好的圖片,這個(gè)時(shí)候就會(huì)出現(xiàn)item B中顯示了item A的圖片,這就是常見(jiàn)的列表錯(cuò)位的問(wèn)題,ImageLoader需要正確的處理這些特殊情況。
上面對(duì)ImageLoader的功能做了一個(gè)全面的分析,下面就可以一步步實(shí)現(xiàn)一個(gè)ImageLoader了,這里主要分為以下幾步。
圖片壓縮功能的實(shí)現(xiàn)
public class ImageResizer {
private static final String TAG = "ImageResizer";
public ImageResizer() {
}
/**
*
* @param res Resources資源
* @param resId 加載的圖片id
* @param reqWidth 期望的加載圖片的寬
* @param reqHeight 期望的加載圖片的高
* @return
*/
public Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
//將options.inJustDecodeBounds設(shè)置為true并加載圖片。設(shè)置為true時(shí),BitmapFactory只會(huì)解析圖片的寬高信息,并不會(huì)真正的加載圖片
options.inJustDecodeBounds = true;
//加載Bitmap
BitmapFactory.decodeResource(res, resId, options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//將options.inJustDecodeBounds設(shè)置為false,這樣就會(huì)完全加載Bitmap
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFileDescriptor(fd,null,options);
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
options.inJustDecodeBounds = false;
return BitmapFactory.decodeFileDescriptor(fd,null,options);
}
public int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
if (reqWidth == 0 || reqHeight == 0) {
return 1;
}
//得到加載的圖片的寬高
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
final int halfHeight = height / 2;
final int halfWidth = width / 2;
//將圖片多次縮小到傳入的參數(shù)范圍內(nèi),并計(jì)算出最終的inSampleSize值
while ((halfHeight / inSampleSize) >= reqHeight
&& (halfWidth / inSampleSize >= reqWidth)) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}