一、Bitmap的高效加載
1、加載圖片
BitmapFactory類提供了四種方法:
-
decodeFile()
從文件系統加載出一個Bitmap對象,間接調用了decodeStream()方法 -
decodeResource()
從資源文件加載,間接調用了decodeStream()方法 -
decodeStream()
從輸入流加載 -
decodeByteArray()
從字節數組加載
2、高效加載的Bitmap
核心思想:采用BitmapFactory.Options來加載所需尺寸的圖片。
2.1、Options.inSampleSize(采樣率)
如果是inSampleSize=1那么和原圖大小一樣
如果是inSampleSize=2那么寬高都為原圖1/2, 而像素為原圖的1/4, 占用的內存大小也為原圖的1/4。
若取值小于1,則無效。建議取2的n次方。
現在有一張圖片像素為:1024*1024, 如果采用ARGB8888(四個顏色通道每個占有一個字節,相當于1點像素占用4個字節的空間)的格式來存儲,那么圖片的占有大小就是1024*1024*4那現在這張圖片在內存中占用4MB。
如果針對剛才的圖片進行inSampleSize=2, 那么最后占用內存大小為512*512*4, 也就是1MB
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中的緩存策略
當程序第一次從網絡上加載圖片后,將其緩存在存儲設備中,下次使用這張圖片的時候就不用再從網絡從獲取了。很多時候為了提高應用的用戶體驗,往往還會把圖片在內存中再緩存一份,因為從內存中加載圖片比存儲設備中快。一般情況會把圖片存一份到內存中,一份到存儲設備中,如果內存中沒找到就去存儲設備中找,還沒有找到就從網絡上下載。
緩存策略包含緩存的添加、獲取和刪除操作。不管是內存還是存儲設備,緩存大小都是有限制的。如何刪除舊的緩存并添加新的緩存,就對應緩存算法。
目前常用的一種緩存算法是LRU(Least Recently Used),最近最少使用算法。核心思想: 當緩存存滿時, 會優先淘汰那些近期最少使用的緩存對象。采用LRU算法的緩存有兩種: LruCache和DiskLruCache。LruCache用于實現內存緩存, DiskLruCache則充當了存儲設備緩存。
1、LruCache
LruCache是一個泛型類, 它內部采用了一個LinkedHashMap以強引用的方式存儲外界的緩存對象, 其提供了get和put方法來完成緩存的獲取和添加的操作。當緩存滿了時,LruCache會移除較早使用的緩存對象, 然后在添加新的緩存對象。LruCache是線程安全的。
強引用: 直接的對象引用
軟引用: 當一個對象只有軟引用存在時, 系統內存不足時此對象會被gc回收
弱引用: 當一個對象只有弱引用存在時, 對象會隨下一次gc時被回收
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 value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
只需要提供緩存的總容量大小(一般為進程可用內存的1/8)并重寫 sizeOf 方法即可。sizeOf
方法作用是計算緩存對象的大小。這里大小的單位需要和總容量的單位(這里是kb)一致,因此除以1024。一些特殊情況下,需要重寫LruCache的entryRemoved方法,LruCache移除舊緩存時會調用entryRemoved
方法,因此可以在entryRemoved中完成一些資源回收工作(如果需要的話)。
還有獲取和添加方法,都比較簡單:
get(K key) V
remove(K key) V
從Android 3.1開始,LruCache成為Android源碼的一部分。
2、DiskLruCache
DiskLruCache用于實現磁盤緩存,DiskLruCache得到了Android官方文檔推薦,但它不屬于Android SDK的一部分。
2.1、DiskLruCache的創建
DiskLruCache并不能通過構造方法來創建, 他提供了open()方法用于創建自身, 如下所示:
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
- File directory:表示磁盤緩存在文件系統中的存儲路徑。可以選擇SD卡上的緩存目錄, 具體是指/sdcard/Andriod/data/package_name/cache目錄,也可以選擇data目錄下. 或者其他地方。 這里給出的建議:如果應用卸載后就希望刪除緩存文件的話,那么就選擇SD卡上的緩存目錄, 如果希望保留緩存數據那就應該選擇SD卡上的其他目錄。
- appVersion: 表示應用的版本號,一般設為1即可。當版本號發生改變的時候DiskLruCache會清空之前所有的緩存文件, 在實際開發中這個實用性不大。
- valueCount: 一般設為1。
- maxSize: 表示緩存的總大小。當緩存大小超出這個設定值后,會清除一些緩存而保證總大小不大于這個設定值。
//初始化DiskLruCache,包括一些參數的設置
public void initDiskLruCache() {
//配置固定參數
// 緩存空間大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;
//下載圖片時的緩存大小
private static final long IO_BUFFER_SIZE = 1024 * 8;
// 緩存空間索引,用于Editor和Snapshot,設置成0表示Entry下面的第一個文件
private static final int DISK_CACHE_INDEX = 0;
//設置緩存目錄
File diskLruCache = getDiskCacheDir(mContext, "bitmap");
if (!diskLruCache.exists())
diskLruCache.mkdirs();
//創建DiskLruCache對象,當然是在空間足夠的情況下
if (getUsableSpace(diskLruCache) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskLruCache,
getAppVersion(mContext), 1, DISK_CACHE_SIZE);
mIsDiskLruCache = true;
} catch (IOException e) {
e.printStackTrace();
}
}
}
//上面的初始化過程總共用了3個方法
//設置緩存目錄
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);
}
// 獲取可用的存儲大小
@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();
}
//獲取應用版本號,注意不同的版本號會清空緩存
public int getAppVersion(Context context) {
try {
PackageInfo info = context.getPackageManager().getPackageInfo(
context.getPackageName(), 0);
return info.versionCode;
} catch (NameNotFoundException e) {
e.printStackTrace();
}
return 1;
2.2、DiskLruCache的緩存添加
DiskLruCache的緩存添加的操作是通過Editor完成的, Editor表示一個緩存對象的編輯對象.
如果還是緩存圖片為例子, 每一張圖片都通過圖片的url為key, 這里由于url可能會有特殊字符所以采用url的md5值作為key. 根據這個key就可以通過edit()來獲取Editor對象, 如果這個緩存對象正在被編輯, 那么edit()就會返回null. 即DiskLruCache不允許同時編輯一個緩存對象.
當用.edit(key)獲得了Editor對象之后. 通過editor.newOutputStream(0)就可以得到一個文件輸出流. 由于之前open()方法設置了一個節點只能有一個數據. 所以在獲得輸出流的時候傳入常量0即可.
有了文件輸出流, 可以當網絡下載圖片時, 圖片就可以通過這個文件輸出流寫入到文件系統上.最后,要通過Editor中commit()來提交寫操作, 如果下載中發生異常, 那么使用Editor中abort()來回退整個操作.
2.3、DiskLruCache的緩存查找
和緩存的添加過程類似, 緩存查找過程也需要將url轉換成key, 然后通過DiskLruCache#get()方法可以得到一個Snapshot對象, 接著在通過Snapshot對象即可得到緩存的文件輸入流, 有了文件輸入流, 自然就可以得到Bitmap對象. 為了避免加載圖片出現OOM所以采用壓縮的方式. 在前面對BitmapFactory.Options的使用說明了. 但是這中方法對FileInputStream的縮放存在問題. 原因是FileInputStream是一種有序的文件流, 而兩次decodeStream調用會影響文件的位置屬性, 這樣在第二次decodeStream的時候得到的會是null. 針對這一個問題, 可以通過文件流來得到它所對應的文件描述符, 然后通過BitmapFactory.decodeFileDescription()來加載一張縮放后的圖片.
/**
* 磁盤緩存的讀取
* @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;
}
3、ImageLoader的實現
一個好的ImageLoader應該具備以下幾點:
- 圖片的壓縮
- 網絡拉取
- 內存緩存
- 磁盤緩存
- 圖片的同步加載
- 圖片的異步加載
異步加載過程:
- bindBitmap先嘗試從內存緩存讀取圖片,如果沒有會在線程池中調用loadBitmap方法。獲取成功將圖片封裝為LoadResult對象通過mMainHandler向UI線程發送消息。選擇線程池和Handler來提供并發能力和異步能力。
- 為了解決View復用導致的列表錯位問題,在給ImageView設置圖片之前都會檢查它的url有沒有發生改變,如果改變就不再給它設置圖片。
三、ImageLoader的使用
1、照片墻效果
實現照片墻效果,如果圖片都需要是正方形;這樣做很快,自定義一個ImageView,重寫onMeasure方法。
@Override
protected void onMeasure(int widthMeasureSpec,int heightMeasureSpec){
super.onMeasure(widthMeasureSpec,widthMeasureSpec);//將原來的參數heightMeasureSpec換成widthMeasureSpec
}
2、優化列表的卡頓現象
- 不要在getView中執行耗時操作,不要在getView中直接加載圖片。
- 控制異步任務的執行頻率:如果用戶刻意頻繁上下滑動,getView方法會不停調用,從而產生大量的異步任務。可以考慮在列表滑動停止加載圖片;給ListView或者GridView設置 setOnScrollListener 并在 OnScrollListener 的 onScrollStateChanged 方法中判斷列表是否處于滑動狀態,如果是的話就停止加載圖片。
- 大部分情況下,可以使用硬件加速解決莫名卡頓問題,通過設置 android:hardwareAccelerated=”true” 即可為Activity開啟硬件加速。