第十二章-Bitmap的加載和Cache(緩存策略、LruCache、DiskLruCache)

本章的主題是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工具。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章