本章的主題是Bitmap的加載和Cache,主要包含三個方面的內容。首先講述如何有效的加載一個Bitmap,這是一個很有意義的話題,由於Bitmap的特殊性以及Android對單個應用所施加的內存限制,比如16MB,這就導致加載Bitmap的時候很容易的出現內存溢出。下面這個異常在開發中應該時常遇到:
java.lang.OutofMemoryError:bitmap size exceeds VM budget
因此如何高效的加載Bitmap是一個很重要也很容易被開發者忽視的問題。
一、Bitmap的高效加載
獲取採樣率的步驟
將上面的4個流程用程序來實現,就產生了下面的代碼:
public static Bitmap decodeSampleBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
//設置只請求大小不加載標記
options.inJustDecodeBounds = true;
//獲取到圖片的寬高
BitmapFactory.decodeResource(res, resId, options);
//計算採樣率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
//重置標記,加載圖片
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
//計算採樣率,邏輯就是採樣後的寬和高都大於請求的寬高,採樣率*2。直到滿足條件返回
public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while (halfHeight / inSampleSize >= reqHeight && (halfWidth / inSampleSize) >= reqWidth) {
inSampleSize *= 2;
}
}
Log.d(TAG,"採樣率 = " + inSampleSize);
return inSampleSize;
}
有了上面的兩個方法,實際使用的時候就很簡單,比如ImageView所期望的大小是100*100像素,這個時候就可以通過如下方式高效地加載並顯示圖片:
mImageView.setImageBitmap(decodeSampleBitmapFromResource(getResources(),R.drawable.ic_launcher,100,100));
除了BitmapFactory的decodeResource方法,其它三個decode系列的方法也是支持採樣加載的,並且處理方式也是類似的,但是decodeStream方法稍微有點特殊,這個會在後續內容詳細介紹。
二、Android中的緩存策略
1、LruCache
LruCache是android3.1所提供的一個緩存類,通過support-v4兼容包可以兼容到早期的Android版本。
LruCache是一個泛型類,它內部採用了一個LinkedHashMap以強引用的方式存儲外界的緩存對象,其提供了get和put的方法來完成緩存的獲取和添加操作,當緩存滿時,LruCache會移除較早使用的緩存對象,然後再添加新的緩存對象。
- 強引用:直接的對象引用。
- 軟引用:當一個對象只有軟引用存在時,系統內存不足時此對象會被gc回收。
- 弱引用:當一個對象只有弱引用存在時,此對象會隨時被gc回收。
另外LruCache是線程安全的,下面是LruCache的定義:
public class LruCache<K, V> {
private final LinkedHashMap<K, V> map;
...
}
LruCache的實現比較簡單,讀者可以參考它的源碼,這裏僅介紹如何使用LruCache來實現內存緩存。下面代碼展示了LruCache的典型的初始化過程:
int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);//獲取可用最大內存
int cacheSize = maxMemory / 8;
mMemoryLruCache = new LruCache<String,Bitmap>(cacheSize){
@Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
}
};
在上面的代碼中,只需要提供緩存的總容量大小並且重寫sizeOf方法即可。sizeOf方法的作用是計算緩存對象的大小,這裏大小的單位需要和總容量的單位一致。對於上面的示例代碼來說,總容量的大小是當前進程可用內存的1/8,單位是KB,而sizeOf方法則完成了Bitmap對象的大小計算。很明顯,之所以除以1024也是爲了將其單位轉換爲KB。一些特殊情況下,還需要重寫LruCache的entryRemoved方法,因此可以在這個方法中來處理資源回收的工作。
除了LruCache的創建以外,從LruCache中獲取一個緩存對象,如下所示。
mMemoryCache.get(key);
向LruCache中添加一個緩存對象,如下所示。
mMemoryCache.put(key,bitmap);
2、DiskLruCache
DiskLruCache用於實現存儲設備緩存,即磁盤緩存,它通過將緩存對象寫入文件系統從而實現緩存的效果。DiskLruCache不屬於SDK的一部分,我們可以從github上獲取到這個源碼。
https://github.com/JakeWharton/DiskLruCache
2.1、DiskLruCache的創建
DiskLruCache並不能通過構造方法來創建,它提供了open方法用於創建自身。
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
String cacheDirPath = getCacheDir().getAbsolutePath();
File diskCacheDir = new File(cacheDirPath + File.separator + "bitmap");//緩存在應用的cache目錄
//File diskCacheDir = new File("/sdcard" + File.separator + "bitmap");//緩存在sdcard目錄
createDiskLruCache(diskCacheDir);
DiskLruCache mDiskLruCache;
public void createDiskLruCache(File diskCacheDir){
long DISK_CACHE_SIZE = 1024 * 1024 *50;//50M
Log.d(TAG,"diskCacheDir = " + diskCacheDir);
if(!diskCacheDir.exists()){
diskCacheDir.mkdirs();
Log.d(TAG,"沒有此路徑,創建");
}
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir,1,1,DISK_CACHE_SIZE);
} catch (IOException e) {
e.printStackTrace();
}
}
2.2、DiskLruCache的緩存添加
DiskLruCache的緩存添加的操作是通過Editor完成的,Editor表示一個緩存對象的編輯對象。這裏仍然以圖片緩存舉例,首先需要獲取圖片url所對應的key,然後根據key就可以通過edit()來獲取Editor對象,如果一個緩存對象正在被編輯,那麼edit()會返回null,即DiskLruCache不允許同時編輯一個緩存對象。之所以把url轉換成key,是因爲圖片的url中很可能有特殊字符,這將影響url在Android中的直接使用,一般採用url的md5值作爲key,如下所示。
private String hashKeyFormUrl(String url){
String cacheKey;
try {
MessageDigest mDigest = MessageDigest.getInstance("MD5");
mDigest.update(url.getBytes());
cacheKey = bytesToHexString(mDigest.digest());
} catch (NoSuchAlgorithmException e) {
cacheKey = String.valueOf(url.hashCode());
}
return cacheKey;
}
private String bytesToHexString(byte[] digest) {
StringBuffer sb = new StringBuffer();
for (int i = 0; i < digest.length; i++) {
String hex = Integer.toHexString(0xFF&digest[i]);
if(hex.length() == 1){
sb.append('0');
}
sb.append(hex);
}
return sb.toString();
}
int DISK_CACHE_INDEX = 0;//這個數字會用作緩存文件
String key = hashKeyFormUrl(url);
Log.d(TAG,"url md5 key = " + key);//c9e8d581b4e4a6138f4105000cf5fa15
final DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if(editor != null){
final OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
}
有了文件輸出流,接下來要怎麼做呢?其實是這樣的,當從網絡下載圖片的時,圖片就可以通過這個文件輸出流寫入到文件系統上,這個過程的實現如下所示。
private boolean downloadUrlToStream(String urlString,OutputStream outputStream){
int IO_BUFFER_SIZE = 8*1024;
HttpURLConnection urlConnection = null;
BufferedOutputStream out = null;
BufferedInputStream in = null;
try {
URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(),IO_BUFFER_SIZE);
out = new BufferedOutputStream(outputStream,IO_BUFFER_SIZE);
int b ;
while ((b = in.read()) != -1){
out.write(b);
}
return true;
} catch (IOException e) {
Log.d(TAG,"下載網絡圖片失敗");
}finally {
if(urlConnection != null){
urlConnection.disconnect();
}
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
out.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
經過上面的步驟,其實並沒有真正的將圖片寫入文件系統,還必須通過Editor的commit()來提交寫入操作,如果圖片下載過程發生了異常,那麼還可以通過Editor的abort()來回退整個操作,這個過程如下所示。
boolean downloadResult = downloadUrlToStream(url, outputStream);
Log.d(TAG,"文件下載緩存的結果 = " + downloadResult);
try {
if(downloadResult && editor != null){
editor.commit();
Log.d(TAG,"下載成功,editor提交");
}else if(editor != null){
editor.abort();
Log.d(TAG,"下載失敗,editor回退");
}
mDiskLruCache.flush();
} catch (IOException e) {
e.printStackTrace();
}
經過上面的幾個步驟,圖片已經被正確地寫入到文件系統了,接下來圖片獲取的操作就不需要請求網絡了。
2.3、DiskLruCache的緩存查找
代碼如下(實際測試通過)
private Bitmap getBitmapFromCache(String url){
Log.d(TAG,"getBitmapFromCache");
int DISK_CACHE_INDEX = 0;
Bitmap bitmap = null;
String keys = hashKeyFormUrl(url);
try {
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keys);
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = decodeSampledBitmapFromFileDescriptor(fileDescriptor, 400, 300);
Log.d(TAG,"bitmap = " + bitmap);
if (bitmap != null) {
//addBitmapToMemoryCache(keys, bitmap);
mImageView.setImageBitmap(bitmap);
}
}
}catch (Exception e){
}
return null;
}
public static 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);
}
上面介紹了DiskLruCache的創建、緩存的添加和查找過程,讀者應該對DiskLruCache的使用方式有了一個大概的瞭解,關於DiskLruCache的內部實現這裏就不再介紹了,有興趣可以查看它的源碼實現。
上面就是緩存的知識,可以基於上面的學習編寫一個實用ImageLoader工具。