美文网首页
Glide缓存机制浅析

Glide缓存机制浅析

作者: leenpong | 来源:发表于2018-11-25 16:46 被阅读0次

    前言

    由于最近项目加载图片这一块用到了Glide框架,以前也用到过,其最核心的部分应该是属于缓存的机制,
    最近花了点时间分析其缓存原理

    最简单的Glide的用法:

        Glide.with(context)  .asBitmap()  .load(imageUrl) .into(imageView);
    

    最终都是要调用into方法:

    RequestBuilder : 
    
        return into(
            glideContext.buildImageViewTarget(view, transcodeClass),
            /*targetListener=*/ null,
            requestOptions);
      }
    
    该方法最终会返回一个ThumbnailRequestCoordinator对象,coordinator.setRequests(fullRequest, thumbRequest);从这个可以看出最终是会根据Option返回两个request
    
    这Request是一个SingleRequest对象 
    SingleRequest.obtain(....);
    
    
    

    这边直接跳到SingleRequest的begin方法:

      @Override
      public void begin() {
        assertNotCallingCallbacks();
        stateVerifier.throwIfRecycled();
        startTime = LogTime.getLogTime();
        if (model == null) {
          if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            width = overrideWidth;
            height = overrideHeight;
          }
          // Only log at more verbose log levels if the user has set a fallback drawable, because
          // fallback Drawables indicate the user expects null models occasionally.
          int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
          onLoadFailed(new GlideException("Received null model"), logLevel);
          return;
        }
    
        if (status == Status.RUNNING) {
          throw new IllegalArgumentException("Cannot restart a running request");
        }
    
        // If we're restarted after we're complete (usually via something like a notifyDataSetChanged
        // that starts an identical request into the same Target or View), we can simply use the
        // resource and size we retrieved the last time around and skip obtaining a new size, starting a
        // new load etc. This does mean that users who want to restart a load because they expect that
        // the view size has changed will need to explicitly clear the View or Target before starting
        // the new load.
        if (status == Status.COMPLETE) {
          onResourceReady(resource, DataSource.MEMORY_CACHE);
          return;
        }
    
        // Restarts for requests that are neither complete nor running can be treated as new requests
        // and can run again from the beginning.
    
         //代码1
        status = Status.WAITING_FOR_SIZE;
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
          onSizeReady(overrideWidth, overrideHeight);
        } else {
          target.getSize(this);
        }
    
    
        if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
            && canNotifyStatusChanged()) {
          target.onLoadStarted(getPlaceholderDrawable());
        }
        if (IS_VERBOSE_LOGGABLE) {
          logV("finished run method in " + LogTime.getElapsedMillis(startTime));
        }
      }
    
    

    其中重要的是代码1

         //代码1
        if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
          onSizeReady(overrideWidth, overrideHeight);
        } else {
          target.getSize(this);
        }
    

    这个地方需要注意的就是这个地方 的overrideWidth和overrideHeight,因为这两个宽高会用于生成缓存的key的参数,从这里可以看出,Glide的缓存是根据给定的宽高或者view的宽高缓存当前的图片,这个是Picaso等图片加载框架不同的地方。

      target.getSize(this);
      如果没有设定特定的宽高,会去拿当前view的宽高
    
    
    void getSize(@NonNull SizeReadyCallback cb) {
          int currentWidth = getTargetWidth();
          int currentHeight = getTargetHeight();
          if (isViewStateAndSizeValid(currentWidth, currentHeight)) {
            cb.onSizeReady(currentWidth, currentHeight);
            return;
          }
    
          // We want to notify callbacks in the order they were added and we only expect one or two
          // callbacks to be added a time, so a List is a reasonable choice.
          if (!cbs.contains(cb)) {
            cbs.add(cb);
          }
          if (layoutListener == null) {
            ViewTreeObserver observer = view.getViewTreeObserver();
            layoutListener = new SizeDeterminerLayoutListener(this);
            observer.addOnPreDrawListener(layoutListener);
          }
    
    

    上面的方法最终会调用到onSizeReady(SingleRequest):

    public void onSizeReady(int width, int height) {
        stateVerifier.throwIfRecycled();
        if (IS_VERBOSE_LOGGABLE) {
          logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
        if (status != Status.WAITING_FOR_SIZE) {
          return;
        }
        status = Status.RUNNING;
    
        float sizeMultiplier = requestOptions.getSizeMultiplier();
        this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
        this.height = maybeApplySizeMultiplier(height, sizeMultiplier);
    
        if (IS_VERBOSE_LOGGABLE) {
          logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
        }
        loadStatus = engine.load(
            glideContext,
            model,
            requestOptions.getSignature(),
            this.width,
            this.height,
            requestOptions.getResourceClass(),
            transcodeClass,
            priority,
            requestOptions.getDiskCacheStrategy(),
            requestOptions.getTransformations(),
            requestOptions.isTransformationRequired(),
            requestOptions.isScaleOnlyOrNoTransform(),
            requestOptions.getOptions(),
            requestOptions.isMemoryCacheable(),
            requestOptions.getUseUnlimitedSourceGeneratorsPool(),
            requestOptions.getUseAnimationPool(),
            requestOptions.getOnlyRetrieveFromCache(),
            this);
    
        // This is a hack that's only useful for testing right now where loads complete synchronously
        // even though under any executor running on any thread but the main thread, the load would
        // have completed asynchronously.
        if (status != Status.RUNNING) {
          loadStatus = null;
        }
        if (IS_VERBOSE_LOGGABLE) {
          logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
        }
      }
    

    其中调用Engine的load方法,才是真正去拿图片的过程。
    备注:严格来讲,Glide的缓存方式是三级缓存的

    engine.load :
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
            resourceClass, transcodeClass, options);
    
        //一级缓存,用一个虚引用数组保存数据
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
          cb.onResourceReady(active, DataSource.MEMORY_CACHE);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Loaded resource from active resources", startTime, key);
          }
          return null;
        }
    
        //二级缓存,用LruCahce做内存缓存处理
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
          cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Loaded resource from cache", startTime, key);
          }
          return null;
        }
    
        //下面的去decode数据的时候,还会更加相应的key去硬盘缓存拿数据(DiskLruCache)
        EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
        if (current != null) {
          current.addCallback(cb);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Added to existing load", startTime, key);
          }
          return new LoadStatus(cb, current);
        }
    
        EngineJob<R> engineJob =
            engineJobFactory.build(
                key,
                isMemoryCacheable,
                useUnlimitedSourceExecutorPool,
                useAnimationPool,
                onlyRetrieveFromCache);
    
        DecodeJob<R> decodeJob =
            decodeJobFactory.build(
                glideContext,
                model,
                key,
                signature,
                width,
                height,
                resourceClass,
                transcodeClass,
                priority,
                diskCacheStrategy,
                transformations,
                isTransformationRequired,
                isScaleOnlyOrNoTransform,
                onlyRetrieveFromCache,
                options,
                engineJob);
    
        jobs.put(key, engineJob);
    
        engineJob.addCallback(cb);
        engineJob.start(decodeJob);
    
        if (VERBOSE_IS_LOGGABLE) {
          logWithTimeAndKey("Started new load", startTime, key);
        }
        return new LoadStatus(cb, engineJob);
      }
    
    

    先分析一下用去作为换成的的生成:

    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
            resourceClass, transcodeClass, options);
    
    通过相应的参数生成一个EngineKey ,里面对hashCode和equals分别做了重写,也就是说,如果一张
    图片的宽高,图片路劲等参数不变,但是做了旋转或者其他不改变参数的操作时,
    拿到的会将是缓存的图片,这个时候往往是定义新的singature,让在路径不变的情况下,也可以改变加载新的图片
    
    

    一级缓存:

      @Nullable
      private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
          return null;
        }
        EngineResource<?> active = activeResources.get(key);
        if (active != null) {
          active.acquire();
        }
    
        return active;
      }
    其中activeResources是一个ActiveResources对象,里面维护了一Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();这样的HashMap,用ResourceWeakReference弱应用保存
    当前的资源,如果拿到直接调用cb.onResourceReady(active, DataSource.MEMORY_CACHE)
    

    如何在弱引用缓存(一级缓存)拿不到数据的时候,拿将会从二级缓存(LruCache)拿数据,并且将数据再
    放到弱引用缓存(一级缓存)

    二级缓存(LruCache)

        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
          cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
          if (VERBOSE_IS_LOGGABLE) {
            logWithTimeAndKey("Loaded resource from cache", startTime, key);
          }
          return null;
        }
    
      private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
        if (!isMemoryCacheable) {
          return null;
        }
    
        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) {
          // Save an object allocation if we've cached an EngineResource (the typical case).
          result = (EngineResource<?>) cached;
        } else {
          result = new EngineResource<>(cached, true /*isMemoryCacheable*/, true /*isRecyclable*/);
        }
        return result;
      }
    
      private final MemoryCache cache 这个cache对象其实就是一个LruResourceCache对象。
    从内存换成拿到资源后,放入一级缓存
        if (cached != null) {
          cached.acquire();
          activeResources.activate(key, cached);
        }
    
      void activate(Key key, EngineResource<?> resource) {
        ResourceWeakReference toPut =
            new ResourceWeakReference(
                key,
                resource,
                getReferenceQueue(),
                isActiveResourceRetentionAllowed);
    
        ResourceWeakReference removed = activeEngineResources.put(key, toPut);
        if (removed != null) {
          removed.reset();
        }
      }
    

    如果从上面一级缓存(弱引用)二级缓存(LruCache)拿不到数据,会去硬盘缓存(DiskLruCache)拿数据
    直接跳转到DecodeJob方法里面的run方法:

    三级缓存

      @Override
      public void run() {
        DataFetcher<?> localFetcher = currentFetcher;
        try {
          if (isCancelled) {
            notifyFailed();
            return;
          }
          runWrapped();
        } catch (Throwable t) {
         
        } finally {
      
        }
      }
    
    
      private void runWrapped() {
        switch (runReason) {
          case INITIALIZE:
            stage = getNextStage(Stage.INITIALIZE);
            currentGenerator = getNextGenerator();
            runGenerators();
            break;
          case SWITCH_TO_SOURCE_SERVICE:
            runGenerators();
            break;
          case DECODE_DATA:
            decodeFromRetrievedData();
            break;
          default:
            throw new IllegalStateException("Unrecognized run reason: " + runReason);
        }
      }
    
    private DataFetcherGenerator getNextGenerator() {
        switch (stage) {
          case RESOURCE_CACHE:
            return new ResourceCacheGenerator(decodeHelper, this);
          case DATA_CACHE:
            return new DataCacheGenerator(decodeHelper, this);
          case SOURCE:
            return new SourceGenerator(decodeHelper, this);
          case FINISHED:
            return null;
          default:
            throw new IllegalStateException("Unrecognized stage: " + stage);
        }
      }
    
    
      private void runGenerators() {
        currentThread = Thread.currentThread();
        startFetchTime = LogTime.getLogTime();
        boolean isStarted = false;
        while (!isCancelled && currentGenerator != null
            && !(isStarted = currentGenerator.startNext())) {
          stage = getNextStage(stage);
          currentGenerator = getNextGenerator();
    
          if (stage == Stage.SOURCE) {
            reschedule();
            return;
          }
        }
        // We've run out of stages and generators, give up.
        if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
          notifyFailed();
        }
    
        // Otherwise a generator started a new load and we expect to be called back in
        // onDataFetcherReady.
      }
    
    

    从上面代码可以知道,最重要的方法其实是runGenerators,另外两个方法其实就是最终拿到要使用的
    DataFetcherGenerator。
    其中currentGenerator.startNext():
    分析DataCacheGenerator里面的startNext:

      @Override
      public boolean startNext() {
        while (modelLoaders == null || !hasNextModelLoader()) {
          sourceIdIndex++;
          if (sourceIdIndex >= cacheKeys.size()) {
            return false;
          }
    
          Key sourceId = cacheKeys.get(sourceIdIndex);
          // PMD.AvoidInstantiatingObjectsInLoops The loop iterates a limited number of times
          // and the actions it performs are much more expensive than a single allocation.
          @SuppressWarnings("PMD.AvoidInstantiatingObjectsInLoops")
          Key originalKey = new DataCacheKey(sourceId, helper.getSignature());//(三级缓存)
          cacheFile = helper.getDiskCache().get(originalKey);
          if (cacheFile != null) {
            this.sourceKey = sourceId;
            modelLoaders = helper.getModelLoaders(cacheFile);
            modelLoaderIndex = 0;
          }
        }
    
        loadData = null;
        boolean started = false;
        while (!started && hasNextModelLoader()) {
          ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
          loadData =
              modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(),
                  helper.getOptions());
          if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
            started = true;
            loadData.fetcher.loadData(helper.getPriority(), this);
          }
        }
        return started;
      }
    
    
    
    cacheFile = helper.getDiskCache().get(originalKey); 这个代码就是去硬盘缓存(三级缓存)看能否拿到对应的数据,
    这里通过生成一个新的key去cache里面拿数据.
    
    
      DiskCache getDiskCache() {
        return diskCacheProvider.getDiskCache();
      }
    
    private static class LazyDiskCacheProvider implements DecodeJob.DiskCacheProvider {
    
        private final DiskCache.Factory factory;
        private volatile DiskCache diskCache;
    
        LazyDiskCacheProvider(DiskCache.Factory factory) {
          this.factory = factory;
        }
    
        @VisibleForTesting
        synchronized void clearDiskCacheIfCreated() {
          if (diskCache == null) {
            return;
          }
          diskCache.clear();
        }
    
        @Override
        public DiskCache getDiskCache() {
          if (diskCache == null) {
            synchronized (this) {
              if (diskCache == null) {
                diskCache = factory.build();
              }
              if (diskCache == null) {
                diskCache = new DiskCacheAdapter();
              }
            }
          }
          return diskCache;
        }
      }
    
      其中factory正是初始化Glide对象生成的
        if (diskCacheFactory == null) {
          diskCacheFactory = new InternalCacheDiskCacheFactory(context);
        }
    
      @Override
      public DiskCache build() {
        File cacheDir = cacheDirectoryGetter.getCacheDirectory();
    
        if (cacheDir == null) {
          return null;
        }
    
        if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {
          return null;
        }
    
        return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);
      }
    }
    
    从上面可以知道  cacheFile = helper.getDiskCache().get(originalKey);其实是从
    DiskLruCacheWrapper拿到的缓存,而DiskLruCacheWrapper正是对DiskLruCache的一个包装类
    
    

    分析到上面,知道这个时候已经是从三级缓存拿数据,如果没拿到就去decode相应的数据?
    但是这还有一个非常值得注意的地方就是去拿三级缓存数据用到的key,先看一下key的源码:

    public interface Key {
      String STRING_CHARSET_NAME = "UTF-8";
      Charset CHARSET = Charset.forName(STRING_CHARSET_NAME);
    
      /**
       * Adds all uniquely identifying information to the given digest.
       *
       * <p> Note - Using {@link java.security.MessageDigest#reset()} inside of this method will result
       * in undefined behavior. </p>
       */
      void updateDiskCacheKey(@NonNull MessageDigest messageDigest);
    
      /**
       * For caching to work correctly, implementations <em>must</em> implement this method and
       * {@link #hashCode()}.
       */
      @Override
      boolean equals(Object o);
    
      /**
       * For caching to work correctly, implementations <em>must</em> implement this method and
       * {@link #equals(Object)}.
       */
      @Override
      int hashCode();
    }
    
    

    从上面发现了一个方法:

     void updateDiskCacheKey(@NonNull MessageDigest messageDigest);
    

    用于在一级缓存和二级缓存拿数据的key, 都是通过key去HashMap数据源拿数据,而HashMa判断一个key是否是同一个,只是用到hashCodeequal是否相同,所以并没有用到上面那个方法,而三级缓存就在获取一个key的时候,比较特殊,会用到上面那个方法去拿到相应的key,所以如果一个图片在一级缓存,二级缓存都没有,但是在三级缓存有,是通过 updateDiskCacheKey去生成key的.
    看一下如何用updateDiskCacheKey这个方法的

    @Override
      public File get(Key key) {
        String safeKey = safeKeyGenerator.getSafeKey(key);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
          Log.v(TAG, "Get: Obtained: " + safeKey + " for for Key: " + key);
        }
        File result = null;
        try {
          // It is possible that the there will be a put in between these two gets. If so that shouldn't
          // be a problem because we will always put the same value at the same key so our input streams
          // will still represent the same data.
          final DiskLruCache.Value value = getDiskCache().get(safeKey);
          if (value != null) {
            result = value.getFile(0);
          }
        } catch (IOException e) {
          if (Log.isLoggable(TAG, Log.WARN)) {
            Log.w(TAG, "Unable to get from disk cache", e);
          }
        }
        return result;
      }
    
      String safeKey = safeKeyGenerator.getSafeKey(key);生成对应的String类型的key
    
      public String getSafeKey(Key key) {
        String safeKey;
        synchronized (loadIdToSafeHash) {
          safeKey = loadIdToSafeHash.get(key);
        }
        if (safeKey == null) {
          safeKey = calculateHexStringDigest(key);
        }
        synchronized (loadIdToSafeHash) {
          loadIdToSafeHash.put(key, safeKey);
        }
        return safeKey;
      }
    
      private String calculateHexStringDigest(Key key) {
        PoolableDigestContainer container = Preconditions.checkNotNull(digestPool.acquire());
        try {
          key.updateDiskCacheKey(container.messageDigest);//生成相应的key
          // calling digest() will automatically reset()
          return Util.sha256BytesToHex(container.messageDigest.digest());
        } finally {
          digestPool.release(container);
        }
      }
    
    从上面可以知道,是通过key.updateDiskCacheKey(container.messageDigest)去更新messageDigest
    然后在将messageDigest里面的数据digest()返回
    

    如果你想修改一个宽高,路径一样的图片,但是又在一级和二级缓存里面没有的图片,如果没有实现
    updateDiskCacheKey该方法根据相应的数据更新key,也可能会拿到三级缓存的图片而返回不是想要的
    图片

    上面就是分析完从三级缓存拿数据

    以上如有说的不对的地方,请指正
    TXH

    相关文章

      网友评论

          本文标题:Glide缓存机制浅析

          本文链接:https://www.haomeiwen.com/subject/evspqqtx.html