Glide源碼探究(二) 內存緩存 LRUCache 內存緩存整體流程 補充: 內存緩存的查詢順序

讓我們接着上一篇筆記繼續講Engine的load方法,這裏面就是Glide的資源加載流程。

public <R> LoadStatus load(...) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options);

    EngineResource<?> memoryResource;
    synchronized (this) {
      memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);

      if (memoryResource == null) {
        return waitForExistingOrStartNewJob(...);
      }
    }
    
    cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE, false);
    return null;
}

這個方法的流程其實也挺清晰的:

  1. 創建緩存的key,這個key由一系列的參數組成,其中最重要的參數model在我們的例子中就是傳進去的url。
  2. 使用這個key從內存緩存中查詢資源
  3. 如果內存緩存中查不到資源就開啓線程去加載資源
  4. 如果內存緩存中可以查到資源就調用cb.onResourceReady回調

流程圖如下:

內存緩存

內存緩存的流程也比較清晰從代碼上看,如果開啓了內存緩存的話會先從ActiveResources中查詢,查不到的話再從Cache裏面查詢:

private EngineResource<?> loadFromMemory( EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {
      return null;
    }

    EngineResource<?> active = loadFromActiveResources(key);
    if (active != null) {
      ...
      return active;
    }

    EngineResource<?> cached = loadFromCache(key);
    if (cached != null) {
      ...
      return cached;
    }

    return null;
}

這兩個東西同樣是內存緩存,那有啥區別呢?我們先看ActiveResources:

// Engine
private EngineResource<?> loadFromActiveResources(Key key) {
    EngineResource<?> active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
}

// ActiveResources
final class ActiveResources {
  ...
  final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
  ...
  synchronized EngineResource<?> get(Key key) {
    ResourceWeakReference activeRef = activeEngineResources.get(key);
    if (activeRef == null) {
      return null;
    }

    EngineResource<?> active = activeRef.get();
    if (active == null) {
      cleanupActiveReference(activeRef);
    }
    return active;
  }
  ...
  static final class ResourceWeakReference extends WeakReference<EngineResource<?>> {
    ...
  }
  ...
}

loadFromActiveResources實際上是從弱引用緩存裏面查詢資源。既然是緩存當然就要講講它的添加和刪除。

弱引用緩存的添加

首先是添加,弱引用緩存的添加基本有兩個時機。

  1. 從Cache裏面查詢到的時候如果能查到,會將查到的資源放入弱引用緩存:
private EngineResource<?> loadFromCache(Key key) {
    EngineResource<?> cached = getEngineResourceFromCache(key);
    if (cached != null) {
      cached.acquire();
      activeResources.activate(key, cached);
    }
    return cached;
}
  1. 子線程加載完資源後會將資源放入弱引用緩存:
public synchronized void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
    if (resource != null && resource.isMemoryCacheable()) {
      activeResources.activate(key, resource);
    }
    ...
}

弱引用緩存的刪除

細心的同學可能會看到cached.acquire()這個操作,我們來看看它的代碼:

synchronized void acquire() {
    ...
    ++acquired;
}

有沒有想到些啥?沒錯,引用計數!

EngineResource是通過引用計數來管理的。有引用計數增加那就有引用計數減少。減少的操作在release方法裏面:

void release() {
  boolean release = false;
  synchronized (this) {
    if (acquired <= 0) {
      throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
    }
    if (--acquired == 0) {
      release = true;
    }
  }
  if (release) {
    listener.onResourceReleased(key, this);
  }
}

如果引用計數降到了0就會調用listener的onResourceReleased回調回去,在onResourceReleased裏面Engine會將資源從弱引用緩存刪除然後移到cache裏:

// Engine
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource, /*forceNextFrame=*/ false);
    }
}

// ActiveResources
synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if (removed != null) {
      removed.reset();
    }
}

EngineResource.release又是什麼時候被調用的呢?其實調用的地方有好幾處,但是最重要的兩處是

  1. 我們手動調Glide的clear清理資源的時候:
// 手動清理資源
Glide.with(context)
    .clear(img)
  1. 綁定的生命LifecycleListener.onDestroy的時候:
// RequestManager
public synchronized void onDestroy() {
  ...
  for (Target<?> target : targetTracker.getAll()) {
    clear(target);
  }
  ...
}

public void clear(@Nullable final Target<?> target) {
  ...
  untrackOrDelegate(target);
}

private void untrackOrDelegate(@NonNull Target<?> target) {
  ...
  Request request = target.getRequest();
  if (!isOwnedByUs && !glide.removeFromManagers(target) && request != null) {
    target.setRequest(null);
    request.clear();
  }
}

// SingleRequest
public void clear() {
  ...
  engine.release(toRelease);
  ...
}

// Engine
public void release(Resource<?> resource) {
  if (resource instanceof EngineResource) {
    ((EngineResource<?>) resource).release();
  }
  ...
}

簡單來講就是加載資源的時候會把資源放入弱引用緩存,但資源不需要的時候會從弱引用緩存裏面拿出移到另一個內存緩存裏面。所以這些資源都是正在使用的,這個弱引用緩存Glide把它叫做ActiveResources也是比較準確的。

這個緩存使用弱引用的意義在於: 資源是保存在request裏面的,而根據我們上篇筆記的知識,request是以setTag的方式保存在view裏面的。所以當view被回收之後,resource也就沒有別的強引用可以連接到gc root,可以被java垃圾回收機制回收

LRUCache

Engine.load會先從ActiveResources中查詢,查不到的話再從Cache裏面查詢,這個Cache其實是一個LruResourceCache:

public class LruResourceCache extends LruCache<Key, Resource<?>> implements MemoryCache {
    ...
}

從這個lru cache裏面加載資源意味着把資源從lru cache裏面移出,放到弱引用緩存中:

private EngineResource<?> loadFromCache(Key key) {
  EngineResource<?> cached = getEngineResourceFromCache(key);
  if (cached != null) {
    cached.acquire();
    activeResources.activate(key, cached);
  }
  return cached;
}

private EngineResource<?> getEngineResourceFromCache(Key key) {
  Resource<?> cached = cache.remove(key);

  final EngineResource<?> result;
  if (cached == null) {
    result = null;
  } else if (cached instanceof EngineResource) {
    result = (EngineResource<?>) cached;
  } else {
    result = new EngineResource<>(cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
  }
  return result;
}

而正如上節我們講的資源的引用計數被清零的時候就會從弱引用緩存中刪除,加入lru cache中(注意這裏放的是強引用,因爲是從view裏面getTage拿到Resource強引用進行release的):

// Engine
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
    activeResources.deactivate(cacheKey);
    if (resource.isMemoryCacheable()) {
      cache.put(cacheKey, resource);
    } else {
      resourceRecycler.recycle(resource, /*forceNextFrame=*/ false);
    }
}

// ActiveResources
synchronized void deactivate(Key key) {
    ResourceWeakReference removed = activeEngineResources.remove(key);
    if (removed != null) {
      removed.reset();
    }
}

內存緩存整體流程

至此整個內存緩存的架構就大體完整了,當資源被使用的時候會被放到弱引用緩存,當資源不再被使用的時候就會被放入LRU Cache:

補充: 內存緩存的查詢順序

先查弱引用緩存再查lru cache這個順序並不是一開始就這樣的,我剛看glide源碼的時候看的是比較舊的版本,那個時候是先查lru cahe,查不到再查弱引用緩存。

這個順序在2017年11月這個commit之後修改的:

這個修改是爲了修復資源被重複加載的bug,但實際上我看這部分修改的時候,感覺交換查詢順序應該和解這個bug沒有直接關係,它更像是作者在重構之後覺得先查lru cache再查弱引用緩存的順序怪怪的(我那個時候也有這種感覺),順手改了下:

這裏它將原本寫在Engine的弱引用Map封裝成了ActiveResources。

那爲啥順序不是一開始就是先查弱引用緩存呢?原因可能是一開始的代碼內存緩存就沒有弱引用緩存:

public <T, Z> LoadStatus load(String id, int width, int height, ResourceDecoder<InputStream, Z> cacheDecoder,
        ResourceFetcher<T> fetcher, ResourceDecoder<T, Z> decoder,  Transformation<Z> transformation,
        ResourceEncoder<Z> encoder, Priority priority, ResourceCallback cb) {

    Key key = keyFactory.buildKey(id, width, height, cacheDecoder, decoder, transformation, encoder);

    Resource cached = cache.get(key);
    if (cached != null) {
        cached.acquire(1);
        cb.onResourceReady(cached);
        return null;
    }

    ResourceRunner current = runners.get(key);
    if (current != null) {
        EngineJob job = current.getJob();
        job.addCallback(cb);
        return new LoadStatus(cb, job);
    }

    ResourceRunner<Z> runner = factory.build(key, width, height, cacheDecoder, fetcher, decoder, transformation,
            encoder, priority, this);
    runner.getJob().addCallback(cb);
    runners.put(key, runner);
    runner.queue();
    return new LoadStatus(cb, runner.getJob());
}

可能是作者在後面優化添加這個弱引用緩存的時候就順手放到了原有邏輯的後面。

其實仔細想想內存緩存的架構,我也覺得這個順序其實並不重要,誰先誰後都不會有什麼問題,無非是說流程是從lru cache拿出來放到弱引用緩存的,查詢的時候先查弱引用緩存會比較符合一般人的思路。

我們回想下兩個緩存存放的資源,簡化到Activity的場景。弱引用緩存放的都是當前activity正在使用的圖片,lru cache是activity退出之後回收的圖片。如果先查弱引用緩存,意味着當上下不停滑動recyclerview的時候效率高一丟丟。如果先查lru cahe,意味着反覆進出同一個activity的時候效率高一丟丟。很難說哪個順序性能比較高。而且這一丟丟性能在現代設備中的確真的是毫無影響,所以讓人好理解是最重要的,先查弱引用緩存沒毛病。

這篇的篇幅也差不多了,下一篇我們繼續講資源的加載部分

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