Bitmap之內存緩存和磁盤緩存詳解

原文首發於微信公衆號:jzman-blog,歡迎關注交流!

Android 中緩存的使用比較普遍,使用相應的緩存策略可以減少流量的消耗,也可以在一定程度上提高應用的性能,如加載網絡圖片的情況,不應該每次都從網絡上加載圖片,應該將其緩存到內存和磁盤中,下次直接從內存或磁盤中獲取,緩存策略一般使用 LRU(Least Recently Used) 算法,即最近最少使用算法,下面將從內存緩存和磁盤緩存兩個方面以圖片爲例 介紹 Android 中如何使用緩存,閱讀本文之前,請先閱讀上篇文章:

內存緩存

LruCache 是 Android 3.1 提供的一個緩存類,通過該類可以快速訪問緩存的 Bitmap 對象,內部採用一個 LinkedHashMap 以強引用的方式存儲需要緩存的 Bitmap 對象,當緩存超過指定的大小之前釋放最近很少使用的對象所佔用的內存。

注意:Android 3.1 之前,一個常用的內存緩存是一個 SoftReference 或 WeakReference 的位圖緩存,現在已經不推薦使用了。Android 3.1 之後,垃圾回收器更加註重回收 SoftWeakference/WeakReference,這使得使用該種方式實現緩存很大程度上無效,使用 support-v4 兼容包中的 LruCache 可以兼容 Android 3.1 之前的版本。

LruCache 的使用

  1. 初始化 LruCache

首先計算需要的緩存大小,具體如下:

//第一種方式:
ActivityManager manager = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
//獲取當前硬件條件下應用所佔的大致內存大小,單位爲M
int memorySize = manager.getMemoryClass();//M
int cacheSize = memorySize/ 8;
//第二種方式(比較常用)
int memorySize = (int) Runtime.getRuntime().maxMemory();//bytes
int cacheSize = memorySize / 8;

然後,初始化 LruCache ,具體如下:

//初始化 LruCache 且設置了緩存大小
LruCache<String, Bitmap> lruCache = new LruCache<String, Bitmap>(cacheSize){
    @Override
    protected int sizeOf(String key, Bitmap value) {
        //計算每一個緩存Bitmap的所佔內存的大小,內存單位應該和 cacheSize 的單位保持一致
        return value.getByteCount();
    }
};
  1. 添加 Bitmap 對象到 LruCache 緩存中
//參數put(String key,Bitmap bitmap)
lruCache.put(key,bitmap)
  1. 獲取緩存中的圖片並顯示
//參數get(String key)
Bitmap bitmap = lruCache.get(key);
imageView.setImageBitmap(bitmap);

下面使用 LruCache 加載一張網絡圖片來演示 LruCache 的簡單使用。

加載網絡圖片

創建一個簡單的 ImageLoader,裏面封裝獲取緩存 Bitmap 、添加 Bitmap 到緩存中以及從緩存中移出 Bitmap 的方法,具體如下:

//ImageLoader
public class ImageLoader {
    private LruCache<String , Bitmap> lruCache;
    public ImageLoader() {
        int memorySize = (int) Runtime.getRuntime().maxMemory() / 1024;

        int cacheSize = memorySize / 8;
        lruCache = new LruCache<String, Bitmap>(cacheSize){
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //計算每一個緩存Bitmap的所佔內存的大小
                return value.getByteCount()/1024;
            }
        };
    }

    /**
     * 添加Bitmapd到LruCache中
     * @param key
     * @param bitmap
     */
    public void addBitmapToLruCache(String key, Bitmap bitmap){
        if (getBitmapFromLruCache(key)==null){
            lruCache.put(key,bitmap);
        }
    }

    /**
     * 獲取緩存的Bitmap
     * @param key
     */
    public Bitmap getBitmapFromLruCache(String key){
        if (key!=null){
            return lruCache.get(key);
        }
        return null;
    }

    /**
     * 移出緩存
     * @param key
     */
    public void removeBitmapFromLruCache(String key){
        if (key!=null){
            lruCache.remove(key);
        }
    }
}

然後創建一個線程類用於加載圖片,具體如下:

//加載圖片的線程
public class LoadImageThread extends Thread {
    private Activity mActivity;
    private String mImageUrl;
    private ImageLoader mImageLoader;
    private ImageView mImageView;

    public LoadImageThread(Activity activity,ImageLoader imageLoader, ImageView imageView,String imageUrl) {
        this.mActivity = activity;
        this.mImageLoader = imageLoader;
        this.mImageView = imageView;
        this.mImageUrl = imageUrl;
    }

    @Override
    public void run() {
        HttpURLConnection connection = null;
        InputStream is = null;
        try {
            URL url = new URL(mImageUrl);
            connection = (HttpURLConnection) url.openConnection();
            is = connection.getInputStream();
            if (connection.getResponseCode() == HttpURLConnection.HTTP_OK){
                final Bitmap bitmap = BitmapFactory.decodeStream(is);
                mImageLoader.addBitmapToLruCache("bitmap",bitmap);
                mActivity.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        mImageView.setImageBitmap(bitmap);
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (connection!=null){
                connection.disconnect();
            }
            if (is!=null){
                try {
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

然後,在 MainActivity 中使用 ImageLoader 加載並緩存網絡圖片到內存中, 先從內存中獲取,如果緩存中沒有需要的 Bitmap ,則從網絡上獲取圖片並添加到緩存中,使用過程中一旦退出應用,系統將會釋放內存,關鍵方法如下:

//獲取圖片
private void loadImage(){
    Bitmap bitmap = imageLoader.getBitmapFromLruCache("bitmap");
   if (bitmap==null){
       Log.i(TAG,"從網絡獲取圖片");
       new LoadImageThread(this,imageLoader,imageView,url).start();
   }else{
       Log.i(TAG,"從緩存中獲取圖片");
       imageView.setImageBitmap(bitmap);
   }
}

// 移出緩存
private void removeBitmapFromL(String key){
    imageLoader.removeBitmapFromLruCache(key);
}

然後在相應的事件裏調用上述獲取圖片、移出緩存的方法,具體如下:

@Override
public void onClick(View v) {
    switch (v.getId()){
        case R.id.btnLoadLruCache:
            loadImage();
            break;
        case R.id.btnRemoveBitmapL:
            removeBitmapFromL("bitmap");
            break;
    }
}

下面來一張日誌截圖說明執行情況:

clipboard.png

磁盤緩存

磁盤緩存就是指將緩存對象寫入文件系統,使用磁盤緩存可有助於在內存緩存不可用時縮短加載時間,從磁盤緩存中獲取圖片相較從緩存中獲取較慢,如果可以應該在後臺線程中處理;磁盤緩存使用到一個 DiskLruCache 類來實現磁盤緩存,DiskLruCache 收到了 Google 官方的推薦使用,DiskLruCache 不屬於 Android SDK 中的一部分,首先貼一個 DiskLruCache 的源碼鏈接
DiskLruCache 源碼地址

DiskLruCache 的創建

DiskLruCache 的構造方法是私有的,故不能用來創建 DiskLruCache,它提供一個 open 方法用於創建自身,方法如下:

/**
 * 返回相應目錄中的緩存,如果不存在則創建
 * @param directory 緩存目錄
 * @param appVersion 表示應用的版本號,一般設爲1
 * @param valueCount 每個Key所對應的Value的數量,一般設爲1
 * @param maxSize 緩存大小
 * @throws IOException if reading or writing the cache directory fails
 */
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
        throws IOException {
    ...
    // 創建DiskLruCache
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
        ...
        return cache;
    }
    //如果緩存目錄不存在,創建緩存目錄以及DiskLruCache
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    ...
    return cache;
}

注意:緩存目錄可以選擇 SD 卡上的緩存目錄,及 /sdcard/Android/data/應用包名/cache 目錄,也可以選擇當前應用程序 data 下的緩存目錄,當然可以指定其他目錄,如果應用卸載後希望刪除緩存文件,就選擇 SD 卡上的緩存目錄,如果希望保留數據請選擇其他目錄,還有一點,如果是內存緩存,退出應用之後緩存將會被清除。

DiskLruCache 緩存的添加

DiskLruCache 緩存的添加是通過 Editor 完成的,Editor 表示一個緩存對象的編輯對象,可以通過其 edit(String key) 方法來獲取對應的 Editor 對象,如果 Editor 正在使用 edit(String key) 方法將會返回 null,即 DiskLruCache 不允許同時操作同一個緩存對象。當然緩存的添加都是通過唯一的 key 來進行添加操作的,那麼什麼作爲 key 比較方便嗎,以圖片爲例,一般講 url 的 MD5 值作爲 key ,計算方式如下:

//計算url的MD5值作爲key
private String hashKeyForDisk(String url) {
    String cacheKey;
    try {
        final 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[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(0xFF & bytes[i]);
        if (hex.length() == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}

通過 url 的 MD5 的值獲取到 key 之後,就可以通過 DiskLruCache 對象的 edit(String key) 方法獲取 Editor 對象,然後通過 Editor 對象的 commit 方法,大概意思就是釋放 Editir 對象,之後就可以通過 key 進行其他操作咯。

當然,獲取到 key 之後就可以向 DiskLruCache 中添加要緩存的東西咯,要加載一個網絡圖片到緩存中,顯然就是的通過下載的方式將要緩存的東西寫入文件系統中,那麼就需要一個輸出流往裏面寫東西,主要有兩種處理方式:

  1. 創建 OutputStream 寫入要緩存的數據,通過 DiskLruCache 的 edit(String key) 方法獲得 Editor 對象,然後通過 OutputStream 轉換爲 Birmap,將該 Bitmap 寫入由 Editor 對象創建的 OutputStream 中,最後調用 Editor 對象的 commit 方法提交;
  2. 先獲得 Editor 對象,根據 Editor 對象創建出 OutputStream 直接寫入要緩存的數據,最後調用 Editor 對象的 commit 方法提交;

這裏以第一種方式爲例,將根據 url 將網絡圖片添加到磁盤緩存中,同時也添加到內存緩存中,具體如下:

//添加網絡圖片到內存緩存和磁盤緩存
public void putCache(final String url, final CallBack callBack){
    Log.i(TAG,"putCache...");
    new AsyncTask<String,Void,Bitmap>(){
        @Override
        protected Bitmap doInBackground(String... params) {
            String key = hashKeyForDisk(params[0]);
            DiskLruCache.Editor editor = null;
            Bitmap bitmap = null;
            try {
                URL url = new URL(params[0]);
                HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                conn.setReadTimeout(1000 * 30);
                conn.setConnectTimeout(1000 * 30);
                ByteArrayOutputStream baos = null;
                if(conn.getResponseCode()==HttpURLConnection.HTTP_OK){
                    BufferedInputStream bis = new BufferedInputStream(conn.getInputStream());
                    baos = new ByteArrayOutputStream();
                    byte[] bytes = new byte[1024];
                    int len = -1;
                    while((len=bis.read(bytes))!=-1){
                        baos.write(bytes,0,len);
                    }
                    bis.close();
                    baos.close();
                    conn.disconnect();
                }
                if (baos!=null){
                    bitmap = decodeSampledBitmapFromStream(baos.toByteArray(),300,200);
                    addBitmapToCache(params[0],bitmap);//添加到內存緩存
                    editor = diskLruCache.edit(key);
                    //關鍵
                    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, editor.newOutputStream(0));
                    editor.commit();//提交
                }
            } catch (IOException e) {
                try {
                    editor.abort();//放棄寫入
                } catch (IOException e1) {
                    e1.printStackTrace();
                }
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            callBack.response(bitmap);
        }
    }.execute(url);
}

DiskLruCache 緩存的獲取

在 DiskLruCache 緩存的添加中瞭解瞭如何獲取 key,獲取到 key 之後,通過 DiskLruCache 對象的 get 方法獲得 Snapshot 對象,然後根據 Snapshot 對象獲得 InputStream,最後通過 InputStream 就可以獲得 Bitmap ,當然可以利用 上篇文章 中的對 Bitmap 採樣的方式進行適當的調整,也可以在緩存之前先壓縮再緩存,獲取 InputStream 的方法具體如下:

//獲取磁盤緩存
public InputStream getDiskCache(String url) {
    Log.i(TAG,"getDiskCache...");
    String key = hashKeyForDisk(url);
    try {
        DiskLruCache.Snapshot snapshot = diskLruCache.get(key);
        if (snapshot!=null){
            return snapshot.getInputStream(0);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
    return null;
}

DiskLruCache 的主要部分大致如上,下面實現一個簡單的三級緩存來說明 LruCache 和 DiskLruCache 的具體使用,MainActivity 代碼如下:

//MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "cache_test";
    public static String CACHE_DIR = "diskCache";  //緩存目錄
    public static int CACHE_SIZE = 1024 * 1024 * 10; //緩存大小
    private ImageView imageView;
    private LruCache<String, String> lruCache;
    private LruCacheUtils cacheUtils;
    private String url = "http://img06.tooopen.com/images/20161012/tooopen_sy_181713275376.jpg";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        imageView = (ImageView) findViewById(R.id.imageView);
    }
    
    @Override
    protected void onResume() {
        super.onResume();
        cacheUtils = LruCacheUtils.getInstance();
        //創建內存緩存和磁盤緩存
        cacheUtils.createCache(this,CACHE_DIR,CACHE_SIZE);
    }
    
    @Override
    protected void onPause() {
        super.onPause();
        cacheUtils.flush();
    }

    @Override
    protected void onStop() {
        super.onStop();
        cacheUtils.close();
    }
    
    public void loadImage(View view){
        load(url,imageView);
    }
    
    public void removeLruCache(View view){
        Log.i(TAG, "移出內存緩存...");
        cacheUtils.removeLruCache(url);
    }
    
    public void removeDiskLruCache(View view){
        Log.i(TAG, "移出磁盤緩存...");
        cacheUtils.removeDiskLruCache(url);
    }
    
    private void load(String url, final ImageView imageView){
        //從內存中獲取圖片
        Bitmap bitmap = cacheUtils.getBitmapFromCache(url);
        if (bitmap == null){
            //從磁盤中獲取圖片
            InputStream is = cacheUtils.getDiskCache(url);
            if (is == null){
                //從網絡上獲取圖片
                cacheUtils.putCache(url, new LruCacheUtils.CallBack<Bitmap>() {
                    @Override
                    public void response(Bitmap bitmap1) {
                        Log.i(TAG, "從網絡中獲取圖片...");
                        Log.i(TAG, "正在從網絡中下載圖片...");
                        imageView.setImageBitmap(bitmap1);
                        Log.i(TAG, "從網絡中獲取圖片成功...");
                    }
                });
            }else{
                Log.i(TAG, "從磁盤中獲取圖片...");
                bitmap = BitmapFactory.decodeStream(is);
                imageView.setImageBitmap(bitmap);
            }
        }else{
            Log.i(TAG, "從內存中獲取圖片...");
            imageView.setImageBitmap(bitmap);
        }
    }
}

佈局文件比較簡單就不貼代碼了,下面是日誌運行截圖說明執行情況,如下圖所示:

clipboard.png

這篇文章記錄了 LruCache 和 DiskLruCache 的基本使用方式,至少應該對這兩個緩存輔助類有了一定的瞭解,它的具體實現請參考源碼。
【文中代碼】:傳送門

可以關注公衆號:jzman-blog,一起交流學習。

clipboard.png

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