Android三級緩存原理及用LruCache、DiskLruCache實現一個三級緩存的ImageLoader

首先附上我的幾篇其它文章鏈接感興趣的可以看看,如果文章有異議的地方歡迎指出,共同進步,順便點贊謝謝!!!
Android framework 源碼分析之Activity啓動流程(android 8.0)
Android studio編寫第一個NDK工程的過程詳解(附Demo下載地址)
面試必備1:HashMap(JDK1.8)原理以及源碼分析
面試必備2:JDK1.8LinkedHashMap實現原理及源碼分析
Android事件分發機制原理及源碼分析
View事件的滑動衝突以及解決方案
Handler機制一篇文章深入分析Handler、Message、MessageQueue、Looper流程和源碼

三級緩存概述

緩存是一種通用的思想可以用在很多場景中,但在實際的開發中經常用於Bitmap的緩存,用於提高圖片的加載效率、提升產品的用戶體驗和節省用戶流量。目前常見的緩存策略是LruCache和DiskLruCachey用它們分別實現內存緩存和硬盤緩存。Lru是Least Recently Used的縮寫即最近最少使用算法,這種算法思想是:當緩存容量快滿時,會刪除最近做少使用的緩存對象。在這裏提醒大家一句三級緩存一般針對的是加載網絡圖片時常用的緩存策略。
附上我自定義的ImageLoader的代碼地址去理解三級緩存原理:https://github.com/mayanhu/ImageLoader

三級緩存的流程

當我們要加載一張網絡上的圖片時一般流程:

  1. 按照一定的規則生成一個緩存Key,生成可以的算法自己可以定義保證唯一性就行,需要注意的是我們的key不要直接用圖片的URL因爲URL中的的特殊字符不方便處理
  2. 先去內存中通過key去讀取內存中緩存的讀取圖片資源,如果內存中有則直接返回該對象
  3. 如果內存中沒有則根據key從磁盤中讀取,如果磁盤緩存有則:1. 返回該資源 2.將該資源重新放入內存緩存中 3. 將改資源從磁盤緩存中移除
  4. 如果磁盤緩存中也沒有改圖片資源,則發起網絡請求,從網絡獲取圖片資源

強引用 、弱引用 、軟引用、虛引用的區別:

因爲LruCache內部採用LinkedHashMap以強引用的形式緩存外界的對象,所以在講LruCache前需要先了解Java對象(即堆內存中對象)引用的四個級別以及各自的特點,以便我們能更好的掌握內存緩存LruCache的實現原理。

  1. 強引用:是指我們直接引用的對象,如String s=new Stirng(“abc”),特點是:只要引用存在,垃圾回收器永遠不會回收,如果無限制的使用new 強對象會拋出OOM異常。
  2. 軟引用WeakReference:特點是:當一個對象只有軟引用存在時,當系統內存不足時此對象會被GC回收。
  3. 弱引用SoftReference:當一個對象只有弱引用存在時,此對象隨時會被GC回收。
  4. 虛引用PhantomReference:如果一個對象僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用主要用來跟蹤對象被垃圾回收器回收的活動。
  5. 在此我只是簡單描述了一下對象引用的特點,以便理解LruCache的實現原理,詳細的用法和特點需要大家自行查閱資料,在這裏不做過多描述。

內存緩存LruCache

首先簡單說一下三級緩存爲什麼優先從內存中加載:

  • 內存的加載速度時最快的,比從磁盤加載快的多
  • 另一方面內存緩存的是Bitmap對象,可直接使用,而磁盤緩存的是文件需要我們先轉換成對象才能使用

LruCache內部採用LinkedHashMap以強引用的形式緩存外界的對象,就是以鍵值對的形式緩存我們的Bitmap對象。LruCache是一個範型類,它是線程安全的因爲它對數據的操作都加了鎖。它的使用也很簡單代碼如下:

    LruCache<String,Bitmap> mLruCache;
    public void init(){
        //當前進程的可用內存
        int mxaMeory=(int)(Runtime.getRuntime().maxMemory()/1024);
        //當前進程的可用內存/8爲緩存大小
        int meoryChache=mxaMeory/8;
        //初始化緩存大小 複寫sizeOf計算緩存對象的大小,單位要和總容量的一直
        mLruCache=new LruCache<String, Bitmap>(meoryChache){
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes()*bitmap.getHeight()/1024;//單位要和meoryChache保持一致
            }

            @Override
            protected void entryRemoved(boolean evicted, String key, Bitmap oldValue, Bitmap newValue) {
                //此方法有需要是複寫,做一些資源回收動做,在LruCache移除最近最少使用的對象時自動調用
                //比如爲了提高內存緩存的效率,我可在用一個弱引用的LinkHashMap去存儲不常使用的對象,
                //實現進一步的緩存
                super.entryRemoved(evicted, key, oldValue, newValue);
            }
        };
    }

除了創建外LruCache還提供了:

  //存儲
   		mLruCache.put(key,value);
  //獲取
       // mLruCache.get(key);
  刪除
//        mLruCache.remove(key)

DiskLruCache

第二級緩存,硬盤緩存DiskLruCache,它是通過將緩存對象寫入文件系統從而實現緩存,需要注意的是DiskLruCache它不是AndroidSDK的源碼,但是他得到了官方文檔的推薦,使用它時需要從如下網址下載:
https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java (注意需要翻牆)。

1:創建DiskLruCache

DiskLruCache不能通過構造方法創建,它提供了open方法用於創建自身,如下是open()方法的源碼:

/**
   * Opens the cache in {@code directory}, creating a cache if none exists
   * there.
   *
   * @param directory  緩存目錄
   * @param  appVersion 表示應用版本號一般設置爲1即可,當其發生變化時DiskLruCache會清空之前所有的緩存文件
   * @param valueCount  表示一個鍵對應幾個值,即一個緩存Key對應幾個緩存文件, 一般設置爲1即可
   * @maxSize  最大緩存容量
   */
  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
      throw new IllegalArgumentException("valueCount <= 0");
    }

    // If a bkp file exists, use it instead.
    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
      File journalFile = new File(directory, JOURNAL_FILE);
      // If journal file also exists just delete backup file.
      if (journalFile.exists()) {
        backupFile.delete();
      } else {
        renameTo(backupFile, journalFile, false);
      }
    }

	//初始化如果存在則執行文件尾加操作,否者創建Dir創建在創建DiskLruCache的實例
    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    if (cache.journalFile.exists()) {
      try {
        cache.readJournal();
        cache.processJournal();
        cache.journalWriter = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }

    // Create a new empty cache.
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    cache.rebuildJournal();
    return cache;
  }

2:DiskLruCache的增加圖片操作

DiskLruCache的緩存操作是通過Editor完成,通過緩存key區獲取Editor對象,Editor表示緩存對象的編輯對象,通過Editor對象獲取一個輸出流指向緩存文件,實現添加緩存操作,這裏需要注意的是圖片的緩存一般不直接使用該圖片的URL,因爲URL中有可能有特殊字符影響使用。一般採用URL的MD5作爲key。添加緩存操作代碼如下
1: 先去獲取緩存key

   
    /**
     * 根據url  獲取MD5key  因爲url中可能含有特殊字符 影響在Android中直接使用
     * @param url
     * @return
     */
    private String hashKeyFormUrl(String url){
        String cacheKey;
        try {
            MessageDigest messageDigest=MessageDigest.getInstance("MD5");
            messageDigest.update(url.getBytes());
            cacheKey=bytesToHexString(messageDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
            cacheKey=String.valueOf(url.hashCode());
        }
        return cacheKey;
    }

    /**
     *
     * @param digest
     * @return
     */
    private String bytesToHexString(byte[] digest) {
        StringBuilder sb=new StringBuilder();
        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();
    }

2:通過緩存key獲取Editor對象實現添加操作:

 /**
     * 添加一個緩存圖片
     * @param urlKey
     */
    public void addDiskCacheEdit(String urlKey){
        if (mDiskLruCache==null) {
            return;
        }
        String key = hashKeyFormUrl(url);
        try {
            //通過key拿到edit對象---》outputStream
            DiskLruCache.Editor edit = mDiskLruCache.edit(key);
            if (edit != null) {
                OutputStream outputStream = edit.newOutputStream(DISK_CACHE_INDEX);
                //因爲open方法中設置一個鍵對應一個值,所以DISK_CACHE_INDEX一設置爲0 即可
                //如果文件下載成功提交編輯
                if (downloadUrlToStream(url,outputStream)){
                    //提交寫操作進行提交到文件系統
                    edit.commit();
                }else {
                    //圖片下載異常 回退整個操作
                    edit.abort();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
/**
     * 下載圖片資源時,將期存入硬盤緩存目錄
     * @param urlImage
     * @param outputStream
     * @return
     */
    private boolean downloadUrlToStream(String urlImage, OutputStream outputStream) {
        HttpURLConnection urlConnection=null;
        BufferedInputStream in=null;
        BufferedOutputStream out=null;
        try {
            URL url=new URL(urlImage);
            urlConnection= (HttpURLConnection) url.openConnection();
            in=new BufferedInputStream(urlConnection.getInputStream());
            out =new BufferedOutputStream(outputStream);
            int b;
            while ((b=in.read())!=-1){
                out.write(b);
            }
            return true;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
           release(urlConnection,in,out);
        }
        return false;
    }

3:DiskLruCache的獲取圖片操作

和圖片的添加操作類似,查找操作也是需要將url轉換爲key,然後通過DiskLruCache的get(String key)獲取Snapshot對象,通過Snapshot獲取緩存文件的輸入流,通過輸入流去獲取緩存的Bitmap對象去使用,只是在使用時存入了內存緩存中。下面是我的獲取硬盤緩存的代碼:

/**
     * 獲取一個硬盤緩存圖片  並且通過控件寬高進行縮放加載  避免OOM
     * @param imageUrl
     * @return
     */
    public Bitmap getBitmapChache(String imageUrl,int reqWith,int reqHeight){
        Bitmap bitmap=null;
        String keyFormUrl = hashKeyFormUrl(imageUrl);
        FileInputStream inputStream=null;
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(keyFormUrl);
            if (snapshot != null) {
                //inputStream是一種有序的文件流,通過Options縮放存在問題,兩次decodeStream影響了文件流的位置屬性,第二次decodeStream時得到的的爲null
                 inputStream= (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
                //解決方案是通過文件夾流來得到對應文件的描述符讓後通過BitmapFactory.decodeFileDescriptor來加載一張縮略圖
                FileDescriptor fileDescriptor = inputStream.getFD();
                bitmap=BitmapUtils.decodeSampledBitmapFileDescriptor(fileDescriptor,reqWith,reqHeight);
                if (bitmap != null) {
                    // TODO: 2019/3/6 添加到內存緩存  自定義ImageLoader
                }
               return bitmap;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }

/**
     * 文件夾流
     * @param fileDescriptor  文件夾流
     * @param reqWidth
     * @param reqHeight
     * @return
     */
  public static Bitmap decodeSampledBitmapFileDescriptor(FileDescriptor fileDescriptor,int reqWidth,int reqHeight){
      BitmapFactory.Options options=new BitmapFactory.Options();
      //inJustDecodeBounds=true只會解析圖片的原始寬高信息,不會真正的去加載圖片
      options.inJustDecodeBounds=true;
      //第一次decode  加載原始圖片數據
      BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
      //計算縮放比例
      options.inSampleSize=calculateInSampleSize(options,reqWidth,reqHeight);
      //重新設置加載圖片
      options.inJustDecodeBounds=false;
      return BitmapFactory.decodeFileDescriptor(fileDescriptor,null,options);
  }

4:刪除緩存文件

 /**
     * 刪除指定的緩存文件
     * @param url
     */
    private void reloveDiskCache(String url){
        String key = hashKeyFormUrl(url);
        try {
            mDiskLruCache.remove(key);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 刪除所有的緩存文件
     * @param 
     */
    private void clearALLCache(){
       
        try {
            mDiskLruCache.delete();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

總結:

  1. 內存緩存LruCache和DiskLruCache都是通過LRU 算法實現 ,對數據的操作都是用來同步鎖機制,即都是線程安全的。
  2. 內存緩存LruCache內部採用LinkedHashMap以強引用的形式緩存外界的對象,就是以鍵值對的形式緩存我們的Bitmap對象。
  3. DiskLruCache添加緩存圖片操作是通過緩存key獲取Eidtor對象,並通過Editor對象獲取指向改緩存文件的輸出流,通過該輸出流實現添加操作。
  4. DiskLruCache獲取緩存圖片的操作是通過緩存key獲取Snapshot對象,通過Snapshot對象可得到緩存文件輸出流,通過該流獲取緩存對象。
  5. 三級緩存的核心就是:LruCache和DiskLruCache的使用,即先從內存緩存中獲取,如果獲取不到再從硬盤緩存中獲取,如果在硬盤中獲取到了則:添加到內存緩存中,並從硬盤緩存中移除,如果磁盤中也沒有獲取到則發起網絡請求進行圖片下載下載成功後進行緩存。

這裏只是簡單的描述了三級緩存的的原理和使用, 它也是現在主流的圖片加載框架GLide、ImageLoader實現圖片緩思想,只是它們的每一步做的更加精密嚴謹,使用更加方便,內部使用了多種設計模式實現了代碼的高度解耦。

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