Bitmap的高效加載
如何加載一個(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的核心思想:采用BitmapFactory.Options來(lái)加載所需尺寸的圖片. 比如說(shuō)一個(gè)ImageView控件的大小為300300. 而圖片的大小為800800. 這個(gè)時(shí)候如果直接加載那么就比較浪費(fèi)資源, 需要更多的內(nèi)存空間來(lái)加載圖片, 這不是很必要的. 這里我們就可以先把圖片按一定的采樣率來(lái)縮小圖片在進(jìn)行加載. 不僅降低了內(nèi)存占用,還在一定程度上避免了OOM異常. 也提高了加載bitmap時(shí)的性能.
而通過(guò)Options參數(shù)來(lái)縮放圖片: 主要是用到了inSampleSize參數(shù), 即采樣率陷寝。
如果是inSampleSize=1那么和原圖大小一樣,
如果是inSampleSize=2那么寬高都為原圖1/2, 而像素為原圖的1/4, 占用的內(nèi)存大小也為原圖的1/4
如果是inSampleSize=3那么寬高都為原圖1/3, 而像素為原圖的1/9, 占用的內(nèi)存大小也為原圖的1/9
以此類(lèi)推…..
要知道Android中加載圖片具體在內(nèi)存中的占有的大小是根據(jù)圖片的像素決定的, 而與圖片的實(shí)際占用空間大小沒(méi)有關(guān)系.而且如果要加載mipmap下的圖片, 還會(huì)根據(jù)不同的分辨率下的文件夾進(jìn)行不同的放大縮小.
列舉現(xiàn)在有一張圖片像素為:10241024, 如果采用ARGB8888(四個(gè)顏色通道每個(gè)占有一個(gè)字節(jié),相當(dāng)于1點(diǎn)像素占用4個(gè)字節(jié)的空間)的格式來(lái)存儲(chǔ).(這里不考慮不同的資源文件下情況分析) 那么圖片的占有大小就是102410244那現(xiàn)在這張圖片在內(nèi)存中占用4MB.
如果針對(duì)剛才的圖片進(jìn)行inSampleSize=2, 那么最后占用內(nèi)存大小為512512*4, 也就是1MB
采樣率的數(shù)值必須是大于1的整數(shù)是才會(huì)有縮放效果, 并且采樣率同時(shí)作用于寬/高, 這將導(dǎo)致縮放后的圖片以這個(gè)采樣率的2次方遞減, 即內(nèi)存占用縮放大小為1/(inSampleSize的二次方). 如果小于1那么相當(dāng)于=1的時(shí)候. 在官方文檔中指出, inSampleSize的取值應(yīng)該總是為2的指數(shù), 比如1,2,4,8,16,32…如果外界傳遞inSampleSize不為2的指數(shù), 那么系統(tǒng)會(huì)向下取整并選擇一個(gè)最接近的2的指數(shù)來(lái)代替. 比如如果inSampleSize=3,那么系統(tǒng)會(huì)選擇2來(lái)代替. 但是這條規(guī)則并不作用于所有的android版本, 所以可以當(dāng)成一個(gè)開(kāi)發(fā)建議
整理一下開(kāi)發(fā)中代碼流程:
將BitmapFactory.Options的inJustDecodeBounds參數(shù)設(shè)置為true并加載圖片沪袭。
從BitmapFactory.Options取出圖片的原始寬高信息, 他們對(duì)應(yīng)于outWidth和outHeight參數(shù)枝缔。
根據(jù)采樣率的規(guī)則并結(jié)合目標(biāo)View的所需大小計(jì)算出采樣率inSampleSize。
將BitmapFactory.Options的inJustDecodeBounds參數(shù)設(shè)為false, 然后重新加載豺瘤。
inJustDecodeBounds這個(gè)參數(shù)的作用就是在加載圖片的時(shí)候是否只是加載圖片寬高信息而不把圖片全部加載到內(nèi)存. 所以這個(gè)操作是個(gè)輕量級(jí)的.
通過(guò)這些步驟就可以整理出以下的工具加載圖片類(lèi)調(diào)用decodeFixedSizeForResource()即可.
public class MyBitmapLoadUtil {
/**
* 對(duì)一個(gè)Resources的資源文件進(jìn)行指定長(zhǎng)寬來(lái)加載進(jìn)內(nèi)存, 并把這個(gè)bitmap對(duì)象返回
*
* @param res 資源文件對(duì)象
* @param resId 要操作的圖片id
* @param reqWidth 最終想要得到bitmap的寬度
* @param reqHeight 最終想要得到bitmap的高度
* @return 返回采樣之后的bitmap對(duì)象
*/
public static Bitmap decodeFixedSizeForResource(Resources res, int resId, int reqWidth, int reqHeight){
// 首先先指定加載的模式 為只是獲取資源文件的大小
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
//Calculate Size 計(jì)算要設(shè)置的采樣率 并把值設(shè)置到option上
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 關(guān)閉只加載屬性模式, 并重新加載的時(shí)候傳入自定義的options對(duì)象
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* 一個(gè)計(jì)算工具類(lèi)的方法, 傳入圖片的屬性對(duì)象和 想要實(shí)現(xiàn)的目標(biāo)大小. 通過(guò)計(jì)算得到采樣值
*/
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
//Raw height and width of image
//原始圖片的寬高屬性
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
// 如果想要實(shí)現(xiàn)的寬高比原始圖片的寬高小那么就可以計(jì)算出采樣率, 否則不需要改變采樣率
if (reqWidth < height || reqHeight < width){
int halfWidth = width/2;
int halfHeight = height/2;
// 判斷原始長(zhǎng)寬的一半是否比目標(biāo)大小小, 如果小那么增大采樣率2倍, 直到出現(xiàn)修改后原始值會(huì)比目標(biāo)值大的時(shí)候
while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
android中的緩存策略
1.lrucache
lrucache是api level 12提供的一個(gè)泛型類(lèi),它內(nèi)部采用一個(gè)linkedhashmap以強(qiáng)引用的方式存儲(chǔ)外界的緩存對(duì)象听诸,提供了get和put方法來(lái)完成緩存的獲取和添加操作坐求,當(dāng)緩存滿(mǎn)了,lrucache會(huì)remove掉較早使用的緩存對(duì)象晌梨,然后再添加新的對(duì)象桥嗤。
過(guò)去實(shí)現(xiàn)內(nèi)存緩存的常用做法是使用softreference或者使用weakreference须妻,但是并不推薦這種做法,從api level 9以后泛领,gc強(qiáng)制回收掉soft荒吏、weak引用,從而導(dǎo)致這些緩存并沒(méi)有任何效率的提升渊鞋。
lrucache的實(shí)現(xiàn)原理:
根據(jù)lru的算法思想绰更,我們需要一種數(shù)據(jù)結(jié)構(gòu)來(lái)快速定位哪個(gè)對(duì)象是最近訪(fǎng)問(wèn)的,哪個(gè)對(duì)象是最長(zhǎng)時(shí)間未訪(fǎng)問(wèn)的锡宋,lrucache選擇的是linkedhashmap這個(gè)數(shù)據(jù)結(jié)構(gòu)儡湾,它是一個(gè)雙向循環(huán)鏈表。來(lái)瞅一眼linkedhashmap的構(gòu)造函數(shù):
/** 初始化linkedhashmap
* 第一個(gè)參數(shù):initialcapacity执俩,初始大小
* 第二個(gè)參數(shù):loadfactor徐钠,負(fù)載因子=0.75f
* 第三個(gè)參數(shù):accessorder=true,基于訪(fǎng)問(wèn)順序役首;accessorder=false丹皱,基于插入順序<br/>
public linkedhashmap(int initialcapacity, float loadfactor, boolean accessorder) {
super(initialcapacity, loadfactor);
init();
this.accessorder = accessorder;
}
所以在lrucache中應(yīng)該選擇accessorder = true,當(dāng)我們調(diào)用put宋税、get方法時(shí)摊崭,linkedhashmap內(nèi)部會(huì)將這個(gè)item移動(dòng)到鏈表的尾部,即在鏈表尾部是最近剛剛使用的item杰赛,鏈表頭部就是最近最少使用的item呢簸。當(dāng)緩存空間不足時(shí),可以remove頭部結(jié)點(diǎn)釋放緩存空間乏屯。
下面舉例lrucache的典型使用姿勢(shì):
int maxmemory = (int) (runtime.getruntime().maxmemory() / 1024);
int cachesize = maxmemory / 8;
mmemorycache = new lrucache<string bitmap="">(cachesize) {
@override
protected int sizeof(string key, bitmap bitmap) {
return bitmap.getrowbytes() * bitmap.getheight() / 1024;
}
};
<br/>// 向 lrucache 中添加一個(gè)緩存對(duì)象
private void addbitmaptomemorycache(string key, bitmap bitmap) {
if (getbitmapfrommemcache(key) == null) {
mmemorycache.put(key, bitmap);
}
}
//獲取一個(gè)緩存對(duì)象
private bitmap getbitmapfrommemcache(string key) {
return mmemorycache.get(key);
}</string>
上述示例代碼中根时,總?cè)萘康拇笮∈钱?dāng)前進(jìn)程的可用內(nèi)存的八分之一(官方推薦是八分之一哈,你們可以自己視情況定)辰晕,sizeof()方法計(jì)算了bitmap的大小蛤迎,sizeof方法默認(rèn)返回的是你緩存item數(shù)目,源碼中直接return 1(這里的源碼比較簡(jiǎn)單含友,可以自己看看~)替裆。
如果你需要cache中某個(gè)值釋放,可以重寫(xiě)entryremoved()方法窘问,這個(gè)方法會(huì)在元素被put或者remove的時(shí)候調(diào)用辆童,源碼默認(rèn)是空實(shí)現(xiàn)。重寫(xiě)entryremoved()方法還可以實(shí)現(xiàn)二級(jí)內(nèi)存緩存惠赫,進(jìn)一步提高性能把鉴。思路如下:重寫(xiě)entryremoved(),把刪除掉的item儿咱,再次存入另一個(gè)linkedhashmap中庭砍。這個(gè)數(shù)據(jù)結(jié)構(gòu)當(dāng)做二級(jí)緩存场晶,每次獲得圖片的時(shí)候,按照一級(jí)緩存 怠缸、二級(jí)緩存峰搪、sdcard、網(wǎng)絡(luò)的順序查找凯旭,找到就停止概耻。
2.disklrucache
當(dāng)我們需要存大量圖片的時(shí)候,我們指定的緩存空間可能很快就用完了罐呼,lrucache會(huì)頻繁地進(jìn)行trimtosize操作將最近最少使用的數(shù)據(jù)remove掉鞠柄,但是hold不住過(guò)會(huì)又要用這個(gè)數(shù)據(jù),又從網(wǎng)絡(luò)download一遍嫉柴,為此有了disklrucache厌杜,它可以保存這些已經(jīng)下載過(guò)的圖片。當(dāng)然计螺,從磁盤(pán)讀取圖片的時(shí)候要比內(nèi)存慢得多夯尽,并且應(yīng)該在非ui線(xiàn)程中載入磁盤(pán)圖片。disklrucache顧名思義登馒,實(shí)現(xiàn)存儲(chǔ)設(shè)備緩存匙握,即磁盤(pán)緩存,它通過(guò)將緩存對(duì)象寫(xiě)入文件系統(tǒng)從而實(shí)現(xiàn)緩存效果陈轿。
ps: 如果緩存的圖片經(jīng)常被使用圈纺,可以考慮使用contentprovider。
disklrucache的實(shí)現(xiàn)原理:
lrucache采用的是linkedhashmap這種數(shù)據(jù)結(jié)構(gòu)來(lái)保存緩存中的對(duì)象麦射,那么對(duì)于disklrucache呢蛾娶?由于數(shù)據(jù)是緩存在本地文件中,相當(dāng)于是持久保存的一個(gè)文件潜秋,即使app kill掉蛔琅,這些文件還在滴。so ,,,,, 到底是啥峻呛?disklrucache也是采用linekedhashmap這種數(shù)據(jù)結(jié)構(gòu)罗售,但是不夠,需要加持buff
日志文件杀饵。日志文件可以看做是一塊“內(nèi)存”莽囤,map中的value只保存文件的簡(jiǎn)要信息谬擦,對(duì)緩存文件的所有操作都會(huì)記錄在日志文件中切距。
disklrucache的初始化:
下面是disklrucache的創(chuàng)建過(guò)程:
private static final long disk_cache_size = 1024 * 1024 * 50; //50mb
file diskcachedir = getdiskcachedir(mcontext, "bitmap");
if (!diskcachedir.exists()) {
diskcachedir.mkdirs();
}
if (getusablespace(diskcachedir) > disk_cache_size) {
try {
mdisklrucache = disklrucache.open(diskcachedir, 1, 1,
disk_cache_size);
} catch (ioexception e) {
e.printstacktrace();
}
}
瞅了一眼,可以知道重點(diǎn)在open()函數(shù)惨远,其中第一個(gè)參數(shù)表示文件的存儲(chǔ)路徑谜悟,緩存路徑可以是sd卡上的緩存目錄话肖,具體是指/sdcard/android/data/package_name/cache,package_name表示當(dāng)前應(yīng)用的包名葡幸,當(dāng)應(yīng)用被卸載后最筒, 此目錄會(huì)一并刪除掉。如果你希望應(yīng)用卸載后蔚叨,這些緩存文件不被刪除床蜘,可以指定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ù)表示緩存的總大小丹擎,比如50mb,當(dāng)緩存大小超過(guò)這個(gè)設(shè)定值后歇父,disklrucache會(huì)清除一些緩存保證總大小不會(huì)超過(guò)設(shè)定值
disklrucache的數(shù)據(jù)緩存與獲取緩存:
數(shù)據(jù)緩存操作是借助disklrucache.editor類(lèi)完成的蒂培,editor表示一個(gè)緩存對(duì)象的編輯對(duì)象。
new thread(new runnable() {
@override
public void run() {
try {
string imageurl = "http://d.url.cn/myapp/qq_desk/friendprofile_def_cover_001.png";
string key = hashkeyfordisk(imageurl); //md5對(duì)url進(jìn)行加密榜苫,這個(gè)主要是為了獲得統(tǒng)一的16位字符
disklrucache.editor editor = mdisklrucache.edit(key); //拿到editor护戳,往journal日志中寫(xiě)入dirty記錄
if (editor != null) {
outputstream outputstream = editor.newoutputstream(0);
if (downloadurltostream(imageurl, outputstream)) { //downloadurltostream方法為下載圖片的方法,并且將輸出流放到outputstream
editor.commit(); //完成后記得commit()垂睬,成功后灸异,再往journal日志中寫(xiě)入clean記錄
} else {
editor.abort(); //失敗后,要remove緩存文件羔飞,往journal文件中寫(xiě)入remove記錄
}
}
mdisklrucache.flush(); //將緩存操作同步到j(luò)ournal日志文件肺樟,不一定要在這里就調(diào)用
} catch (ioexception e) {
e.printstacktrace();
}
}
}).start();
上述示例代碼中,每次調(diào)用edit()方法時(shí)逻淌,會(huì)返回一個(gè)新的editor對(duì)象么伯,通過(guò)它可以得到一個(gè)文件輸出流;調(diào)用commit()方法將圖片寫(xiě)入到文件系統(tǒng)中卡儒,如果失敗田柔,通過(guò)abort()方法進(jìn)行回退。
而獲取緩存和緩存的添加過(guò)程類(lèi)似骨望,將url轉(zhuǎn)換為key硬爆,然后通過(guò)disklrucache的get方法得到一個(gè)snapshot對(duì)象,接著通過(guò)snapshot對(duì)象得到緩存的文件輸入流擎鸠。有了文件輸入流缀磕,bitmap就get到了。
bitmap bitmap = null;
string key = hashkeyformurl(url);
disklrucache.snapshot snapshot = mdisklrucache.get(key);
if (snapshot != null) {
fileinputstream fileinputstream = (fileinputstream)snapshot.getinputstream(disk_cache_index);
filedescriptor filedescriptor = fileinputstream.getfd();
bitmap = mimageresizer.decodesampledbitmapfromfiledescriptor(filedescriptor,
reqwidth, reqheight);
......
}
disklrucache優(yōu)化思考:
disklrucache是基于日志文件的,每次對(duì)緩存文件操作都需要進(jìn)行日志記錄袜蚕,我們可以不用日志文件糟把,在第一次構(gòu)造disklrucache時(shí),直接從程序訪(fǎng)問(wèn)緩存目錄下的文件牲剃,并將每個(gè)緩存文件的訪(fǎng)問(wèn)時(shí)間作為初始值記錄在map中的value值遣疯,每次訪(fǎng)問(wèn)或保存緩存都更新相應(yīng)key對(duì)應(yīng)的緩存文件的訪(fǎng)問(wèn)時(shí)間,避免了頻繁地io操作凿傅。
#####3. 緩存策略對(duì)比與總結(jié)
lrucache是android中已經(jīng)封裝好的類(lèi)缠犀,disklrucache需要導(dǎo)入相應(yīng)的包才可以使用。
可以在ui線(xiàn)程中直接使用lrucache聪舒;使用disklrucache時(shí)夭坪,由于緩存或者獲取都需要對(duì)本地文件進(jìn)行操作,因此要在子線(xiàn)程中實(shí)現(xiàn)过椎。
lrucache主要用于內(nèi)存緩存室梅,當(dāng)app kill掉的時(shí)候,緩存也跟著沒(méi)了疚宇;而disklrucache主要用于存儲(chǔ)設(shè)備緩存亡鼠,app kill掉的時(shí)候,緩存還在
lrucache的內(nèi)部實(shí)現(xiàn)是linkedhashmap敷待,對(duì)于元素的添加或獲取用put间涵、get方法即可。而disklrucache是通過(guò)文件流的形式進(jìn)行緩存榜揖,所以對(duì)于元素的添加或獲取通過(guò)輸入輸出流來(lái)實(shí)現(xiàn)勾哩。