Bitmap的高效加載
如何加載一個圖片?首先BitmapFactory類提供了四種方法: decodeFile(), decodeResource(), decodeStream(), decodeByteArray(). 分別用于從文件系統, 資源文件, 輸入流以及字節數組加載出一個Bitmap對象. 其中decodeFile和decodeResource又間接調用了decodeStream()方法, 這四類方法最終是在Android的底層實現的, 對應著BitmapFactory類的幾個native方法.
高效加載的Bitmap的核心思想:采用BitmapFactory.Options來加載所需尺寸的圖片. 比如說一個ImageView控件的大小為300300. 而圖片的大小為800800. 這個時候如果直接加載那么就比較浪費資源, 需要更多的內存空間來加載圖片, 這不是很必要的. 這里我們就可以先把圖片按一定的采樣率來縮小圖片在進行加載. 不僅降低了內存占用,還在一定程度上避免了OOM異常. 也提高了加載bitmap時的性能.
而通過Options參數來縮放圖片: 主要是用到了inSampleSize參數, 即采樣率。
如果是inSampleSize=1那么和原圖大小一樣,
如果是inSampleSize=2那么寬高都為原圖1/2, 而像素為原圖的1/4, 占用的內存大小也為原圖的1/4
如果是inSampleSize=3那么寬高都為原圖1/3, 而像素為原圖的1/9, 占用的內存大小也為原圖的1/9
以此類推…..
要知道Android中加載圖片具體在內存中的占有的大小是根據圖片的像素決定的, 而與圖片的實際占用空間大小沒有關系.而且如果要加載mipmap下的圖片, 還會根據不同的分辨率下的文件夾進行不同的放大縮小.
列舉現在有一張圖片像素為:10241024, 如果采用ARGB8888(四個顏色通道每個占有一個字節,相當于1點像素占用4個字節的空間)的格式來存儲.(這里不考慮不同的資源文件下情況分析) 那么圖片的占有大小就是102410244那現在這張圖片在內存中占用4MB.
如果針對剛才的圖片進行inSampleSize=2, 那么最后占用內存大小為512512*4, 也就是1MB
采樣率的數值必須是大于1的整數是才會有縮放效果, 并且采樣率同時作用于寬/高, 這將導致縮放后的圖片以這個采樣率的2次方遞減, 即內存占用縮放大小為1/(inSampleSize的二次方). 如果小于1那么相當于=1的時候. 在官方文檔中指出, inSampleSize的取值應該總是為2的指數, 比如1,2,4,8,16,32…如果外界傳遞inSampleSize不為2的指數, 那么系統會向下取整并選擇一個最接近的2的指數來代替. 比如如果inSampleSize=3,那么系統會選擇2來代替. 但是這條規則并不作用于所有的android版本, 所以可以當成一個開發建議
整理一下開發中代碼流程:
將BitmapFactory.Options的inJustDecodeBounds參數設置為true并加載圖片。
從BitmapFactory.Options取出圖片的原始寬高信息, 他們對應于outWidth和outHeight參數。
根據采樣率的規則并結合目標View的所需大小計算出采樣率inSampleSize。
將BitmapFactory.Options的inJustDecodeBounds參數設為false, 然后重新加載。
inJustDecodeBounds這個參數的作用就是在加載圖片的時候是否只是加載圖片寬高信息而不把圖片全部加載到內存. 所以這個操作是個輕量級的.
通過這些步驟就可以整理出以下的工具加載圖片類調用decodeFixedSizeForResource()即可.
public class MyBitmapLoadUtil {
/**
* 對一個Resources的資源文件進行指定長寬來加載進內存, 并把這個bitmap對象返回
*
* @param res 資源文件對象
* @param resId 要操作的圖片id
* @param reqWidth 最終想要得到bitmap的寬度
* @param reqHeight 最終想要得到bitmap的高度
* @return 返回采樣之后的bitmap對象
*/
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 計算要設置的采樣率 并把值設置到option上
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 關閉只加載屬性模式, 并重新加載的時候傳入自定義的options對象
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
/**
* 一個計算工具類的方法, 傳入圖片的屬性對象和 想要實現的目標大小. 通過計算得到采樣值
*/
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;
// 如果想要實現的寬高比原始圖片的寬高小那么就可以計算出采樣率, 否則不需要改變采樣率
if (reqWidth < height || reqHeight < width){
int halfWidth = width/2;
int halfHeight = height/2;
// 判斷原始長寬的一半是否比目標大小小, 如果小那么增大采樣率2倍, 直到出現修改后原始值會比目標值大的時候
while((halfHeight/inSampleSize) >= reqHeight && (halfWidth/inSampleSize) >= reqWidth){
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
android中的緩存策略
1.lrucache
lrucache是api level 12提供的一個泛型類,它內部采用一個linkedhashmap以強引用的方式存儲外界的緩存對象,提供了get和put方法來完成緩存的獲取和添加操作,當緩存滿了,lrucache會remove掉較早使用的緩存對象,然后再添加新的對象。
過去實現內存緩存的常用做法是使用softreference或者使用weakreference,但是并不推薦這種做法,從api level 9以后,gc強制回收掉soft、weak引用,從而導致這些緩存并沒有任何效率的提升。
lrucache的實現原理:
根據lru的算法思想,我們需要一種數據結構來快速定位哪個對象是最近訪問的,哪個對象是最長時間未訪問的,lrucache選擇的是linkedhashmap這個數據結構,它是一個雙向循環鏈表。來瞅一眼linkedhashmap的構造函數:
/** 初始化linkedhashmap
* 第一個參數:initialcapacity,初始大小
* 第二個參數:loadfactor,負載因子=0.75f
* 第三個參數:accessorder=true,基于訪問順序;accessorder=false,基于插入順序<br/>
public linkedhashmap(int initialcapacity, float loadfactor, boolean accessorder) {
super(initialcapacity, loadfactor);
init();
this.accessorder = accessorder;
}
所以在lrucache中應該選擇accessorder = true,當我們調用put、get方法時,linkedhashmap內部會將這個item移動到鏈表的尾部,即在鏈表尾部是最近剛剛使用的item,鏈表頭部就是最近最少使用的item。當緩存空間不足時,可以remove頭部結點釋放緩存空間。
下面舉例lrucache的典型使用姿勢:
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 中添加一個緩存對象
private void addbitmaptomemorycache(string key, bitmap bitmap) {
if (getbitmapfrommemcache(key) == null) {
mmemorycache.put(key, bitmap);
}
}
//獲取一個緩存對象
private bitmap getbitmapfrommemcache(string key) {
return mmemorycache.get(key);
}</string>
上述示例代碼中,總容量的大小是當前進程的可用內存的八分之一(官方推薦是八分之一哈,你們可以自己視情況定),sizeof()方法計算了bitmap的大小,sizeof方法默認返回的是你緩存item數目,源碼中直接return 1(這里的源碼比較簡單,可以自己看看~)。
如果你需要cache中某個值釋放,可以重寫entryremoved()方法,這個方法會在元素被put或者remove的時候調用,源碼默認是空實現。重寫entryremoved()方法還可以實現二級內存緩存,進一步提高性能。思路如下:重寫entryremoved(),把刪除掉的item,再次存入另一個linkedhashmap中。這個數據結構當做二級緩存,每次獲得圖片的時候,按照一級緩存 、二級緩存、sdcard、網絡的順序查找,找到就停止。
2.disklrucache
當我們需要存大量圖片的時候,我們指定的緩存空間可能很快就用完了,lrucache會頻繁地進行trimtosize操作將最近最少使用的數據remove掉,但是hold不住過會又要用這個數據,又從網絡download一遍,為此有了disklrucache,它可以保存這些已經下載過的圖片。當然,從磁盤讀取圖片的時候要比內存慢得多,并且應該在非ui線程中載入磁盤圖片。disklrucache顧名思義,實現存儲設備緩存,即磁盤緩存,它通過將緩存對象寫入文件系統從而實現緩存效果。
ps: 如果緩存的圖片經常被使用,可以考慮使用contentprovider。
disklrucache的實現原理:
lrucache采用的是linkedhashmap這種數據結構來保存緩存中的對象,那么對于disklrucache呢?由于數據是緩存在本地文件中,相當于是持久保存的一個文件,即使app kill掉,這些文件還在滴。so ,,,,, 到底是啥?disklrucache也是采用linekedhashmap這種數據結構,但是不夠,需要加持buff
日志文件。日志文件可以看做是一塊“內存”,map中的value只保存文件的簡要信息,對緩存文件的所有操作都會記錄在日志文件中。
disklrucache的初始化:
下面是disklrucache的創建過程:
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();
}
}
瞅了一眼,可以知道重點在open()函數,其中第一個參數表示文件的存儲路徑,緩存路徑可以是sd卡上的緩存目錄,具體是指/sdcard/android/data/package_name/cache,package_name表示當前應用的包名,當應用被卸載后, 此目錄會一并刪除掉。如果你希望應用卸載后,這些緩存文件不被刪除,可以指定sd卡上其他目錄。第二個參數表示應用的版本號,一般設為1即可。第三個參數表示單個結點所對應數據的個數,一般設為1。第四個參數表示緩存的總大小,比如50mb,當緩存大小超過這個設定值后,disklrucache會清除一些緩存保證總大小不會超過設定值
disklrucache的數據緩存與獲取緩存:
數據緩存操作是借助disklrucache.editor類完成的,editor表示一個緩存對象的編輯對象。
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對url進行加密,這個主要是為了獲得統一的16位字符
disklrucache.editor editor = mdisklrucache.edit(key); //拿到editor,往journal日志中寫入dirty記錄
if (editor != null) {
outputstream outputstream = editor.newoutputstream(0);
if (downloadurltostream(imageurl, outputstream)) { //downloadurltostream方法為下載圖片的方法,并且將輸出流放到outputstream
editor.commit(); //完成后記得commit(),成功后,再往journal日志中寫入clean記錄
} else {
editor.abort(); //失敗后,要remove緩存文件,往journal文件中寫入remove記錄
}
}
mdisklrucache.flush(); //將緩存操作同步到journal日志文件,不一定要在這里就調用
} catch (ioexception e) {
e.printstacktrace();
}
}
}).start();
上述示例代碼中,每次調用edit()方法時,會返回一個新的editor對象,通過它可以得到一個文件輸出流;調用commit()方法將圖片寫入到文件系統中,如果失敗,通過abort()方法進行回退。
而獲取緩存和緩存的添加過程類似,將url轉換為key,然后通過disklrucache的get方法得到一個snapshot對象,接著通過snapshot對象得到緩存的文件輸入流。有了文件輸入流,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優化思考:
disklrucache是基于日志文件的,每次對緩存文件操作都需要進行日志記錄,我們可以不用日志文件,在第一次構造disklrucache時,直接從程序訪問緩存目錄下的文件,并將每個緩存文件的訪問時間作為初始值記錄在map中的value值,每次訪問或保存緩存都更新相應key對應的緩存文件的訪問時間,避免了頻繁地io操作。
#####3. 緩存策略對比與總結
lrucache是android中已經封裝好的類,disklrucache需要導入相應的包才可以使用。
可以在ui線程中直接使用lrucache;使用disklrucache時,由于緩存或者獲取都需要對本地文件進行操作,因此要在子線程中實現。
lrucache主要用于內存緩存,當app kill掉的時候,緩存也跟著沒了;而disklrucache主要用于存儲設備緩存,app kill掉的時候,緩存還在
lrucache的內部實現是linkedhashmap,對于元素的添加或獲取用put、get方法即可。而disklrucache是通過文件流的形式進行緩存,所以對于元素的添加或獲取通過輸入輸出流來實現。