導(dǎo)語(yǔ)
主要介紹如何高效地加載一個(gè)Bitmap,Android中常用的緩存策略枪萄,如何優(yōu)化列表的卡頓。
主要內(nèi)容
- Bitmap的高效加載
- Android中的緩存策略
- ImageLoader的使用
具體內(nèi)容
Bitmap的高效加載
先來(lái)簡(jiǎn)單介紹一下如何加載一個(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的核心思想:采用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中的緩存策略
當(dāng)程序第一次從網(wǎng)絡(luò)上加載圖片后典徘,將其緩存在存儲(chǔ)設(shè)備中蟀苛,下次使用這張圖片的時(shí)候就不用再?gòu)木W(wǎng)絡(luò)從獲取了慧妄。很多時(shí)候?yàn)榱颂岣邞?yīng)用的用戶體驗(yàn)初澎,往往還會(huì)把圖片在內(nèi)存中再緩存一份,因?yàn)閺膬?nèi)存中加載圖片比存儲(chǔ)設(shè)備中快序仙。一般情況會(huì)把圖片存一份到內(nèi)存中梅鹦,一份到存儲(chǔ)設(shè)備中裆甩,如果內(nèi)存中沒(méi)找到就去存儲(chǔ)設(shè)備中找,還沒(méi)有找到就從網(wǎng)絡(luò)上下載齐唆。
緩存策略包含緩存的添加嗤栓、獲取和刪除操作。不管是內(nèi)存還是存儲(chǔ)設(shè)備箍邮,緩存大小都是有限制的茉帅。如何刪除舊的緩存并添加新的緩存,就對(duì)應(yīng)緩存算法锭弊。
目前常用的一種緩存算法是LRU(Least Recently Used), 最近最少使用算法. 核心思想: 當(dāng)緩存存滿時(shí), 會(huì)優(yōu)先淘汰那些近期最少使用的緩存對(duì)象. 采用LRU算法的緩存有兩種: LruCache和DiskLruCache,LruCahe用于實(shí)現(xiàn)內(nèi)存緩存, DiskLruCache則充當(dāng)了存儲(chǔ)設(shè)備緩存, 當(dāng)組合使用后就可以實(shí)現(xiàn)一個(gè)類(lèi)似ImageLoader這樣的類(lèi)庫(kù).
LruCache
LruCache是Android 3.1所提供的一個(gè)緩存類(lèi), 通過(guò)support-v4兼容包可以兼容到早期的Android版本
LruCache是一個(gè)泛型類(lèi), 它內(nèi)部采用了一個(gè)LinkedHashMap以強(qiáng)引用的方式存儲(chǔ)外界的緩存對(duì)象, 其提供了get和put方法來(lái)完成緩存的獲取和添加的操作. 當(dāng)緩存滿了時(shí), LruCache會(huì)移除較早使用的緩存對(duì)象, 然后在添加新的緩存對(duì)象. 普及一下各種引用的區(qū)別:
- 強(qiáng)引用: 直接的對(duì)象引用
- 軟引用: 當(dāng)一個(gè)對(duì)象只有軟引用存在時(shí), 系統(tǒng)內(nèi)存不足時(shí)此對(duì)象會(huì)被gc回收
- 弱引用: 當(dāng)一個(gè)對(duì)象只有弱引用存在時(shí), 對(duì)象會(huì)隨下一次gc時(shí)被回收
LruCache是線程安全的堪澎。
LruCache 典型初始化過(guò)程:
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
這里只需要提供緩存的總?cè)萘看笮?一般為進(jìn)程可用內(nèi)存的1/8)并重寫(xiě) sizeOf 方法即可.sizeOf方法作用是計(jì)算緩存對(duì)象的大小。這里大小的單位需要和總?cè)萘康膯挝唬ㄟ@里是kb)一致味滞,因此除以1024樱蛤。一些特殊情況下,需要重寫(xiě)LruCache的entryRemoved方法剑鞍,LruCache移除舊緩存時(shí)會(huì)調(diào)用entryRemoved方法昨凡,因此可以在entryRemoved中完成一些資源回收工作(如果需要的話)。
還有獲取和添加方法攒暇,都比較簡(jiǎn)單:
mMemoryCache.get(key)
mMemoryCache.put(key,bitmap)
通過(guò)remove方法可以刪除一個(gè)指定的對(duì)象土匀。
從Android 3.1開(kāi)始,LruCache稱(chēng)為Android源碼的一部分形用。
DiskLruCache
DiskLruCache用于實(shí)現(xiàn)磁盤(pán)緩存就轧,DiskLruCache得到了Android官方文檔推薦证杭,但它不屬于Android SDK的一部分,源碼在這里妒御。
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)
- File directory: 表示磁盤(pán)緩存在文件系統(tǒng)中的存儲(chǔ)路徑. 可以選擇SD卡上的緩存目錄, 具體是指/sdcard/Andriod/data/package_name/cache目錄, package_name表示當(dāng)前應(yīng)用的包名, 當(dāng)應(yīng)用被卸載后, 此目錄會(huì)一并刪除掉. 也可以選擇data目錄下. 或者其他地方. 這里給出的建議:如果應(yīng)用卸載后就希望刪除緩存文件的話 , 那么就選擇SD卡上的緩存目錄, 如果希望保留緩存數(shù)據(jù)那就應(yīng)該選擇SD卡上的其他目錄.
- int appVersion: 表示應(yīng)用的版本號(hào), 一般設(shè)為1即可. 當(dāng)版本號(hào)發(fā)生改變的時(shí)候DiskLruCache會(huì)清空之前所有的緩存文件, 在實(shí)際開(kāi)發(fā)中這個(gè)實(shí)用性不大.
- int valueCount: 表示單個(gè)節(jié)點(diǎn)所對(duì)應(yīng)的數(shù)據(jù)的個(gè)數(shù), 一般設(shè)為1.
- long maxSize: 表示緩存的總大小, 比如50MB, 當(dāng)緩存大小超出這個(gè)設(shè)定值后, DiskLruCache會(huì)清除一些緩存而保證總大小不大于這個(gè)設(shè)定值.
//初始化DiskLruCache解愤,包括一些參數(shù)的設(shè)置
public void initDiskLruCache
{
//配置固定參數(shù)
// 緩存空間大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
//下載圖片時(shí)的緩存大小
private static final long IO_BUFFER_SIZE = 1024 * 8;
// 緩存空間索引,用于Editor和Snapshot乎莉,設(shè)置成0表示Entry下面的第一個(gè)文件
private static final int DISK_CACHE_INDEX = 0;
//設(shè)置緩存目錄
File diskLruCache = getDiskCacheDir(mContext, "bitmap");
if(!diskLruCache.exists())
diskLruCache.mkdirs();
//創(chuàng)建DiskLruCache對(duì)象送讲,當(dāng)然是在空間足夠的情況下
if(getUsableSpace(diskLruCache) > DISK_CACHE_SIZE)
{
try
{
mDiskLruCache = DiskLruCache.open(diskLruCache,
getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskLruCache = true;
}catch(IOException e)
{
e.printStackTrace();
}
}
}
//上面的初始化過(guò)程總共用了3個(gè)方法
//設(shè)置緩存目錄
public File getDiskCacheDir(Context context, String uniqueName) {
String cachePath;
if (Environment.MEDIA_MOUNTED.equals(Environment
.getExternalStorageState())
|| !Environment.isExternalStorageRemovable()) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
// 獲取可用的存儲(chǔ)大小
@TargetApi(VERSION_CODES.GINGERBREAD)
private long getUsableSpace(File path) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.GINGERBREAD)
return path.getUsableSpace();
final StatFs stats = new StatFs(path.getPath());
return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
}
//獲取應(yīng)用版本號(hào),注意不同的版本號(hào)會(huì)清空緩存
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
}
DiskLruCache的緩存添加:
DiskLruCache的緩存添加的操作是通過(guò)Editor完成的, Editor表示一個(gè)緩存對(duì)象的編輯對(duì)象.
如果還是緩存圖片為例子, 每一張圖片都通過(guò)圖片的url為key, 這里由于url可能會(huì)有特殊字符所以采用url的md5值作為key. 根據(jù)這個(gè)key就可以通過(guò)edit()來(lái)獲取Editor對(duì)象, 如果這個(gè)緩存對(duì)象正在被編輯, 那么edit()就會(huì)返回null. 即DiskLruCache不允許同時(shí)編輯一個(gè)緩存對(duì)象.
當(dāng)用.edit(key)獲得了Editor對(duì)象之后. 通過(guò)editor.newOutputStream(0)就可以得到一個(gè)文件輸出流. 由于之前open()方法設(shè)置了一個(gè)節(jié)點(diǎn)只能有一個(gè)數(shù)據(jù). 所以在獲得輸出流的時(shí)候傳入常量0即可.
有了文件輸出流, 可以當(dāng)網(wǎng)絡(luò)下載圖片時(shí), 圖片就可以通過(guò)這個(gè)文件輸出流寫(xiě)入到文件系統(tǒng)上.最后惋啃,要通過(guò)Editor中commit()來(lái)提交寫(xiě)操作, 如果下載中發(fā)生異常, 那么使用Editor中abort()來(lái)回退整個(gè)操作.
DiskLruCache的緩存查找:
和緩存的添加過(guò)程類(lèi)似, 緩存查找過(guò)程也需要將url轉(zhuǎn)換成key, 然后通過(guò)DiskLruCache#get()方法可以得到一個(gè)Snapshot對(duì)象, 接著在通過(guò)Snapshot對(duì)象即可得到緩存的文件輸入流, 有了文件輸入流, 自然就可以得到Bitmap對(duì)象. 為了避免加載圖片出現(xiàn)OOM所以采用壓縮的方式. 在前面對(duì)BitmapFactory.Options的使用說(shuō)明了. 但是這中方法對(duì)FileInputStream的縮放存在問(wèn)題. 原因是FileInputStream是一種有序的文件流, 而兩次decodeStream調(diào)用會(huì)影響文件的位置屬性, 這樣在第二次decodeStream的時(shí)候得到的會(huì)是null. 針對(duì)這一個(gè)問(wèn)題, 可以通過(guò)文件流來(lái)得到它所對(duì)應(yīng)的文件描述符, 然后通過(guò)BitmapFactory.decodeFileDescription()來(lái)加載一張縮放后的圖片.
/**
* 磁盤(pán)緩存的讀取
* @param url
* @param reqWidth
* @param reqHeight
* @return
*/
private Bitmap loadBitmapFromDiskCache(String url, int reqWidth, int reqHeight) throws IOException
{
if(Looper.myLooper() == Looper.getMainLooper())
Log.w(TAG, "it's not recommented load bitmap from UI Thread");
if(mDiskLruCache == null)
return null;
Bitmap bitmap = null;
String key = hashKeyForDisk(url);
Snapshot snapshot = mDiskLruCache.get(key);
if(snapshot != null)
{
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fd = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampleBitmapFromFileDescriptor(fd, reqWidth, reqHeight);
if(bitmap != null)
addBitmapToMemoryCache(key, bitmap);
}
return bitmap;
}
ImageLoader的實(shí)現(xiàn)
一個(gè)好的ImageLoader應(yīng)該具備以下幾點(diǎn):
- 圖片的壓縮
- 網(wǎng)絡(luò)拉取
- 內(nèi)存緩存
- 磁盤(pán)緩存
- 圖片的同步加載
- 圖片的異步加載
圖片壓縮功能
ImageResizer
內(nèi)存緩存和磁盤(pán)緩存
ImageLoader
同步加載和異步加載的接口設(shè)計(jì)
ImageLoader 173行
異步加載過(guò)程:
- bindBitmap先嘗試從內(nèi)存緩存讀取圖片哼鬓,如果沒(méi)有會(huì)在線程池中調(diào)用loadBitmap方法。獲取成功將圖片封裝為L(zhǎng)oadResult對(duì)象通過(guò)mMainHandler向UI線程發(fā)送消息边灭。選擇線程池和Handler來(lái)提供并發(fā)能力和異步能力异希。
- 為了解決View復(fù)用導(dǎo)致的列表錯(cuò)位問(wèn)題,在給ImageView設(shè)置圖片之前都會(huì)檢查它的url有沒(méi)有發(fā)生改變绒瘦,如果改變就不再給它設(shè)置圖片称簿。(76行)
ImageLoader的使用
照片墻效果
實(shí)現(xiàn)照片墻效果,如果圖片都需要是正方形惰帽;這樣做很快憨降,自定義一個(gè)ImageView,重寫(xiě)onMeasure方法该酗。
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,widthMeasureSpec);//將原來(lái)的參數(shù)heightMeasureSpec換成widthMeasureSpec
}
優(yōu)化列表的卡頓現(xiàn)象
- 不要在getView中執(zhí)行耗時(shí)操作授药,不要在getView中直接加載圖片。
- 控制異步任務(wù)的執(zhí)行頻率:如果用戶刻意頻繁上下滑動(dòng)垂涯,getView方法會(huì)不停調(diào)用烁焙,從而產(chǎn)生大量的異步任務(wù)航邢「福可以考慮在列表滑動(dòng)停止加載圖片;給ListView或者GridView設(shè)置 setOnScrollListener 并在 OnScrollListener 的 onScrollStateChanged 方法中判斷列表是否處于滑動(dòng)狀態(tài)膳殷,如果是的話就停止加載圖片操骡。
- 大部分情況下,可以使用硬件加速解決莫名卡頓問(wèn)題赚窃,通過(guò)設(shè)置 android:hardwareAccelerated=”true” 即可為Activity開(kāi)啟硬件加速册招。