Android Glide源码剖析系列(四)缓存机制及其原理


Glide源码剖析系列

  • Android Glide源码剖析系列(一)图片加载请求如何感知组件生命周期
  • Android Glide源码剖析系列(二)Glide如何管理图片加载请求
  • Android Glide源码剖析系列(三)深入理解Glide图片加载流程
  • Android Glide源码剖析系列(四)缓存机制及其原理

为什么选择Glide?

  • 多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
  • 生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)Glide可以感知调用页面的生命周期,这就是优势
  • 高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
  • 高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)

小结:支持图片格式多;Bitmap复用和主动回收;生命周期感应;优秀的缓存策略;加载速度快(Bitmap默认格式RGB565)

Glide简单使用

Glide.with(this)
        .load("https://t7.baidu.com/it/u=3779234486,1094031034&fm=193&f=GIF")
        .error(R.drawable.aaa)
        .placeholder(R.drawable.ic_android_black_24dp)
        .fallback(R.drawable.aaa)
        .diskCacheStrategy(DiskCacheStrategy.ALL)
        .skipMemoryCache(false)
        .into(imageView);

阅读这篇文章之前,如果对图片加载流程不熟悉,强烈建议先阅读并理解Android Glide源码解析系列(三)深入理解Glide图片加载流程,因为Glide图片缓存的读取和保存都是在加载过程中完成的。

上文提到过,Glide在加载图片的时候,有对图片进行缓存处理,分别是内存缓存和磁盘缓存。这两级缓存的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用从网络或其他地方重复获取图片资源。

缓存key

Glide图片缓存的读取和保存都需要用key标识,通过上篇文章我们知道key在Engine#load()方法中创建:

  public  LoadStatus load(
      GlideContext glideContext,
      Object model, Key signature, int width, int height, Class resourceClass, Class transcodeClass,
      Priority priority, DiskCacheStrategy diskCacheStrategy,
      Map, Transformation> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb,
      Executor callbackExecutor) {
    long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;

    EngineKey key =
        keyFactory.buildKey(
            model,  //图片地址
            signature,  //签名信息
            width, height, //图片宽
            transformations,
            resourceClass,
            transcodeClass,
            options);  //配置信息

    ……

    return null;
  }

使用EngineKeyFactory构建EngineKey对象,这就是最终用于图片缓存的key。我们可以看到key的值由model、signature、width、height等多个参数共同决定,所以改变任何一个参数都会产生一个新的缓存key。

准备好了key,就可以开始缓存工作了。

内存缓存

内存缓存就是把已经加载到内存中的图片缓存起来,当下次需要展示这张的时候直接从内存中读取,这样不仅可以减少重新获取图片(例如网络下载、从磁盘读取)造成的资源浪费,也避免重复将图片载入到内存中。这样就大大提高了图片加载的速度。

例如在列表中加载大量网络图片时,已经加载过一次的图片可以直接从内存中读取并展示出来,这样就大大提升了App性能,用户使用起来当然更加顺滑。

Glide默认为我们开启了内存缓存,虽然大部分场景下开启内存缓存是更好的选择,但是如果在特殊情况下不需要开启内存缓存,Glide也为我们提供了接口来关闭内存缓存功能。

Glide.with(this)
        .load("https://t7.baidu.com/it/u=3779234486,1094031034&fm=193&f=GIF")
        .skipMemoryCache(true)  //true表示关闭内存缓存
        .into(imageView);

Glide内存缓存的实现采取的是方案是:LruCache算法+弱引用机制。

LruCache算法(Least Recently Used):也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到阈值之前从内存中移除。

关于LruCache算法可以参考Android 缓存策略之LruCache

除了使用LruCache算法缓存图片之外,Glide还会将正在使用的图片对象弱引用保存到HashMap中,具体实现类是ActiveResources,后面会介绍这个类。

继续分析内存缓存,Glide读取内存缓存入口同样是在Engine#load()方法中:

  public  LoadStatus load(
      GlideContext glideContext,
      Object model,
      Key signature,
      int width,
      int height,
      Class resourceClass,
      Class transcodeClass,
      Priority priority,
      DiskCacheStrategy diskCacheStrategy,
      Map, Transformation> transformations,
      boolean isTransformationRequired,
      boolean isScaleOnlyOrNoTransform,
      Options options,
      boolean isMemoryCacheable,
      boolean useUnlimitedSourceExecutorPool,
      boolean useAnimationPool,
      boolean onlyRetrieveFromCache,
      ResourceCallback cb,
      Executor callbackExecutor) {
    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(
            glideContext,
            model,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            options,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache,
            cb,
            callbackExecutor,
            key,
            startTime);
      }
    }

    // Avoid calling back while holding the engine lock, doing so makes it easier for callers to
    // deadlock.
    cb.onResourceReady(
        memoryResource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
    return null;
  }

通过这段代码可以得出两个结论

  • 当Glide加载图片时,首先会从内存缓存中去查找,如果内存中有符合条件的图片缓存,就不需要重新把图片加载进内存中了。
  • 内存缓存的优先级是高于磁盘缓存的。

继续分析loadFromMemory(key, isMemoryCacheable, startTime)方法:

  private EngineResource loadFromMemory(
      EngineKey key, boolean isMemoryCacheable, long startTime) {
    if (!isMemoryCacheable) {  //1
      return null;
    }

    EngineResource active = loadFromActiveResources(key);  //2
    if (active != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return active;
    }

    EngineResource cached = loadFromCache(key);  //3
    if (cached != null) {
      if (VERBOSE_IS_LOGGABLE) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return cached;
    }

    return null;
  }

  @Nullable
  private EngineResource loadFromActiveResources(Key key) {
    EngineResource active = activeResources.get(key);
    if (active != null) {
      active.acquire();
    }

    return active;
  }

  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) {
      // Save an object allocation if we've cached an EngineResource (the typical case).
      result = (EngineResource) cached;
    } else {
      result =
          new EngineResource<>(
              cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
    }
    return result;
  }
  1. 判断是否开启了内存缓存功能,默认开启,可以在RequestOption中关闭内存缓存
  2. 正在使用的图片缓存activeResources中查找图片资源,找到直接返回,否则继续在内存缓存中查找
  3. 如果内存缓存中查找到图片资源,将图片从cache中移除,并且添加到正在使用的图片缓存activeResources

我们知道内存缓存使用的数据结构是LinkedHashMap,那么正在使用的图片缓存是如何保存的?答案或许就藏在ActiveResources类。

final class ActiveResources {
  @VisibleForTesting final Map activeEngineResources = new HashMap<>();

  synchronized void activate(Key key, EngineResource resource) {
    ResourceWeakReference toPut =
        new ResourceWeakReference(
            key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);

    ResourceWeakReference removed = activeEngineResources.put(key, toPut);
    if (removed != null) {
      removed.reset();
    }
  }

  @Nullable
  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;
  }
}

把正在使用的图片对象的弱引用添加到HashMap:activeEngineResources中,使用弱引用是因为持有图片对象的Activity或Fragment有可能会被销毁,这样做可以及时清除缓存并释放内存,防止内存泄漏。

内存缓存功能小结:

  • 读取缓存的顺序是:正在使用的图片缓存 > 内存缓存 > 磁盘缓存
  • 缓存正在使用的图片采取HashMap+弱引用,而内存缓存使用LinkedHashMap

说了这么多好像都是内存缓存的读取过程,那么内存缓存是在哪里保存的呢?上篇文章分析图片加载流程的时候,我们知道DecodeJob在成功获取到图片资源后,会调用notifyEncodeAndRelease()方法处理资源,其中有一项工作就是把图片写入到缓存中。

#DecodeJob.java
  private void notifyEncodeAndRelease(
      Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
    GlideTrace.beginSection("DecodeJob.notifyEncodeAndRelease");
    try {
      if (resource instanceof Initializable) {
        ((Initializable) resource).initialize();  //准备显示资源
      }

      Resource result = resource;

      notifyComplete(result, dataSource, isLoadedFromAlternateCacheKey);  //资源获取成功

  }

继续分析notifyComplete()方法

  private void notifyComplete(
      Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
    setNotifiedOrThrow();
    callback.onResourceReady(resource, dataSource, isLoadedFromAlternateCacheKey);
  }

callback.onResourceReady()方法在EngineJob中实现。继续追踪代码:

#EngineJob.java
  @Override
  public void onResourceReady(
      Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
    synchronized (this) {
      this.resource = resource;
      this.dataSource = dataSource;
      this.isLoadedFromAlternateCacheKey = isLoadedFromAlternateCacheKey;
    }
    notifyCallbacksOfResult();
  }

  void notifyCallbacksOfResult() {
    ResourceCallbacksAndExecutors copy;
    Key localKey;
    EngineResource localResource;
    synchronized (this) {
      //我们省略了异常情况的处理代码和一些注释代码

      engineResource = engineResourceFactory.build(resource, isCacheable, key, resourceListener); //1
     
      hasResource = true;
      copy = cbs.copy();  //2
      incrementPendingCallbacks(copy.size() + 1);

      localKey = key;
      localResource = engineResource;
    }

    engineJobListener.onEngineJobComplete(this, localKey, localResource);  //3

    for (final ResourceCallbackAndExecutor entry : copy) {
      entry.executor.execute(new CallResourceReady(entry.cb));  //4
    }
    decrementPendingCallbacks();
  }
  1. engineResource 是资源的一个包装类,负责计算资源被引用的次数,次数为0的时候可以回收资源
  2. copy内部包装的是Executors.mainThreadExecutor()主线程池,方便切换到主线程
  3. EngineJob执行完毕,把获取的图片加入activeResources并且从EngineJob缓存列表移除当前job
  @Override
  public synchronized void onEngineJobComplete(
      EngineJob engineJob, Key key, EngineResource resource) {
    // A null resource indicates that the load failed, usually due to an exception.
    if (resource != null && resource.isMemoryCacheable()) {
      activeResources.activate(key, resource);
    }

    jobs.removeIfCurrent(key, engineJob);
  }
  1. 把任务切换到主线程执行,也就是说之前的操作都是在子线程中处理
    @Override
    public void run() {
      // Make sure we always acquire the request lock, then the EngineJob lock to avoid deadlock
      // (b/136032534).
      synchronized (cb.getLock()) {
        synchronized (EngineJob.this) {
          if (cbs.contains(cb)) {
            // Acquire for this particular callback.
            engineResource.acquire();  //增加资源引用次数
            callCallbackOnResourceReady(cb); 
            removeCallback(cb);
          }
          decrementPendingCallbacks();
        }
      }
    }

  @GuardedBy("this")
  void callCallbackOnResourceReady(ResourceCallback cb) {
    try {
      // This is overly broad, some Glide code is actually called here, but it's much
      // simpler to encapsulate here than to do so at the actual call point in the
      // Request implementation.
      cb.onResourceReady(engineResource, dataSource, isLoadedFromAlternateCacheKey);  //在SingleRequest中实现
    } catch (Throwable t) {
      throw new CallbackException(t);
    }
  }

图片准备完毕,cb.onResourceReady()回调方法在SingleRequest中实现

#SingleRequest.java
  @Override
  public void onResourceReady(
      Resource resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
    stateVerifier.throwIfRecycled();
    Resource toRelease = null;
    try {
      synchronized (requestLock) {
        loadStatus = null;
        if (resource == null) {
          GlideException exception =
              new GlideException(
                  "Expected to receive a Resource with an "
                      + "object of "
                      + transcodeClass
                      + " inside, but instead got null.");
          onLoadFailed(exception);
          return;
        }

        Object received = resource.get();
        if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) {
          toRelease = resource;
          this.resource = null;
          GlideException exception =
              new GlideException(
                  "Expected to receive an object of "
                      + transcodeClass
                      + " but instead"
                      + " got "
                      + (received != null ? received.getClass() : "")
                      + "{"
                      + received
                      + "} inside"
                      + " "
                      + "Resource{"
                      + resource
                      + "}."
                      + (received != null
                          ? ""
                          : " "
                              + "To indicate failure return a null Resource "
                              + "object, rather than a Resource object containing null data."));
          onLoadFailed(exception);
          return;
        }

        if (!canSetResource()) {
          toRelease = resource;
          this.resource = null;
          // We can't put the status to complete before asking canSetResource().
          status = Status.COMPLETE;
          GlideTrace.endSectionAsync(TAG, cookie);
          return;
        }

        onResourceReady(
            (Resource) resource, (R) received, dataSource, isLoadedFromAlternateCacheKey);
      }
    } finally {
      if (toRelease != null) {
        engine.release(toRelease);
      }
    }
  }

finally语句中,如果图片资源被标记为需要释放,会调用engine.release(toRelease)释放图片资源

#Engine.java
  public void release(Resource resource) {
    if (resource instanceof EngineResource) {
      ((EngineResource) resource).release();
    } else {
      throw new IllegalArgumentException("Cannot release anything but an EngineResource");
    }
  }

调用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);
    }
  }

acquired变量用于计算图片资源被引用的次数,acquired==0表示该图片资源可以被回收,在Engine的onResourceReleased()方法中实现资源回收

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

首先从activeResources中移除图片缓存,同时将图片添加到内存缓存中。

内存缓存的读取与添加原理已经分析完毕,概括起来就是:

正在使用的图片资源以弱引用的方式缓存到activeResources中,不在使用中的图片以强引用的方式缓存到LruCache中。
加载图片时首先在activeResources中查找资源,然后才在LruCache缓存中查找。

磁盘缓存

磁盘缓存是将图片存储到设备本地存储空间,下次使用图片可以直接从本地读取缓存文件,这样可以防止应用从网络上重复下载和读取图片。

Glide同样为我们提供了磁盘缓存的配置接口

Glide.with(this)
        .load("https://t7.baidu.com/it/u=3779234486,1094031034&fm=193&f=GIF")
        .diskCacheStrategy(DiskCacheStrategy.NONE)
        .into(imageView);

Glide支持多种磁盘缓存策略:

  • NONE:禁用磁盘缓存
  • DATA:缓存decode操作之前图片资源
  • RESOURCE:缓存decode之后的图片资源
  • AUTOMATIC:根据DataFetcher和EncodeStrategy自动调整缓存策略
  • ALL:远程图片同时支持RESOURCE和DATA,本地图片只支持RESOURCE

通过上篇文章我们知道,如果在内存缓存中没有找到图片,就会开启一个新线程DecodeJob从磁盘缓存读取图片,磁盘缓存读取失败才真正开始加载model指定的图片。

#DataCacheGenerator.java
  @Override
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      try {
        boolean isDataInCache = cacheData(data); //添加磁盘缓存
        if (!isDataInCache) {
          return true;
        }
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to properly rewind or write data to cache", e);
        }
      }
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      //加载model指定的图片
    }
    return started;
  }

加载图片成功之后需要将图片加入到磁盘缓存中,dataToCache 表示的是待缓存的图片资源,调用cacheData()方法把图片添加到磁盘缓存:

  private boolean cacheData(Object dataToCache) throws IOException {
    long startTime = LogTime.getLogTime();
    boolean isLoadingFromSourceData = false;
    try {
      DataRewinder rewinder = helper.getRewinder(dataToCache);
      Object data = rewinder.rewindAndGet();
      Encoder encoder = helper.getSourceEncoder(data);
      DataCacheWriter writer = new DataCacheWriter<>(encoder, data, helper.getOptions());
      DataCacheKey newOriginalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
      DiskCache diskCache = helper.getDiskCache();  //1
      diskCache.put(newOriginalKey, writer);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(
            TAG,
            "Finished encoding source to cache"
                + ", key: "
                + newOriginalKey
                + ", data: "
                + dataToCache
                + ", encoder: "
                + encoder
                + ", duration: "
                + LogTime.getElapsedMillis(startTime));
      }

      if (diskCache.get(newOriginalKey) != null) {  //2
        originalKey = newOriginalKey;
        sourceCacheGenerator =
            new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
        // We were able to write the data to cache.
        return true;
      } else {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(
              TAG,
              "Attempt to write: "
                  + originalKey
                  + ", data: "
                  + dataToCache
                  + " to the disk"
                  + " cache failed, maybe the disk cache is disabled?"
                  + " Trying to decode the data directly...");
        }

        isLoadingFromSourceData = true;
        cb.onDataFetcherReady(
            loadData.sourceKey,
            rewinder.rewindAndGet(),
            loadData.fetcher,
            loadData.fetcher.getDataSource(),
            loadData.sourceKey);
      }
      // We failed to write the data to cache.
      return false;
    } finally {
      if (!isLoadingFromSourceData) {
        loadData.fetcher.cleanup();
      }
    }
  }

  1. 磁盘缓存是通过DiskLruCache来实现的,所以通过DecodeHelper获取DiskLruCache并把图片put进去
  2. diskCache.get(newOriginalKey):在磁盘缓存中读取newOriginalKey对应的图片:如果读取成功说明图片成功缓存到磁盘,初始化DataCacheGenerator(为读取磁盘缓存埋下伏笔)并且返回true;如果读取失败说明图片没有缓存到磁盘中,直接解析图片资源并且返回false

磁盘缓存的添加过程为磁盘缓存的读取埋下了一个重要的伏笔,也就是DataCacheGenerator。我们重新回到startNext()方法:

#DataCacheGenerator.java
  @Override
  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      try {
        boolean isDataInCache = cacheData(data); //添加磁盘缓存
        if (!isDataInCache) {
          return true;
        }
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "Failed to properly rewind or write data to cache", e);
        }
      }
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {  //注释1
      return true;
    }
    sourceCacheGenerator = null;

    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      //注释2:加载model指定的图片
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
              || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        startNextLoad(loadData);
      }
    }
    return started;
  }

图片成功添加到磁盘缓存,那么cacheData(data)方法返回true,并且sourceCacheGenerator初始化成功,执行注释1处代码:

#DataCacheGenerator.java
  @Override
  public boolean startNext() {
    GlideTrace.beginSection("DataCacheGenerator.startNext");
    try {
      while (modelLoaders == null || !hasNextModelLoader()) {
        sourceIdIndex++;
        if (sourceIdIndex >= cacheKeys.size()) {
          return false;
        }
        //注释3
        Key sourceId = cacheKeys.get(sourceIdIndex);
        @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 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;
    } finally {
      GlideTrace.endSection();
    }
  }

注释3:创建缓存key在DiskLruCache中查找缓存图片,如果找到了缓存图片就直接进入到缓存图片的加载流程;否则回到注释2处加载model指定的图片

磁盘缓存的添加和读取过程分析完毕。最后附上一张流程图:


你可能感兴趣的:(Android Glide源码剖析系列(四)缓存机制及其原理)