Android:跟着實戰項目學緩存策略之DiskLruCache詳談

##寫在前面java

以前花費大心思更新了一篇《Android:跟着實現項目學緩存策略之LruCache詳談》,原本是準備用項目實戰的方式分享一下緩存策略的使用。可是因爲篇幅過長,DiskLruCache也比較複雜,因此決定把DiskLruCache抽取出來單獨講。本文仍然是在上一篇文章中新聞小項目基礎上來講明DiskLurCache的用法,以及與LruCache的不一樣。文章的目錄以下:git

  • 寫在前面
  • 遺留問題
  • DiskLruCache詳解
    • 基本介紹
    • 實戰運用
  • 緩存策略對比與總結
  • 結語
  • 項目源碼

##遺留問題github

上一篇文章中已經將圖片成功的緩存到內存中,當全部圖片緩存完成後,再次滑動就已經不須要從新加載圖片了。可是注意看下面這張圖的現象:算法

存回收,緩存隨之回收

能夠看到,成功緩存後確實在應用內再次滑動就不須要加載了,可是若是此時咱們kill掉APP,從新打開的話,仍然是須要加載的。這是爲何呢?數組

答案很顯然,由於LruCache是將文件類型緩存到內存中,隨着APP中Activity的銷燬,內存也會隨之回收。也就將內存中的緩存回收掉,再次打開APP的時候,內存中找不到緩存,固然須要從新加載了。緩存

因此如何才能緩存到存儲設備中呢?下面就來詳細說說。安全

##DiskLruCache詳解bash

###基本介紹網絡

DiskLruCache與LruCache不一樣,它不是Android中已經封裝好的類,因此 想要使用的話須要從網上下載。關於下載這個類,我也是費了很多功夫,你們若是想嘗試的話,能夠直接Copy我這個項目中的 com.libcore.io 包下的全部文件便可,這個就很少說了。下面這是它的一個基本定義,也是開發藝術探索中任老師說的:app

DiskLruCache用於實現存儲設備緩存,即磁盤緩存,它經過將緩存對象寫入文件系統從而實現緩存的效果。

注意,重點是將緩存對象寫入文件系統,你們可能不太理解,不過不用擔憂,後面會說到。先來它的建立、添加、獲取方法。

####一、建立

與LruCache不一樣的是,它不能經過構造方法的方式來建立,它的建立方法是經過DiskLruCache類的一個靜態方法 open 來建立。具體以下:

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

其中有四個參數,很好理解:

  • File directory:這是緩存文件在磁盤中的存儲路徑,這是必需要指定的,通常來講是選擇SD卡上的緩存目錄,APP卸載後自動刪除緩存。
  • int appVersion:這個是版本號,用處不大,正常設置爲1便可。
  • int valueCount:這個是單個節點所對應的數據個數,其實就是一個key對應多少個value,正常設置爲1便可,這樣key和value一一對應,方便查找。
  • long maxSize:這個就是緩存的總大小,很好理解。

這樣看來,建立一個DiskLruCache就至少要指定文件的目錄與緩存大小。因此建立方式以下:

//DiskLruCache
private DiskLruCache mDiskCache;
//指定磁盤緩存大小
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB
//獲得緩存文件
File diskCacheDir = getDiskCacheDir(mContext, "diskcache");
//若是文件不存在 直接建立
if (!diskCacheDir.exists()) {
	diskCacheDir.mkdirs();
}
mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1,DISK_CACHE_SIZE);
複製代碼
/**
 * 建立緩存文件
 *
 * @param context  上下文對象
 * @param filePath 文件路徑
 * @return 返回一個文件
 */
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);
}
複製代碼

注意,下面的方法是一個工具方法,用來返回一個文件,難度不大。這樣就建立了一個DiskLruCache。

####二、設置key

通常來講,須要用到緩存的地方都是須要聯網下載的,因此這個key最好的就是須要下載的文件的Url。可是Url中可能有一些特殊字符,因此最好的方式就是將其轉換成MD5值。

MD5是計算機安全領域普遍使用的一種散列函數,用以提供消息的完整性保護。

說簡單點,就是一種加密算法,將一串信息轉成定長的一串字符。這裏只是防止Url中的特殊字符影響正常使用。下面給出如何轉成MD5,這是《Android開發藝術探索》中的源碼,能夠當成工具方法,直接用便可。

/**
 * 將URL轉換成key
 *
 * @param url 圖片的URL
 * @return
 */
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的字節數組轉換成哈希字符串
 *
 * @param bytes URL的字節數組
 * @return
 */
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();
}
複製代碼

####三、添加

與LruCache不一樣的是,LruCache內部實現是Map,添加直接用put便可;而DiskLruCache是將文件存儲到文件中,因此須要經過文件輸出流的形式將文件寫入到文件系統中。可是僅僅寫入是不夠的,必須經過Editor對象來提交。它是緩存對象的編輯對象。它是根據文件的Url對應的key的 edit() 方法獲取。

值得注意的是,若是返回的Editor對象正在被編輯,那麼返回的結果不爲null。反之若是返回null,表示編輯對象可用。因此咱們在使用前必須判斷一下返回的Editor對象是否爲空。若是不爲空的話,那就經過Editor對象的 commi 方法來提交寫入操做,固然你也能夠經過 abort 方法來撤銷寫入操做。

說了這麼多,概括來講DiskLruCache的添加操做分爲三步:

  • 經過文件的Url將文件寫入文件系統
  • 經過Url對應的key來獲得一個不爲空的Editor對象
  • 經過這個Editor對象來對寫入操做進行提交或者撤銷操做

好了,如今來看具體的實現代碼,代碼邏輯應該很清晰:

/**
 * 將URL中的圖片保存到輸出流中
 *
 * @param urlString    圖片的URL地址
 * @param outputStream 輸出流
 * @return 輸出流
 */
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(), IO_BUFFER_SIZE);
        out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
        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;
}
複製代碼
/**
 * 將Bitmap寫入緩存
 *
 * @param url
 * @return
 * @throws IOException
 */
private Bitmap addBitmapToDiskCache(String url) throws IOException {
    //若是當前線程是在主線程 則異常
    if (Looper.myLooper() == Looper.getMainLooper()) {
        throw new RuntimeException("can not visit network from UI Thread.");
    }
    if (mDiskCache == null) {
        return null;
    }

    //設置key,並根據URL保存輸出流的返回值決定是否提交至緩存
    String key = hashKeyFormUrl(url);
    DiskLruCache.Editor editor = mDiskCache.edit(key);
    if (editor != null) {
        OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
        if (downloadUrlToStream(url, outputStream)) {
            editor.commit();
        } else {
            editor.abort();
        }
        mDiskCache.flush();
    }
    return getBitmapFromDiskCache(url);
}
複製代碼

####四、獲取

相比較於添加操做,獲取操做很簡單。固然仍是經過key來獲取。有了key,能夠經過DiskLruCache的get方法獲取到一個 Snapshot 對象,再經過這個對象的 getInputStream 方法獲得文件的輸入流,獲得了輸出流固然能夠獲取流中的文件了。

因此歸納起來,獲取緩存中文件的步驟也有三個:

  • 經過key來獲得一個Snapshot對象
  • 經過Snapshot獲得一個文件輸入流
  • 經過文件輸入流獲得文件對象

具體的代碼實現以下:

/**
 * 從緩存中取出Bitmap
 *
 * @param url 圖片的URL
 * @return 返回Bitmap對象
 * @throws IOException
 */
private Bitmap getBitmapFromDiskCache(String url) throws IOException {
    //若是當前線程是主線程 則異常
    if (Looper.myLooper() == Looper.getMainLooper()) {
        Log.w(TAG, "load bitmap from UI Thread, it's not recommended!");
    }
    //若是緩存中爲空  直接返回爲空
    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) {
        //獲得文件輸入流
        FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
        FileDescriptor fileDescriptor = fileInputStream.getFD();
        bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
    }
    return bitmap;
}
複製代碼

####五、補充

若是你們仔細看了上面的代碼會發現不論是緩存的添加仍是獲取方法中,都有下面這段代碼:

//若是當前線程是主線程 則異常
    if (Looper.myLooper() == Looper.getMainLooper()) {
        Log.w("DiskLruCache", "load bitmap from UI Thread, it's not recommended!");
    }	
複製代碼

這是由於這兩個方法都不能在主線程中調用,因此須要檢查一下,若是不是主線程的話,直接拋出異常。這也算是一個細節吧。

###實戰運用

好了,經過上面的分塊講解,你們應該對DiskLruCache有了基本的認識了。如今咱們就對上一個項目添加這樣的緩存策略。一樣的,爲了方便你們對比查看,我仍然把這些方法封裝到DiskCacheUtil類。

給出代碼以前,咱們也大體梳理一下思路:

  • 首先要初始化DiskLruCache,這個毋庸置疑
  • 其次就須要提供DiskLruCache的添加、獲取方法。
  • 而這個添加獲取方法須要用到key值,因此要將Url轉成MD5值。
  • 剩下的就是經過AsyncTask來展現圖片了,並在展現過程當中添加到緩存中。
  • 固然不要忘了,前一篇所說的ListView滑動中止加載,靜止才能加載的優化。

下面直接給出代碼,代碼比較長,可是冷靜下來,按照前面說的邏輯來看是否是很清晰呢?

/**
 * 利用DiskLruCache來緩存圖片
 */
public class DiskCacheUtil {
    private Context mContext;

    private ListView mListView;
    private Set<NewsAsyncTask> mTaskSet;

    //定義DiskLruCache
    private DiskLruCache mDiskCache;
    //指定磁盤緩存大小
    private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB
    //IO緩存流大小
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    //緩存個數
    private static final int DISK_CACHE_INDEX = 0;
    //緩存文件是否建立
    private boolean mIsDiskLruCacheCreated = false;

    public DiskCacheUtil(Context context, ListView listView) {
        this.mListView = listView;
        mTaskSet = new HashSet<>();
        mContext = context.getApplicationContext();
        //獲得緩存文件
        File diskCacheDir = getDiskCacheDir(mContext, "diskcache");
        //若是文件不存在 直接建立
        if (!diskCacheDir.exists()) {
            diskCacheDir.mkdirs();
        }
        if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
            try {
                mDiskCache = DiskLruCache.open(diskCacheDir, 1, 1,
                        DISK_CACHE_SIZE);
                mIsDiskLruCacheCreated = true;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 經過異步任務的方式加載數據
     *
     * @param iv  圖片的控件
     * @param url 圖片的URL
     */
    public void showImageByAsyncTask(ImageView iv, final String url) throws IOException {
        //從緩存中取出圖片
        Bitmap bitmap = getBitmapFromDiskCache(url);
        //若是緩存中沒有,則須要從網絡中下載
        if (bitmap == null) {
            iv.setImageResource(R.mipmap.ic_launcher);
        } else {
            //若是緩存中有 直接設置
            iv.setImageBitmap(bitmap);
        }
    }

    /**
     * 將一個URL轉換成bitmap對象
     *
     * @param urlStr 圖片的URL
     * @return
     */
    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(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(is);
            connection.disconnect();
            return bitmap;
        } catch (java.io.IOException e) {
            e.printStackTrace();
        } finally {
            try {
                is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 將URL中的圖片保存到輸出流中
     *
     * @param urlString    圖片的URL地址
     * @param outputStream 輸出流
     * @return 輸出流
     */
    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(), IO_BUFFER_SIZE);
            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
            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;
    }

    /**
     * 加載從start到end的全部的Image
     *
     * @param start
     * @param end
     */
    public void loadImages(int start, int end) throws IOException {
        for (int i = start; i < end; i++) {
            String url = NewsAdapter.urls[i];
            //從緩存中取出圖片
            Bitmap bitmap = getBitmapFromDiskCache(url);
            //若是緩存中沒有,則須要從網絡中下載
            if (bitmap == null) {
                NewsAsyncTask task = new NewsAsyncTask(url);
                task.execute(url);
                mTaskSet.add(task);
            } else {
                //若是緩存中有 直接設置
                ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                imageView.setImageBitmap(bitmap);
            }
        }
    }

    /**
     * 中止全部當前正在運行的任務
     */
    public void cancelAllTask() {
        if (mTaskSet != null) {
            for (NewsAsyncTask task : mTaskSet) {
                task.cancel(false);
            }
        }
    }

    /*--------------------------------DiskLruCaChe的實現-----------------------------------------*/

    /**
     * 建立緩存文件
     *
     * @param context  上下文對象
     * @param filePath 文件路徑
     * @return 返回一個文件
     */
    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);
    }

    /**
     * 獲得當前可用的空間大小
     *
     * @param path 文件的路徑
     * @return
     */
    private long getUsableSpace(File path) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD) {
            return path.getUsableSpace();
        }
        final StatFs stats = new StatFs(path.getPath());
        return (long) stats.getBlockSize() * (long) stats.getAvailableBlocks();
    }

    /**
     * 將URL轉換成key
     *
     * @param url 圖片的URL
     * @return
     */
    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的字節數組轉換成哈希字符串
     *
     * @param bytes URL的字節數組
     * @return
     */
    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寫入緩存
     *
     * @param url
     * @return
     * @throws IOException
     */
    private Bitmap addBitmapToDiskCache(String url) throws IOException {
        //若是當前線程是在主線程 則異常
        if (Looper.myLooper() == Looper.getMainLooper()) {
            throw new RuntimeException("can not visit network from UI Thread.");
        }
        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(DISK_CACHE_INDEX);
            if (downloadUrlToStream(url, outputStream)) {
                //提交寫入操做
                editor.commit();
            } else {
                //撤銷寫入操做
                editor.abort();
            }
            mDiskCache.flush();
        }
        return getBitmapFromDiskCache(url);
    }
複製代碼
/**
     * 從緩存中取出Bitmap
     *
     * @param url 圖片的URL
     * @return 返回Bitmap對象
     * @throws IOException
     */
    private Bitmap getBitmapFromDiskCache(String url) throws IOException {
        //若是當前線程是主線程 則異常
        if (Looper.myLooper() == Looper.getMainLooper()) {
            Log.w("DiskLruCache", "load bitmap from UI Thread, it's not recommended!");
        }
        //若是緩存中爲空  直接返回爲空
        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) {
            //獲得文件輸入流
            FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
            //獲得文件描述符
            FileDescriptor fileDescriptor = fileInputStream.getFD();
            bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        }
        return bitmap;
    }

     /*--------------------------------DiskLruCaChe的實現-----------------------------------------*/
複製代碼
/*--------------------------------異步任務AsyncTask的實現--------------------------------------*/
	    /**
	     * 異步任務類
	     */
	    private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
	        private String url;
        public NewsAsyncTask(String url) {
            this.url = url;
        }

        @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);
            ImageView imageView = (ImageView) mListView.findViewWithTag(url);
            if (imageView != null && bitmap != null) {
                imageView.setImageBitmap(bitmap);
            }
            mTaskSet.remove(this);
        }
    }

    /*--------------------------------異步任務AsyncTask的實現--------------------------------------*/
}
複製代碼

最後不要忘了在自定義Adapter中調用DiskCache這個工具類,並把圖片加載方法換成DiskLruCache方式:

//第三種方式 經過異步任務方式設置 且利用DiskLruCache存儲到磁盤緩存中
    try {
        mDiskCacheUtil.showImageByAsyncTask(viewHolder.iconImage, iconUrl);
    } catch (IOException e) {
        e.printStackTrace();
    }
複製代碼

好了,如今來看效果圖吧:

DiskLruCaChe

從圖中能夠看出儘管退出了APP,可是從新打開的時候,仍然不須要加載圖片,大功告成!

##緩存策略對比與總結

好了,DiskLruCache也講完了。回顧以前的LruCache,一樣是Android中的緩存策略。那它們之間有什麼不一樣呢?

  1. LruCache是Android中的已經封裝好的類,能夠直接用。可是DiskLruCache須要導入對應的包後,才能使用。
  2. LruCache實現的是內存緩存,當APP被kill的時候,緩存也隨之消失。而DiskLruCache實現的是磁盤緩存,當APP被kill的時候,緩存仍然不會消失。
  3. LruCache的內部實現是LinkedHashMap,也就是集合。因此添加獲取方式經過put與get就好了。而DiskLruCache是經過文件流的形式來緩存,因此添加獲取是經過輸入輸出流來實現。

大致也就也上三種主要的區別。

最後我想說的是,本項目是爲了你們看起來方便,有對比性,因此把普通線程加載、LruCache加載、DiskLruCache加載分別封裝了不一樣的類。

可是在平常開發中,須要Bitmap的壓縮類與這幾種加載方式在一塊兒封裝成一個大的類。就是你們常提到的 ImageLoader 。它專門用來處理Bitmap的加載。

這樣作的好處就是將三種加載方式結合,也就是你們常據說的 三級緩存機制 ,網上也有不少優秀的ImageLoader,固然你們也能夠嘗試嘗試,本身寫出一個ImageLoader。

##結語

經過兩篇文章中的一個小小的實戰項目,終於把緩存策略說完了。寫文章的過程當中本身也是回顧了整個項目,受益不淺。有些時候把一個東西用本身的話分享出來而且讓別人能聽懂,比本身學一個東西要難不少。因此以爲常常寫博客,仍是對知識的消化有點幫助的。

最後因爲我水平有限,項目和文章中不免會有錯誤,歡迎你們指正與交流。

##項目源碼

IamXiaRui - MoocNewsDemo


我的博客:www.iamxiarui.com

原文連接:http://www.iamxiarui.com/?p=719

相關文章
相關標籤/搜索