1. 程式人生 > >安卓快取之DiskLruCache及設計(非同步+快取)圖片載入器DiskCacheImageLoader

安卓快取之DiskLruCache及設計(非同步+快取)圖片載入器DiskCacheImageLoader

DiskLruCache

DiskLruCache是一套硬碟快取的解決方案,演算法同LruCache基於LRU演算法。

  1. DiskLruCache不是由Google官方編寫的,這個類沒有被包含在Android API當中。這個類需要從網上下載

一、DiskLruCache的基本使用

1. 初始化DiskLruCache

呼叫它的open()靜態方法建立一個DiskLruCache的例項

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)

引數說明:

  • File directory:指定的是資料的快取地址,一般為應用的內部/外部快取資料夾,這樣當應用解除安裝時,裡面的資料也會被清除。

  • int appVersion:指定當前應用程式的版本號。

  • int valueCount :指定同一個key可以對應多少個快取檔案,一般都是傳1,這樣key和value一一對應,方便查詢。

  • long maxsize:指定最多可以快取資料的總位元組數。

  • 獲取DiskLruCache的快取目錄directory

    當SD卡存在或者SD卡不可被移除的時候,就呼叫getExternalCacheDir()方法來獲取手機內部快取路徑,否則就呼叫getCacheDir()方法來獲取手機外部快取路徑。前者獲取到的就是 /sdcard/Android/data//cache 這個路徑,後者獲取到的是 /data/data//cache 這個路徑。

    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();
      }
    
      cacheDir = new File(cachePath + File.separator + uniqueName);
    
      // 判斷快取資料夾是否存在,不存在則建立             
      if (!cacheDir.exists()) {
            cacheDir.mkdirs();
      }
    
      return cacheDir;
    }       
    
  • 獲取應用版本號appVersion

    每當版本號改變,快取路徑下儲存的所有資料都會被清除掉。

    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物件方式如下

    private DiskLruCache mDiskCache;
    
    //指定磁碟快取大小
    private long DISK_CACHE_SIZE = 1024 * 1024 * 10;//10MB
    //得到快取牡蠣
    File cacheDir = getDiskCacheDir(mContext, "Bitmap");
    
    mDiskCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1,DISK_CACHE_SIZE);
    

2. 將檔案寫入DiskLruCache快取

寫入操作是藉助DiskLruCache.Editor這個類完成,需要呼叫DiskLruCache的edit()方法來獲取例項。

public Editor edit(String key) throws IOException

edit()方法接收一個引數key,該key將會成為快取檔案的檔名,並且需要和檔案的URL是一 一對應。

直接使用URL來作為key不太合適,因為檔案的URL中可能包含一些特殊字元,這些字元有可能在命名檔案時是不合法的。
最簡單的做法就是將圖片的URL進行MD5編碼,編碼後的字串肯定是唯一的,並且只會包含0-F這樣的字元,完全符合檔案的命名規則。

  • 將檔案的URL字串進行MD5編碼:

    public String hashKeyForDisk(String key) {
      String cacheKey;
      try {
        final MessageDigest mDigest = MessageDigest.getInstance("MD5");
        mDigest.update(key.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
      } catch (NoSuchAlgorithmException e) {
        cacheKey = String.valueOf(key.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();
    }
    
  • 獲取DiskLruCache.Editor的例項

    String imageUrl = "http://.....jpg";
    // 轉化為對應的key
    String key = hashKeyForDisk(imageUrl);
    
    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    
  • 通過editor獲取輸出流並寫入資料

    if (editor != null) {
        OutputStream outputStream = editor.newOutputStream(0);
        if (downloadUrlToStream(url, outputStream)) {
            editor.commit(); // 提交編輯
        } else {
            editor.abort(); // 放棄編輯
        }
        mDiskCache.flush(); // 重新整理快取
    }
    
    
    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
    
      HttpURLConnection urlConnection = null;
      BufferedOutputStream out = null;
      BufferedInputStream in = null;
      try {
        final URL url = new URL(urlString);
        urlConnection = (HttpURLConnection) url.openConnection();
        in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
        out = new BufferedOutputStream(outputStream, 8 * 1024);
        int b;
        while ((b = in.read()) != -1) {
          out.write(b);
        }
        return true;
      } catch (final IOException e) {
        e.printStackTrace();
      } finally {
        if (urlConnection != null) {
          urlConnection.disconnect();
        }
        try {
          if (out != null) {
            out.close();
          }
          if (in != null) {
            in.close();
          }
        } catch (final IOException e) {
          e.printStackTrace();
        }
      }
      return false;
    }
    

    注:newOutputStream()方法接收一個index引數,由於前面在設定valueCount的時候指定的是1,這裡index傳0就可以了。

    在寫入操作執行完之後,我們還需要呼叫一下commit()方法進行提交才能使寫入生效,呼叫abort()方法的話則表示放棄此次寫入。

3. 獲取快取檔案:

  • 呼叫DiskLruCache的get方法來得到Snapshot物件

    get()方法要求傳入一個key來獲取到相應的快取資料,而這個key就是將檔案URL進行MD5編碼後的值

    public synchronized Snapshot get(String key) throws IOException
    
    
    
    String imageUrl = "http://.....jpg";
    // 轉化為key
    String key = hashKeyForDisk(imageUrl);
    
    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
    
  • 通過Snapshot得到檔案輸入流

    InputStream is = snapShot.getInputStream(0);
    

    getInputStream()方法也需要傳一個index引數,這裡傳入0。這是因為我們在open方法中引數設定為一個key對應一個檔案

  • 通過檔案輸入流得到檔案物件(以獲取bitmap為例)

    Bitmap bitmap = BitmapFactory.decodeStream(is);
    

4. 移除某個key的快取

藉助DiskLruCache的remove()方法移除快取

public synchronized boolean remove(String key) throws IOException

這個方法不經常被呼叫。因為DiskLruCache會根據我們在呼叫open()方法時設定的快取最大值來自動刪除多餘的快取。

5. 獲取快取的總位元組數和刪除全部快取

藉助DiskLruCache的size()方法獲取快取的總位元組數

public synchronized long size() 

這個方法會返回當前快取路徑下所有快取資料的總位元組數,以byte為單位。如果應用程式中需要在介面上顯示當前快取資料的總大小,就可以通過呼叫這個方法計算出。

public void delete() throws IOException

這個方法用於將所有的快取資料全部刪除,可以實現手動清理快取功能。

6. 重新整理快取狀態

 public synchronized void flush() throws IOException {
        checkNotClosed();
        trimToSize();
        journalWriter.flush();
    }

這個方法用於將記憶體中的操作記錄同步到日誌檔案(journal檔案)當中。這個方法非常重要,因為DiskLruCache能夠正常工作的前提就是要依賴於journal檔案中的內容。可在Activity的onPause()方法中去呼叫一次flush()方法。

二、使用DiskLruCache封裝圖片載入器

根據DiskLruCache,我們可以將它封裝在DiskCacheImageLoader來對圖片進行快取。設計模式為使用單例模式來設計:

在Logcat中列印非同步任務個數:

package com.cxmscb.cxm.cacheproject;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.os.StatFs;
import android.provider.ContactsContract;
import android.util.Log;
import android.widget.ImageView;
import android.widget.ListView;



import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashSet;
import java.util.Set;

import libcore.io.DiskLruCache;

/**
 * 利用DiskLruCache來快取圖片
 */
public class DiskCacheImageLoader {


    private Context mContext;

    private Set<DiskCacheAsyncTask> mTaskSet;

    //DiskLruCache
    private DiskLruCache mDiskCache;


    private static DiskCacheImageLoader mDiskCacheImageLoader;

    public static DiskCacheImageLoader getInstance(Context context){
        if(mDiskCacheImageLoader==null){
            synchronized (DiskCacheImageLoader.class){
                if(mDiskCacheImageLoader==null){
                    mDiskCacheImageLoader = new DiskCacheImageLoader(context);
                }
            }
        }
        return  mDiskCacheImageLoader;
    }


    private DiskCacheImageLoader(Context context) {

        mTaskSet = new HashSet<>();
        mContext = context.getApplicationContext();
        //得到快取檔案
        File diskCacheDir = getDiskCacheDir(mContext, "Bitmap");
        //如果檔案不存在 直接建立
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }

        try {
            mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1,
                            1024*1024*20);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }


    /**
     * 將一個URL轉換成bitmap物件
     *
     */
    public Bitmap getBitmapFromURL(String urlStr) {
        Bitmap bitmap;
        InputStream is = null;

        try {
            URL url = new URL(urlStr);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            is = new BufferedInputStream(connection.getInputStream(), 1024*8);
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 將URL中的圖片儲存到輸出流中
     *
     */
    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), 1024*8);
            out = new BufferedOutputStream(outputStream, 1024*8);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }



    /**
     * 普通地載入url
     *
     */
    public void loadImage(ImageView imageView,String url){
        //從快取中取出圖片
        Bitmap bitmap = null;
        try {
            bitmap = getBitmapFromDiskCache(url);
        } catch (IOException e) {
            e.printStackTrace();
        }
        //如果快取中沒有,則需要從網路中下載
        if (bitmap == null) {
            DiskCacheAsyncTask task = new DiskCacheAsyncTask(imageView);
            task.execute(url);
            mTaskSet.add(task);
        } else {
            //如果快取中有 直接設定
            imageView.setImageBitmap(bitmap);
        }
    }

    /**
     * 為listview載入從start到end的所有的Image
     *
     */
    public void loadTagedImagesInListView(int start, int end,String[] urls,ListView mListView) {
        for (int i = start; i < end; i++) {
            String url = urls[i];
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            loadImage(imageView,url);
        }
        Log.i("num of task"," "+mTaskSet.size());
    }



    /**
     * 建立快取檔案
     *
     */
    public File getDiskCacheDir(Context context, String filePath) {
        boolean externalStorageAvailable = Environment
                .getExternalStorageState().equals(Environment.MEDIA_MOUNTED);
        final String cachePath;
        if (externalStorageAvailable) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }

        return new File(cachePath + File.separator + filePath);
    }



    /**
     * 將URL轉換成key
     *
     */
    private String hashKeyFormUrl(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;
    }

    /**
     * 將Url的位元組陣列轉換成雜湊字串
     *
     */
    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();
    }

    /**
     * 將Bitmap寫入快取
     *
     */
    private Bitmap addBitmapToDiskCache(String url) throws IOException {


        if (mDiskCache == null) {
            return null;
        }

        //設定key,並根據URL儲存輸出流的返回值決定是否提交至快取
        String key = hashKeyFormUrl(url);
        //得到Editor物件
        DiskLruCache.Editor editor = mDiskCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(0);
            if (downloadUrlToStream(url, outputStream)) {
                //提交寫入操作
                editor.commit();
            } else {
                //撤銷寫入操作
                editor.abort();
            }
            mDiskCache.flush();
        }
        return getBitmapFromDiskCache(url);
    }


    /**
     * 從快取中取出Bitmap
     *
     */
    private Bitmap getBitmapFromDiskCache(String url) throws IOException {

        //如果快取中為空  直接返回為空
        if (mDiskCache == null) {
            return null;
        }

        //通過key值在快取中找到對應的Bitmap
        Bitmap bitmap = null;
        String key = hashKeyFormUrl(url);
        //通過key得到Snapshot物件
        DiskLruCache.Snapshot snapShot = mDiskCache.get(key);
        if (snapShot != null) {
            //得到檔案輸入流
            InputStream ins = snapShot.getInputStream(0);

            bitmap = BitmapFactory.decodeStream(ins);
        }
        return bitmap;
    }






    /**
     * 非同步任務類
     */
    private class DiskCacheAsyncTask extends AsyncTask<String, Void, Bitmap> {
        private ImageView imageView;

        public DiskCacheAsyncTask(ImageView imageView){
            this.imageView = imageView;
        }


        @Override
        protected Bitmap doInBackground(String... params) {

            Bitmap bitmap = getBitmapFromURL(params[0]);
            //儲存到快取中
            if (bitmap != null) {
                try {
                    //寫入快取
                    addBitmapToDiskCache(params[0]);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return bitmap;
        }

        @Override
        protected void onPostExecute(Bitmap bitmap) {
            super.onPostExecute(bitmap);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTaskSet.remove(this);
            Log.i("num of task"," "+mTaskSet.size());
        }
    }

    /**
     * 停止所有當前正在執行的任務
     */
    public void cancelAllTask() {
        if (mTaskSet != null) {
            for (DiskCacheAsyncTask task : mTaskSet) {
                task.cancel(false);
            }
            mTaskSet.clear();
            Log.i("num of task"," "+mTaskSet.size());
        }
    }


}

三、 使用LruCacheImageLoader來載入網路圖片:

一、專案效果圖:

開始ListView從網路上載入圖片,關閉網路退出應用後,再開啟應用,從本地DiskLruCache中載入圖片

這裡寫圖片描述

二、對ListView載入圖片的優化:

listView.setOnScrollListener(new AbsListView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(AbsListView absListView, int scrollState) {
                if(scrollState==SCROLL_STATE_IDLE){
                    mDiskImageLoader.loadTagedImagesInListView(mStart,mEnd,urls,listView);
                }else {
                    mDiskImageLoader.cancelAllTask();
                }
            }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
        mStart = firstVisibleItem;
        mEnd = firstVisbleItem + visibleItemCount;
        if(mFirstIn && visibleItemCount > 0){
            mDiskImageLoader.loadTagedImagesInListView(mStart,mEnd,urls,listView); 
            mFirstIn = false;
        }
    }
});

參考