天天看點

Glide-拆解圖檔多級緩存正文從源碼層級拆解Glide的緩存結構Demo

Glide-圖檔LRU緩存、複用池

  • 正文
    • 第一層 活動資源
    • 第二層 記憶體緩存
    • 第三、四層 磁盤緩存
    • Bitmap複用
  • 從源碼層級拆解Glide的緩存結構
      • 1、首先是活動緩存所存儲的對象
      • 2、活動緩存—ActivityResource
      • 3、記憶體緩存—MemoryCache
      • 4、Bitmap複用—BitmapPool
  • Demo

正文

Glide 使用簡明的流式文法API,大多數情況下,可能完成圖檔的設定你隻需要:

Glide.with(activity) .load(url) .into(imageView);

預設情況下,Glide 會在開始一個新的圖檔請求之前檢查以下多級的緩存:

  1. 活動資源 (Active Resources)
  2. 記憶體緩存 (Memory Cache)
  3. 資源類型(Resource Disk Cache)
  4. 原始資料 (Data Disk Cache)

    活動資源:如果目前對應的圖檔資源正在使用,則這個圖檔會被Glide放入活動緩存。

    記憶體緩存:如果圖檔最近被加載過,并且目前沒有使用這個圖檔,則會被放入記憶體中

    資源類型: 被解碼後的圖檔寫入磁盤檔案中,解碼的過程可能修改了圖檔的參數(如:inSampleSize、inPreferredConfig)

    原始資料: 圖檔原始資料在磁盤中的緩存(從網絡、檔案中直接獲得的原始資料)

    在調用into之後,Glide會首先從Active Resources查找目前是否有對應的活躍圖檔,沒有則查找記憶體緩存,沒有則查找資源類型,沒有則查找資料來源。

Glide-拆解圖檔多級緩存正文從源碼層級拆解Glide的緩存結構Demo

相較于常見的記憶體+磁盤緩存,Glide将其緩存分成了4層。

第一層 活動資源

當需要加載某張圖檔能夠從記憶體緩存中獲得的時候,在圖檔加載時主動将對應圖檔從記憶體緩存中移除,加入到活動資源中。

這樣也可以避免因為達到記憶體緩存最大值或者系統記憶體壓力導緻的記憶體緩存清理,進而釋放掉活動資源中的圖檔(recycle)。

活動資源中是一個”引用計數"的圖檔資源的弱引用集合。

因為同一張圖檔可能在多個地方被同時使用,每一次使用都會将引用計數+1,而當引用計數為0時候,則表示這個圖檔沒有被使用也就是沒有強引用了。

這樣則會将圖檔從活動資源中移除,并加入記憶體緩存。

/**
 * 這個資源沒有正在使用了
 * 将其從活動資源移除
 * 重新加入到記憶體緩存中
 *
 * @param key
 * @param resource
 */
@Override
public void onResourceReleased(Key key, Resource resource) {
    activeResource.deactivate(key);
    //從活動緩存中移除也就是活動緩存中引用計數為0的時候需要加入記憶體緩存
    lruMemoryCache.put(key, resource);
}
           

第二層 記憶體緩存

記憶體緩存預設使用LRU(緩存淘汰算法/最近最少使用算法),當資源從活動資源移除的時候,會加入此緩存。使用圖檔的時候會主動從此緩存移除,加入活動資源

LRU在Android support-v4中提供了LruCache工具類。

/**
 * @param maxSize for caches that do not override {@link #sizeOf}, this is
 *     the maximum number of entries in the cache. For all other caches,
 *     this is the maximum sum of the sizes of the entries in this cache.
 */
public LruCache(int maxSize) {
    if (maxSize <= 0) {
        throw new IllegalArgumentException("maxSize <= 0");
    }
    this.maxSize = maxSize;
    this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
           

LRUCache通過看源碼即可發現,内部實作很簡單,通過雙向連結清單LinkedHashMap來進行實作的,其中設定accessOrder為true時,會進行自動排序,也就是get之後item會被提到隊列頭部;

LruCache會在每次get/put的時候判斷資料如果達到了maxSize,則會優先删除tail尾端的資料。

Glide-拆解圖檔多級緩存正文從源碼層級拆解Glide的緩存結構Demo

第三、四層 磁盤緩存

磁盤緩存同樣使用LRU算法。

Resource緩存的是經過解碼後的圖檔,如果再使用就不需要再去進行解碼配置(BitmapFactory.Options),加快獲得圖檔速度。

比如原圖是一個100x100的ARGB_8888圖檔,在首次使用的時候需要的是50x50的RGB_565圖檔,那麼Resource将50x50 RGB_565緩存下來,

再次使用此圖檔的時候就可以從 Resource 獲得。不需要去計算inSampleSize(縮放因子)。

Data 緩存的則是圖像原始資料。

Bitmap複用

如果緩存都不存在,那麼會從源位址獲得圖檔(網絡/檔案)。而在解析圖檔的時候會需要可以獲得BitmapPool(複用池),達到複用的效果。

Glide-拆解圖檔多級緩存正文從源碼層級拆解Glide的緩存結構Demo

複用效果如上。在未使用複用的情況下,每張圖檔都需要一塊記憶體。而使用複用的時候,如果存在能被複用的圖檔會重複使用該圖檔的記憶體。

是以複用并不能減少程式正在使用的記憶體大小。Bitmap複用,解決的是減少頻繁申請記憶體帶來的性能(抖動、碎片)問題。

圖檔複用依賴于inBitmap和mutible,具體原理可檢視google文檔,後面在講BitmapPool在Glide中的使用也會提到

https://developer.android.google.cn/topic/performance/graphics/manage-memory#java

Google給出的案例可以看出:

使用方式為在解析的時候設定Options的inBitmap屬性。

Bitmap的inMutable需要為true。

Android 4.4及以上隻需要被複用的Bitmap的記憶體必須大于等于需要新獲得Bitmap的記憶體,則允許複用此Bitmap。

4.4以下(3.0以上)則被複用的Bitmap與使用複用的Bitmap必須寬、高相等并且使用複用的Bitmap解碼時設定的inSampleSize為1,才允許複用。

是以Glide中,在每次解析一張圖檔為Bitmap的時候(磁盤緩存、網絡/檔案)會從其BitmapPool中查找一個可被複用的Bitmap。

BitmapPool是Glide中的Bitmap複用池,同樣适用LRU來進行管理。

當一個Bitmap從記憶體緩存 被動的被移除(記憶體緊張、達到maxSize)的時候并不會被recycle。而是加入這個BitmapPool,隻有從這個BitmapPool 被動

被移除的時候,Bitmap的記憶體才會真正被recycle釋放。

基礎知識已經講完了,接下來我們開始拆解Glide

從源碼層級拆解Glide的緩存結構

1、首先是活動緩存所存儲的對象

public class Resource {
private Bitmap bitmap;
    //引用計數
    private int acquired;
    //對外部的回調,當引用計數為0的時候通過該回調需要放入記憶體緩存
    private ResourceListener listener;
    private Key key;
   
   //接下來描述一下主要方法,也就是應用計數的加減
   


    /**
     * 引用計數+1
     */
    public void acquire() {
        if (bitmap.isRecycled()) {
            throw new IllegalStateException("Acquire a recycled resource");
        }
        ++acquired;
    }


   /**
     * 引用計數-1
     */
    public void release() {
        if (--acquired == 0) {
            //如果圖檔不再使用需要回調出去
            listener.onResourceReleased(key, this);
        }
    }


   /**
     * 當acquired 為0的時候 回調 onResourceReleased
     */
    public interface ResourceListener {
        void onResourceReleased(Key key, Resource resource);
    }
   
}
           

2、活動緩存—ActivityResource

由于之前說到,活動緩存實作的無非就是

  1. 引用計數
  2. 弱引用
  3. 不再使用時回調出去
  4. 使用時再從記憶體當中put進來

看源碼:

public class ActiveResource {
   //隊列,由于存儲結構為弱引用,是以需要通過隊列進行管理
   private ReferenceQueue<Resource> queue;  
   //回調接口,當圖檔不再使用,從活動緩存中移除時需要回調出去
   private final Resource.ResourceListener resourceListener;
   // 弱應用 (強軟弱虛可自行查資料了解)
   private Map<Key, ResourceWeakReference> activeResources = new HashMap<>();
   // 當圖檔被GC回收之後,會被加入queue,這個時候通過保護線程進行監聽,實時更新activityResources,確定activityResources中都是有效的圖檔資源
   private Thread cleanReferenceQueueThread;
   //标志位,通過該标志位來判斷是否需要關閉目前保護線程
   private boolean isShutdown;


   //接下來也是對主要方法做講解


   
   /**
    * 加入活動緩存
    *
    * @param key
    * @param resource
    */
   public void activate(Key key, Resource resource) {
       resource.setResourceListener(key, resourceListener);
       activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
   }


   * 移除活動緩存
    */
   public Resource deactivate(Key key) {
       ResourceWeakReference reference = activeResources.remove(key);
       if (reference != null) {
           return reference.get();
       }
       return null;
   }


/**
    * 引用隊列,通知我們弱引用被回收了
    * 讓我們得到通知的作用
    *
    * @return
    */
   private ReferenceQueue<Resource> getReferenceQueue() {
       if (null == queue) {
           queue = new ReferenceQueue<>();
           cleanReferenceQueueThread = new Thread() {
               @Override
               public void run() {
                   while (!isShutdown) {
                       try {
                           //被回收掉的引用
                           ResourceWeakReference ref = (ResourceWeakReference) queue.remove();
                           activeResources.remove(ref.key);
                       } catch (InterruptedException e) {
                       }
                   }
               }
           };
           cleanReferenceQueueThread.start();
       }
       return queue;
   }


   void shutdown() {
       isShutdown = true;
       if (cleanReferenceQueueThread != null) {
           cleanReferenceQueueThread.interrupt();
           try {
               //5s  必須結束掉線程
               cleanReferenceQueueThread.join(TimeUnit.SECONDS.toMillis(5));
               if (cleanReferenceQueueThread.isAlive()) {
                   throw new RuntimeException("Failed to join in time");
               }
           } catch (InterruptedException e) {
               e.printStackTrace();
           }
       }
   }


   static final class ResourceWeakReference extends WeakReference<Resource> {

       final Key key;

       public ResourceWeakReference(Key key, Resource referent,
                                    ReferenceQueue<? super Resource> queue) {
           super(referent, queue);
           this.key = key;
       }
   }
}
           

3、記憶體緩存—MemoryCache

由于之前說到,記憶體緩存實作的無非就是

  1. 活動緩存不用了抛給記憶體緩存–put
  2. 活動緩存沒找到,從記憶體緩存中尋找,找的之後再加入活動緩存當中—remove(該remove不進行資源釋放)
  3. 由于記憶體緩存使用LRUCache,是以存在達到最大存儲的時候會移除末尾的item,這個時候并不會馬上執行recycler操作,而是會将其加入BitmapPool當中,進行記憶體複用
/**
* Created by Administrator on 2018/5/4.
*/

public interface MemoryCache {
   interface ResourceRemoveListener{
       void onResourceRemoved(Resource resource);
   }
   Resource put(Key key, Resource resource);
   void setResourceRemoveListener(ResourceRemoveListener listener);
   Resource remove2(Key key);
}
           

具體實作

public class LruMemoryCache extends LruCache<Key, Resource> implements MemoryCache {

   private ResourceRemoveListener listener;
   private boolean isRemoved;
   public LruMemoryCache(int maxSize) {
       super(maxSize);
   }

   //由于父類為LruCache,需要重寫sizeof,因為預設傳回為1
   @Override
   protected int sizeOf(Key key, Resource value) {
       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
           //當在4.4以上手機複用的時候 需要通過此函數獲得占用記憶體
           //由于記憶體複用的原因,可能目前圖檔占用記憶體大于實際圖檔大小,需要擷取圖檔實際占用記憶體大小
           return value.getBitmap().getAllocationByteCount();
       }
       //getByteCount傳回的是圖檔實際大小
       return value.getBitmap().getByteCount();
   }

   //由于父類為LruCache,需要重新entryRemoved,根據實際情況判斷是否需要recycler
   @Override
   protected void entryRemoved(boolean evicted, Key key, Resource oldValue, Resource newValue) {
       //給複用池使用
       if (null != listener && null != oldValue && !isRemoved) {
           listener.onResourceRemoved(oldValue);
       }
   }


   //主動remove的情況不進行回調出去
   @Override
   public Resource remove2(Key key) {
       // 如果是主動移除的不會掉 listener.onResourceRemoved
       isRemoved = true;
       Resource remove = remove(key);
       isRemoved = false;
       return remove;
   }

   /**
    * 資源移除監聽
    *
    * @param listener
    */
   @Override
   public void setResourceRemoveListener(ResourceRemoveListener listener) {
       this.listener = listener;
   }
}
           

4、Bitmap複用—BitmapPool

由于之前說到,記憶體複用實作的無非就是

  1. 讀–get
  2. 寫–put

接口實作為

public interface BitmapPool {
    void put(Bitmap bitmap);
    /**
     * 獲得一個可複用的Bitmap
     *  三個參數計算出 記憶體大小
     * @param width
     * @param height
     * @param config 主要用來差別是ARGB8888還是RGB565,分别對應4位元組和2位元組
     * @return
     */
    Bitmap get(int width,int height,Bitmap.Config config);
}
           

Glide當中BitmapPool的實作也是基于LRUCache

public class LruBitmapPool extends LruCache<Integer, Bitmap> implements BitmapPool {
    //通過isRemoved标記位來防止主動移除的時候調用entryRemoved引起的錯誤邏輯調用
    private boolean isRemoved;

    // 負責篩選
    NavigableMap<Integer, Integer> map = new TreeMap<>();

    private final static int MAX_OVER_SIZE_MULTIPLE = 2;

    public LruBitmapPool(int maxSize) {
        super(maxSize);
    }

    /**
     * 将Bitmap放入複用池
     *
     * @param bitmap
     */
    @Override
    public void put(Bitmap bitmap) {
        //isMutable 是複用的标記 必須是true
        if (!bitmap.isMutable()) {
            bitmap.recycle();
            return;
        }
        int size = bitmap.getAllocationByteCount();
        //如果複用池大小不夠,則舍棄目前bitmap
        if (size >= maxSize()) {
            bitmap.recycle();
            return;
        }
        put(size, bitmap);
        map.put(size, 0);
    }

    /**
     * 獲得一個可複用的Bitmap
     */
    @Override
    public Bitmap get(int width, int height, Bitmap.Config config) {
        //新Bitmap需要的記憶體大小  (隻關心 argb8888和RGB65)
        int size = width * height * (config == Bitmap.Config.ARGB_8888 ? 4 : 2);
        //獲得等于 size或者大于size的key
        Integer key = map.ceilingKey(size);
        //從key集合從找到一個>=size并且 <= size*MAX_OVER_SIZE_MULTIPLE
        if (null != key && key <= size * MAX_OVER_SIZE_MULTIPLE) {
            isRemoved = true;
            Bitmap remove = remove(key);
            isRemoved = false;
            return remove;
        }
        return null;
    }


    @Override
    protected int sizeOf(Integer key, Bitmap value) {
        return value.getAllocationByteCount();
    }

    @Override
    protected void entryRemoved(boolean evicted, Integer key, Bitmap oldValue, Bitmap newValue) {
        map.remove(key);
        //非主動移除時需要recycle
        if (!isRemoved)
            oldValue.recycle();
    }
}
           

到這裡已經實作了Glide多級緩存所需要的相應存儲結構

Demo

public class CacheTest implements Resource.ResourceListener, MemoryCache.ResourceRemoveListener {

    LruMemoryCache lruMemoryCache;
    ActiveResource activeResource;
    BitmapPool bitmapPool;

    public Resource test(Key key) {
        bitmapPool = new LruBitmapPool(10);
        //記憶體緩存
        lruMemoryCache = new LruMemoryCache(10);
        lruMemoryCache.setResourceRemoveListener(this);
        //活動資源緩存
        activeResource = new ActiveResource(this);

        /**
         * 第一步 從活動資源中查找是否有正在使用的圖檔
         */
        Resource resource = activeResource.get(key);
        if (null != resource) {
            //當不使用的時候 release
            resource.acquire();
            return resource;
        }
        /**
         * 第二步 從記憶體緩存中查找
         */
        resource = lruMemoryCache.get(key);
        if (null != resource) {
            //1.為什麼從記憶體緩存移除?
            // 因為lru可能移除此圖檔 我們也可能recycle掉此圖檔
            // 如果不移除,則下次使用此圖檔從活動資源中能找到,但是這個圖檔可能被recycle掉了
            lruMemoryCache.remove2(key);
            resource.acquire();
            activeResource.activate(key, resource);
            return resource;
        }
        return null;
    }

    /**
     * 這個資源沒有正在使用了
     * 将其從活動資源移除
     * 重新加入到記憶體緩存中
     *
     * @param key
     * @param resource
     */
    @Override
    public void onResourceReleased(Key key, Resource resource) {
        activeResource.deactivate(key);
        lruMemoryCache.put(key, resource);
    }

    /**
     * 從記憶體緩存被動移除 回調
     * 放入 複用池
     *
     * @param resource
     */
    @Override
    public void onResourceRemoved(Resource resource) {
        bitmapPool.put(resource.getBitmap());
    }
}
           

參考 https://www.jianshu.com/p/97fd67720b34

繼續閱讀