Glide 源码阅读笔记(一)

Glide 源码阅读笔记(一)

GlideAndroid 中的老牌网络图片加载库,虽然年纪比较大,但是它和很多新生代的网络图片加载库相比在各方面依然不落下风。它的友好的使用 API 、优秀的内存管理和对 Android 中特有组建生命周期处理等等优点依然是许多 Android 开发者目前会优先选择使用 Glide 的理由。同样 Glide 中的优秀设计在我们实际开发中也是有可以借鉴的地方,所以从本篇文章开始分析 Glide 的源码。

源码阅读基于 Glide 4.16.0 版本

简单使用

添加依赖

Kotlin 复制代码
// ...
// Glide 核心  
implementation("com.github.bumptech.glide:glide:4.16.0")  
// Glide OkHttp 支持(可选)  
implementation("com.github.bumptech.glide:okhttp4-integration:4.16.0")  
// Glide 配置注解处理器 (可选)  
kapt("com.github.bumptech.glide:compiler:4.16.0")
// ...

初始化

Kotlin 复制代码
// 自定义 OkHttp Client.
val appOkHttpClient: OkHttpClient by lazy {
    OkHttpClient.Builder()
        .addInterceptor {
            val request = it.request()
            Log.d("OkHttp", "Request Start: $request")
            val response = it.proceed(request)
            Log.d("OkHttp", "Request End: $request,Response Body Size: ${response.header("Content-Length")}")
            response
        }
        .build()
}
// 配置的 Module 类必须用注解 @GlideModule 修饰,同时需要继承于 AppGlideModule 类。
@GlideModule
class MyGlideModule : AppGlideModule() {
    
    // 通过 applyOptions() 方法,来修改 Glide 实例初始化时的配置,我下面的代码修改了 ArrayPool 中的缓存大小为 2MB
    override fun applyOptions(context: Context, builder: GlideBuilder) {
        super.applyOptions(context, builder)
        builder.setArrayPool(LruArrayPool(2 * 1024 * 1024))
        Log.d(TAG, "ApplyOptions: set custom array pool.")
    }
    
    // 替换原有的关键组件,我在下面就将原有的网络请求组件替换成 OkHttp,而且使用了我自定义的 OkHttpClient。
    override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
        super.registerComponents(context, glide, registry)
        registry.replace(
            GlideUrl::class.java,
            InputStream::class.java,
            OkHttpUrlLoader.Factory(appOkHttpClient)
        )
    }

    companion object {
        const val TAG = "MyGlideModule"
    }
}

如果要使上面的初始化代码生效,必须在添加依赖时添加注解处理器。

如果需要替换 Glide 的网络请求为 OkHttp 时,需要添加 OkHttp 的迁移依赖,如果需要使用自定义的 OkHttpClient,就需要做类似于上面代码中 registerComponents() 方法中的操作,如果不需要使用自定义的 OkHttpClient 就不需要重写 registerComponents() 方法,默认 Glide 会为我们创建一个默认的 OkHttpClient

简单加载图片

Kotlin 复制代码
// ...
Glide.with(this)
    .load("https://upload-images.jianshu.io/upload_images/5809200-a99419bb94924e6d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240")
    .into(findViewById<ImageView>(R.id.my_iv))  
// ...    

Glide 实例创建

通常我们要用 Glide 加载图片时,所调用的第一个方法是 Glide#with() 方法,我们以它作为第一个分析的方法:

Java 复制代码
  public static RequestManager with(@NonNull Context context) {
    return getRetriever(context).get(context);
  }
  
  private static RequestManagerRetriever getRetriever(@Nullable Context context) {
    // Context could be null for other reasons (ie the user passes in null), but in practice it will
    // only occur due to errors with the Fragment lifecycle.
    Preconditions.checkNotNull(context, DESTROYED_ACTIVITY_WARNING);
    return Glide.get(context).getRequestManagerRetriever();
  }

上面代码简单来说就是通过 Glide#get() 方法获取一个 Glide 实例,然后通过 Glide#getRequestManagerRetriever() 方法获取 RequestManagerRetriever 实例,最终通过 RequestManagerRetriever#get() 方法获取 RequestManager 实例,然后返回。

本节内容主要探索 Glide 的实例是怎么创建的,所以看看 Glide#get() 方法的实现:

Java 复制代码
  public static Glide get(@NonNull Context context) {
    // 经典 double check 创建单例对象
    if (glide == null) {
      // 获取注解生成器生成的 GeneratedAppGlideModuleImpl 对象的实例
      GeneratedAppGlideModule annotationGeneratedModule =
          getAnnotationGeneratedGlideModules(context.getApplicationContext());
      synchronized (Glide.class) {
        if (glide == null) {
          // 创建 Glide 实例
          checkAndInitializeGlide(context, annotationGeneratedModule);
        }
      }
    }

    return glide;
  }

Glide 中用到了很多的 double check 的方式创建单例对象,这里也是这样,首先会通过 getAnnotationGeneratedGlideModules() 方法来获取注解处理器生成的 GeneratedAppGlideModuleImpl 对象的实例,然后通过 checkAndInitializeGlide() 方法来创建 Glide 实例。

我们先看看 getAnnotationGeneratedGlideModules() 方法的实现:

Java 复制代码
  private static GeneratedAppGlideModule getAnnotationGeneratedGlideModules(Context context) {
    GeneratedAppGlideModule result = null;
    try {
      Class<GeneratedAppGlideModule> clazz =
          (Class<GeneratedAppGlideModule>)
              Class.forName("com.bumptech.glide.GeneratedAppGlideModuleImpl");
      result =
          clazz.getDeclaredConstructor(Context.class).newInstance(context.getApplicationContext());
    } catch (ClassNotFoundException e) {
      if (Log.isLoggable(TAG, Log.WARN)) {
        Log.w(
            TAG,
            "Failed to find GeneratedAppGlideModule. You should include an"
                + " annotationProcessor compile dependency on com.github.bumptech.glide:compiler"
                + " in your application and a @GlideModule annotated AppGlideModule implementation"
                + " or LibraryGlideModules will be silently ignored");
      }
      // These exceptions can't be squashed across all versions of Android.
    } catch (InstantiationException e) {
      throwIncorrectGlideModule(e);
    } catch (IllegalAccessException e) {
      throwIncorrectGlideModule(e);
    } catch (NoSuchMethodException e) {
      throwIncorrectGlideModule(e);
    } catch (InvocationTargetException e) {
      throwIncorrectGlideModule(e);
    }
    return result;
  }

上面的方法简单高效,直接通过反射的方式去创建 GeneratedAppGlideModuleImpl 实例,构造函数的参数是 ApplicationContext

接着看看 checkAndInitializeGlide() 方法的逻辑:

Java 复制代码
  @GuardedBy("Glide.class")
  @VisibleForTesting
  static void checkAndInitializeGlide(
      @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) {
    // In the thread running initGlide(), one or more classes may call Glide.get(context).
    // Without this check, those calls could trigger infinite recursion.
    if (isInitializing) {
      throw new IllegalStateException(
          "Glide has been called recursively, this is probably an internal library error!");
    }
    isInitializing = true;
    try {
      initializeGlide(context, generatedAppGlideModule);
    } finally {
      isInitializing = false;
    }
  }
  
    @GuardedBy("Glide.class")
  private static void initializeGlide(
      @NonNull Context context, @Nullable GeneratedAppGlideModule generatedAppGlideModule) {
    initializeGlide(context, new GlideBuilder(), generatedAppGlideModule);
  }

  @GuardedBy("Glide.class")
  @SuppressWarnings("deprecation")
  private static void initializeGlide(
      @NonNull Context context,
      @NonNull GlideBuilder builder,
      @Nullable GeneratedAppGlideModule annotationGeneratedModule) {
    Context applicationContext = context.getApplicationContext();
    List<GlideModule> manifestModules = Collections.emptyList();
    if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
      // 解析 Manifest 中的 GlideModule(该 API 已经废弃)
      manifestModules = new ManifestParser(applicationContext).parse();
    }


    // 过滤 Manifest 中被移除的 GlideModule
    if (annotationGeneratedModule != null
        && !annotationGeneratedModule.getExcludedModuleClasses().isEmpty()) {
      Set<Class<?>> excludedModuleClasses = annotationGeneratedModule.getExcludedModuleClasses();
      Iterator<GlideModule> iterator = manifestModules.iterator();
      while (iterator.hasNext()) {
        GlideModule current = iterator.next();
        if (!excludedModuleClasses.contains(current.getClass())) {
          continue;
        }
        if (Log.isLoggable(TAG, Log.DEBUG)) {
          Log.d(TAG, "AppGlideModule excludes manifest GlideModule: " + current);
        }
        iterator.remove();
      }
    }

    if (Log.isLoggable(TAG, Log.DEBUG)) {
      for (GlideModule glideModule : manifestModules) {
        Log.d(TAG, "Discovered GlideModule from manifest: " + glideModule.getClass());
      }
    }

    RequestManagerRetriever.RequestManagerFactory factory =
        annotationGeneratedModule != null
            ? annotationGeneratedModule.getRequestManagerFactory()
            : null;
    builder.setRequestManagerFactory(factory);
    // 将 GlideBuilder 传递给 Manifest 中的 Module.
    for (GlideModule module : manifestModules) {
      module.applyOptions(applicationContext, builder);
    }
    // 将 GlideBuilder 传递给注解生成器生成的 Module。
    if (annotationGeneratedModule != null) {
      annotationGeneratedModule.applyOptions(applicationContext, builder);
    }
    // 构建 Glide 实例
    Glide glide = builder.build(applicationContext, manifestModules, annotationGeneratedModule);
    // Glide 监听应用配置改变和低内存的状态
    applicationContext.registerComponentCallbacks(glide);
    Glide.glide = glide;
  }

这里简单解释一下 initializeGlide() 方法:

  1. 解析 Manifest 中的 Module 实例,该方法已经废弃,推荐使用我在 demo 中通过注解的方式来实现自定义配置。
  2. 通过注解生成的代码,来过滤 Manifest 中的 Module
  3. 将构建 GlideGlideBuilder 对象通过 applyOptins() 方法传递给每一个 Manifest 中的 Module 和注解生成的 Module,这里就完成了对 Glide 构建时的自定义配置。
  4. 最后通过 GlideBuilder#build() 方法完成对 Glide 实例的创建。
  5. 通过 ApplicationContext#registerComponentCallbacks() 监听应用配置改变和低内存的状态,然后通知给 Glide 实例。

Glide 中处理缓存的对象

我们继续看看 GlideBuilder#build() 方法的源码。

Java 复制代码
  @NonNull
  Glide build(
      @NonNull Context context,
      List<GlideModule> manifestModules,
      AppGlideModule annotationGeneratedGlideModule) {
    if (sourceExecutor == null) {
      sourceExecutor = GlideExecutor.newSourceExecutor();
    }

    if (diskCacheExecutor == null) {
      diskCacheExecutor = GlideExecutor.newDiskCacheExecutor();
    }

    if (animationExecutor == null) {
      animationExecutor = GlideExecutor.newAnimationExecutor();
    }

    if (memorySizeCalculator == null) {
      memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
    }

    if (connectivityMonitorFactory == null) {
      connectivityMonitorFactory = new DefaultConnectivityMonitorFactory();
    }

    if (bitmapPool == null) {
      int size = memorySizeCalculator.getBitmapPoolSize();
      if (size > 0) {
        bitmapPool = new LruBitmapPool(size);
      } else {
        bitmapPool = new BitmapPoolAdapter();
      }
    }

    if (arrayPool == null) {
      arrayPool = new LruArrayPool(memorySizeCalculator.getArrayPoolSizeInBytes());
    }

    if (memoryCache == null) {
      memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
    }

    if (diskCacheFactory == null) {
      diskCacheFactory = new InternalCacheDiskCacheFactory(context);
    }

    if (engine == null) {
      engine =
          new Engine(
              memoryCache,
              diskCacheFactory,
              diskCacheExecutor,
              sourceExecutor,
              GlideExecutor.newUnlimitedSourceExecutor(),
              animationExecutor,
              isActiveResourceRetentionAllowed);
    }

    if (defaultRequestListeners == null) {
      defaultRequestListeners = Collections.emptyList();
    } else {
      defaultRequestListeners = Collections.unmodifiableList(defaultRequestListeners);
    }

    GlideExperiments experiments = glideExperimentsBuilder.build();
    RequestManagerRetriever requestManagerRetriever =
        new RequestManagerRetriever(requestManagerFactory);

    return new Glide(
        context,
        engine,
        memoryCache,
        bitmapPool,
        arrayPool,
        requestManagerRetriever,
        connectivityMonitorFactory,
        logLevel,
        defaultRequestOptionsFactory,
        defaultTransitionOptions,
        defaultRequestListeners,
        manifestModules,
        annotationGeneratedGlideModule,
        experiments);
  }

这里解释一下上面代码中创建的对象:

  • SourceExecutor:通过 CPU 核心数来计算线程数,最大 4 个线程。
  • DiskCacheExecutor:线程数为 1。
  • AnimatorExecutor:线程数是 SourceExecutor 的一半。
  • MemorySizeCalculator:计算各种内存缓存的大小。
  • ConnectivityMonitorFactory:在有网络状态监听权限的情况下,监听网络状态。
  • BitmapPoolBitmap 的缓存,在 Android 8 及其以上版本缓存大小是 0;在 Android 8 以下版本缓存的大小是 4 倍屏幕大小图片所需要的内存。
  • ArrayPool:用来存储 int 数组和 byte 数组的缓存,它的大小是 4MB;如果是低内存设备内存大小是 2MB。
  • MemoryCacheResource 对象的缓存,在 Glide 中有很多的对象都是 Resource,它的缓存大小是 2 倍屏幕大小图片所需要的内存。
  • DiskCacheFactory:本地磁盘缓存处理。
  • Engine:请求的引擎
  • RequestManagerRetriever:用于查找对应 ContextRequestManager

本节内容只讨论和缓存(内存缓存和磁盘缓存)相关的对象,其他的相关的对象后续的文章还会继续讨论。

MemorySizeCalculator

计算各种内存缓存的大小,包括 BitmapPoolArrayPoolMemoryCache 等。

我们看看 MemorySizeCalculator 对象的构造函数怎么计算各种内存的大小的:

Java 复制代码
  MemorySizeCalculator(MemorySizeCalculator.Builder builder) {
    this.context = builder.context;
    // int 数组和 byte 数组内存缓存,低内存设备为 2MB,普通设备为 4MB
    arrayPoolSize =
        isLowMemoryDevice(builder.activityManager)
            ? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
            : builder.arrayPoolSizeBytes;
     // 最大的可用内存缓存,如果是低内存设备是最大堆内存乘以 0.33;如果是普通设备最大堆内存乘以 0.4。        
    int maxSize =
        getMaxSize(
            builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);

    int widthPixels = builder.screenDimensions.getWidthPixels();
    int heightPixels = builder.screenDimensions.getHeightPixels();
    // 计算屏幕大小的图片所占的内存
    int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
    // Bitmap 内存缓存,Android 8 以上是 1 倍屏幕大小图片内存所占用内存,Android 8 以下是 4 倍屏幕大小图片内存所占用的内存。
    int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);
    // Resource 内存缓存,2 倍屏幕大小图片内存占用内存。
    int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
    int availableSize = maxSize - arrayPoolSize;

    // 如果 Bitmap 缓存和 Resource 缓存之和大于了最大的可用缓存,重新计算它们的缓存大小。
    if (targetMemoryCacheSize + targetBitmapPoolSize <= availableSize) {
      memoryCacheSize = targetMemoryCacheSize;
      bitmapPoolSize = targetBitmapPoolSize;
    } else {
      float part = availableSize / (builder.bitmapPoolScreens + builder.memoryCacheScreens);
      memoryCacheSize = Math.round(part * builder.memoryCacheScreens);
      bitmapPoolSize = Math.round(part * builder.bitmapPoolScreens);
    }

    // ...
  }

可能你会疑惑 Bitmap 缓存在 Android 8 及其以上为 0,而 Android 8 以下是 4 倍屏幕大小内存。因为 Android 8 以上的 Bitmap 的配置使用的是 Config.HARDWARE,它所占用的是显存,而不占用内存,Bitmap 中的数据也无法修改,所以不缓存。

LruBitmapPool

LruBitmapPoolBitmapPool 的实现,缓存大小是 4 倍屏幕大小图片占用内存,我们来分析一下它的工作原理。

缓存数据

我们看看 LruBitmapPool#get() 方法的实现:

Java 复制代码
  @Override
  public synchronized void put(Bitmap bitmap) {
    // 检查 Bitmap 的状态,是否可回收
    if (bitmap == null) {
      throw new NullPointerException("Bitmap must not be null");
    }
    if (bitmap.isRecycled()) {
      throw new IllegalStateException("Cannot pool recycled bitmap");
    }
    // Bitmap 需要支持可修改,内存大小小于缓存的最大可以使用的缓存,必须是允许缓存的配置。
    if (!bitmap.isMutable()
        || strategy.getSize(bitmap) > maxSize
        || !allowedConfigs.contains(bitmap.getConfig())) {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(
            TAG,
            "Reject bitmap from pool"
                + ", bitmap: "
                + strategy.logBitmap(bitmap)
                + ", is mutable: "
                + bitmap.isMutable()
                + ", is allowed config: "
                + allowedConfigs.contains(bitmap.getConfig()));
      }
      bitmap.recycle();
      return;
    }

    final int size = strategy.getSize(bitmap);
    // 执行缓存
    strategy.put(bitmap);
    tracker.add(bitmap);
    // 更新缓存的 bitmap 数量,和当前缓存的占用
    puts++;
    currentSize += size;

    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
    }
    dump();
    // 检查是否达到最大的缓存值,如果达到了最大的缓存值,清除最旧的 Bitmap.
    evict();
  }

简单梳理一下上面的逻辑:

  1. 检查 Bitmap 的状态,必须满足以下的状态才可以缓存:
  • 不为空
  • 没有被回收
  • 可以被修改
  • 内存占用大小,小于当前 BitmapPool 的最大缓存值
  • 是允许被缓存的 Bitmap 配置。
  1. 通过 strategy#put() 方法完成缓存,strategy 的实现类通常是 SizeConfigStrategy
  2. 更新当前缓存的 Bitmap 数量和占用的缓存大小。
  3. 通过 evict() 方法,检查是否达到最大的缓存值,如果达到了最大的缓存值,清除最旧的 Bitmap

我们先看看 evict() 方法是如何实现的:

Java 复制代码
  private synchronized void trimToSize(long size) {
    // 循环清除最旧的数据,直到当前的缓存大小,小于目标大小
    while (currentSize > size) {
      // 通过 SizeConfigStrategy#removeLast() 方法移除最旧的缓存
      final Bitmap removed = strategy.removeLast();
      // TODO: This shouldn't ever happen, see #331.
      if (removed == null) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(TAG, "Size mismatch, resetting");
          dumpUnchecked();
        }
        currentSize = 0;
        return;
      }
      tracker.remove(removed);
      // 重新计算当前缓存内存
      currentSize -= strategy.getSize(removed);
      // 增加被移除的 Bitmap 数量
      evictions++;
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
      }
      dump();
      // 回收 Bitmap
      removed.recycle();
    }
  }

上面回收最旧的数据用到了 SizeConfigStrategy#removeLast() 方法来实现。

我们先看看 SizeConfigStrategy#put() 方法是如何插入缓存的:

Java 复制代码
  private final KeyPool keyPool = new KeyPool();
  private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
  private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();

  @Override
  public void put(Bitmap bitmap) {
    // 获取 Bitmap 大小
    int size = Util.getBitmapByteSize(bitmap);
    // 通过 Bitmap 配置与大小计算 Key
    Key key = keyPool.get(size, bitmap.getConfig());
    // 将 Bitmap 缓存到 GroupedLinkedMap 中
    groupedMap.put(key, bitmap);
    
    // 重新计算当前配置的 Bitmap 的数量。
    NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
    Integer current = sizes.get(key.size);
    sizes.put(key.size, current == null ? 1 : current + 1);
  }

解释一下上面代码:

  1. 通过 Bitmap 大小和配置计算 KeyKeyPool 中计算也非常简单,它的内部最多缓存 20 个 Key,如果没有缓存就创建一个新的,KeyhashCode 是根据 Bitmap 的配置与大小来计算的。
  2. 通过 GroupedLinkedMap 来缓存 Bitmap,它是类似于 HashMap,不过和 HashMap 不同的是:HashMap 会移除相同 Key 的数据;GroupedLinkedMap 不会移除相同 Key 的数据,而是把相同 Key 的数据以列表的形式串联起来。HashMapget() 方法也不会移除原来的数据,而 GroupedLinkedMap 会移除获取到的数据,后面我们会看 GroupedLinkedMap 的代码。
  3. 重新计算当前配置的 Bitmap 数量,存储在 sortedSizes中,它是一个 HashMapKeyBitmap.ConfigValue 是一个 Map<Integer, Integer>(其中 KeyBitmap 的大小, Value 是对应的数量)。

我们再来看看 GroupedLinkedMap#put() 方法是如何缓存数据的:

Java 复制代码
  private final LinkedEntry<K, V> head = new LinkedEntry<>();
  private final Map<K, LinkedEntry<K, V>> keyToEntry = new HashMap<>();

  public void put(K key, V value) {
    // 获取对应的 LinkedEntry
    LinkedEntry<K, V> entry = keyToEntry.get(key);

    if (entry == null) {
      // LinkedEntry 为空
      // 创建一个新的 LinkedEntry 对象。
      entry = new LinkedEntry<>(key);
      // 将当前 entry 插入到链表尾部
      makeTail(entry);
      // 将当前 entry 添加到 Map 中
      keyToEntry.put(key, entry);
    } else {
      // LinkedMap 不为空
      // 回收当前的 Key.
      key.offer();
    }

    entry.add(value);
  }
  
 private static class LinkedEntry<K, V> {
    @Synthetic final K key;
    private List<V> values;
    LinkedEntry<K, V> next;
    LinkedEntry<K, V> prev;

    // Used only for the first item in the list which we will treat specially and which will not
    // contain a value.
    LinkedEntry() {
      this(null);
    }

    LinkedEntry(K key) {
      next = prev = this;
      this.key = key;
    }

    @Nullable
    public V removeLast() {
      final int valueSize = size();
      return valueSize > 0 ? values.remove(valueSize - 1) : null;
    }

    public int size() {
      return values != null ? values.size() : 0;
    }

    public void add(V value) {
      if (values == null) {
        values = new ArrayList<>();
      }
      values.add(value);
    }
  }

理解 GroupedLinkedMap 主要就是理解 LinkedEntry 就够了,它是一个双向的环状链表,其中还存储了我们存储的 Value 列表(在 BitmapPool 中就是一个 Bitmap 的列表),其中 LinkedEntry 的新旧就是通过双向的环状链表来排序。在上面的代码中我们的新创建的 LinkedEntry 就会通过 makeTail() 方法插入到链表的尾部,我们来看看它的实现:

Java 复制代码
  private void makeTail(LinkedEntry<K, V> entry) {
    // 先移除当前节点
    removeEntry(entry);
    // 当前节点的前一个节点指向 head 的前一个节点
    entry.prev = head.prev;
    // 当前节点的下一个节点指向 head
    entry.next = head;
    // 更新当前节点的前后节点的状态
    updateEntry(entry);
  }
  
  private static <K, V> void removeEntry(LinkedEntry<K, V> entry) {
    entry.prev.next = entry.next;
    entry.next.prev = entry.prev;
  }
  
  private static <K, V> void updateEntry(LinkedEntry<K, V> entry) {
    entry.next.prev = entry;
    entry.prev.next = entry;
  }

在上面讲到移除最旧一条数据时会调用 GroupedLinkedMap#put() 方法,我们再看看它的实现:

Java 复制代码
  @Override
  @Nullable
  public Bitmap removeLast() {
    Bitmap removed = groupedMap.removeLast();
    if (removed != null) {
      int removedSize = Util.getBitmapByteSize(removed);
      decrementBitmapOfSize(removedSize, removed);
    }
    return removed;
  }

我们看到它又调用了 GroupedLinkedMap#removeLast() 方法:

Java 复制代码
  @Nullable
  public V removeLast() {
    // 获取最后一个 LinkedEntry
    LinkedEntry<K, V> last = head.prev;

    while (!last.equals(head)) {
      // 通过 LinkedEntry#removeLast() 方法移除最后一个元素
      V removed = last.removeLast();
      if (removed != null) {
        // 移除的数据不为空直接返回
        return removed;
      } else {
        // 移除的数据为空表示,LinkedEntry 已经为空了,需要从链表中移除,也需要从 HashMap 中移除
        removeEntry(last);
        keyToEntry.remove(last.key);
        // 回收对应的 Key
        last.key.offer();
      }
      // 获取当前的 LinkedEntry 的前一个节点继续移除数据,直到有数据被移除。  
      last = last.prev;
    }

    return null;
  }

上面的代码也比较简单就不再多说。

获取数据

直接从 LruBitmapPool#get() 方法看起:

Java 复制代码
  public Bitmap get(int width, int height, Bitmap.Config config) {
    // 获取缓存
    Bitmap result = getDirtyOrNull(width, height, config);
    if (result != null) {
      // 擦除原有的 Bitmap 中的数据
      result.eraseColor(Color.TRANSPARENT);
    } else {
      // 创建一个新的空白的 Bitmap
      result = createBitmap(width, height, config);
    }

    return result;
  }
  
  @NonNull
  @Override
  public Bitmap getDirty(int width, int height, Bitmap.Config config) {
    Bitmap result = getDirtyOrNull(width, height, config);
    if (result == null) {
      result = createBitmap(width, height, config);
    }
    return result;
  }
  
   @Nullable
  private synchronized Bitmap getDirtyOrNull(
      int width, int height, @Nullable Bitmap.Config config) {
    // 交验配置不为 HARDWARE  
    assertNotHardwareConfig(config);
    // 从 SizeConfigStrategy 中获取 Bitmap 缓存
    final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
    if (result == null) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Missing bitmap=" + strategy.logBitmap(width, height, config));
      }
      // 记录未命中的数量
      misses++;
    } else {
      // 记录命中的数量
      hits++;
      // 更新缓存大小
      currentSize -= strategy.getSize(result);
      tracker.remove(result);
      normalize(result);
    }
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Get bitmap=" + strategy.logBitmap(width, height, config));
    }
    dump();

    return result;
  }

上面代码看着挺多,其实非常简单,通过 SizeConfigStrategy#get() 方法去获取缓存,如果缓存不为空,将获取到的缓存 Bitmap 数据擦除。如果缓存为空,创建一个新的 Bitmap

然后再看看 SizeConfigStrategy#get() 方法的实现:

Java 复制代码
  @Override
  @Nullable
  public Bitmap get(int width, int height, Bitmap.Config config) {
    int size = Util.getBitmapByteSize(width, height, config);
    // 计算出最优的 key
    Key bestKey = findBestKey(size, config);
    
    // 调用 GroupedLinkedMap#get() 方法获取 Bitmap
    Bitmap result = groupedMap.get(bestKey);
    if (result != null) {
      // 获取到的缓存不为空
      // Decrement must be called before reconfigure.
      // 更新 Bitmap Size 的记录
      decrementBitmapOfSize(bestKey.size, result);
      // 重新设置 Bitmap 中的尺寸和配置
      result.reconfigure(width, height, config);
    }
    return result;
  }

我们再看看 GroupedLinkedMap#get() 的实现:

Java 复制代码
  public V get(K key) {
    // 获取 Entry
    LinkedEntry<K, V> entry = keyToEntry.get(key);
    if (entry == null) {
    
      // 如果原来的 Entry 为空创建一个新的添加到 map 中
      entry = new LinkedEntry<>(key);
      keyToEntry.put(key, entry);
    } else {
      // 回收 key
      key.offer();
    }
    
    // 将当前 entry 设置为 head.
    makeHead(entry);
    
    // 获取并移除一个 Bitmap。
    return entry.removeLast();
  }

上面代码也是非常简单了,我们再简单看看 makeHead() 的实现:

Java 复制代码
  private void makeHead(LinkedEntry<K, V> entry) {
    // 移除当前 entry
    removeEntry(entry);
    // 前一个节点指向 head
    entry.prev = head;
    // 后一个节点指向当前 head 的后一个节点
    entry.next = head.next;
    // 更新 entry 前后节点的状态
    updateEntry(entry);
  }

LruArrayPool

LruArrayPoolLruBitmapPool 工作的方式非常的类似,当你理解了 LruBitmapPool 后,理解 LruArrayPool 是非常的简单,它是用来缓存 int 数组和 byte 数组的缓存池。

缓存数据

Java 复制代码
  public synchronized <T> void put(T array) {
    @SuppressWarnings("unchecked")
    Class<T> arrayClass = (Class<T>) array.getClass();
    
    // 获取数组的 Adapter 对象。
    ArrayAdapterInterface<T> arrayAdapter = getAdapterFromType(arrayClass);
    // 获取数组的长度
    int size = arrayAdapter.getArrayLength(array);
    // 计算数组的大小
    int arrayBytes = size * arrayAdapter.getElementSizeInBytes();
    // 如果数组过大,跳过缓存
    if (!isSmallEnoughForReuse(arrayBytes)) {
      return;
    }
    // 计算 key
    Key key = keyPool.get(size, arrayClass);
    // 同样是用 GroupedLinkedMap 来存储
    groupedMap.put(key, array);
    // 计算当前类型的数组的缓存数量
    NavigableMap<Integer, Integer> sizes = getSizesForAdapter(arrayClass);
    Integer current = sizes.get(key.size);
    sizes.put(key.size, current == null ? 1 : current + 1);
    // 更新缓存占用大小
    currentSize += arrayBytes;
    // 计算是否达到缓存上限,达到上限后移除最久的缓存,直到没有超过上限为止。
    evict();
  }

简单整理下上面的代码:

  1. 通过 getAdapterFromType() 方法查找对应数组的 Adapter 对象。
  2. 计算数组的内存占用大小。
  3. 如果数组过大,就跳过缓存,直接退出方法;反之,继续后续的流程。
  4. 通过数组长度和 arrayClass 来计算 key
  5. 通过 GroupedLinkedMap#put() 方法来缓存,前面已经分析过了。
  6. 更新缓存的数量。
  7. 更新缓存占用大小。
  8. 计算是否达到缓存上限,达到上限后移除最久的缓存,直到没有超过上限为止。

我们再看看 getAdapterFromType() 方法的实现:

Java 复制代码
  private <T> ArrayAdapterInterface<T> getAdapterFromType(Class<T> arrayPoolClass) {
    ArrayAdapterInterface<?> adapter = adapters.get(arrayPoolClass);
    if (adapter == null) {
      if (arrayPoolClass.equals(int[].class)) {
        adapter = new IntegerArrayAdapter();
      } else if (arrayPoolClass.equals(byte[].class)) {
        adapter = new ByteArrayAdapter();
      } else {
        throw new IllegalArgumentException(
            "No array pool found for: " + arrayPoolClass.getSimpleName());
      }
      adapters.put(arrayPoolClass, adapter);
    }
    return (ArrayAdapterInterface<T>) adapter;
  }

从上面代码我们也能够看出只生成了 int[]byte[]ArrayAdapter,也就是只能够缓存 int[]byte[]

获取数据

Java 复制代码
  @Override
  public synchronized <T> T get(int size, Class<T> arrayClass) {
    Integer possibleSize = getSizesForAdapter(arrayClass).ceilingKey(size);
    final Key key;
    // 计算 key
    if (mayFillRequest(size, possibleSize)) {
      key = keyPool.get(possibleSize, arrayClass);
    } else {
      key = keyPool.get(size, arrayClass);
    }
    return getForKey(key, arrayClass);
  }
  
  private <T> T getForKey(Key key, Class<T> arrayClass) {
    ArrayAdapterInterface<T> arrayAdapter = getAdapterFromType(arrayClass);
    
    // 直接从 GroupedLinkedMap 中获取缓存
    T result = getArrayForKey(key);
    if (result != null) {
      // 重新计算缓存占用和 size 的信息
      currentSize -= arrayAdapter.getArrayLength(result) * arrayAdapter.getElementSizeInBytes();
      decrementArrayOfSize(arrayAdapter.getArrayLength(result), arrayClass);
    }

    if (result == null) {
      if (Log.isLoggable(arrayAdapter.getTag(), Log.VERBOSE)) {
        Log.v(arrayAdapter.getTag(), "Allocated " + key.size + " bytes");
      }
      // 如果缓存中没有,创建一个新的数组
      result = arrayAdapter.newArray(key.size);
    }
    return result;
  }
  
  @Nullable
  private <T> T getArrayForKey(Key key) {
    return (T) groupedMap.get(key);
  }

上面代码也非常简单,简单来说就是先从 GrouptedLinkedMap 中获取缓存,如果缓存获取失败就创建一个新的数组。

LruResourceCache

它缓存的是 Resource 对象,它是继承于 LruCache

缓存数据

我们看看 LruCache#put() 方法的实现:

Java 复制代码
  private final Map<T, Entry<Y>> cache = new LinkedHashMap<>(100, 0.75f, true);
  @Nullable
  public synchronized Y put(@NonNull T key, @Nullable Y item) {
    // 获取 item 的大小
    final int itemSize = getSize(item);
    // 如果 item 大小超过缓存最大值,直接丢弃
    if (itemSize >= maxSize) {
      onItemEvicted(key, item);
      return null;
    }

    if (item != null) {
      // 更新当前缓存大小
      currentSize += itemSize;
    }
    // 缓存数据
    @Nullable Entry<Y> old = cache.put(key, item == null ? null : new Entry<>(item, itemSize));
    if (old != null) {
      // 如果 old 数据不为空
      // 更新缓存大小
      currentSize -= old.size;

      if (!old.value.equals(item)) {
        onItemEvicted(key, old.value);
      }
    }
    // 计算是否达到缓存上限,达到上限后移除最久的缓存,直到没有超过上限为止。
    evict();

    return old != null ? old.value : null;
  }

注意上面的代码使用的缓存实现类是 LinkedHashMap,而且它的 order 参数是 true,也就是遍历时会按插入的先后顺序获取值。我们再简单看看 evict() 方法的实现:

Java 复制代码
  private void evict() {
    trimToSize(maxSize);
  }
  
  protected synchronized void trimToSize(long size) {
    Map.Entry<T, Entry<Y>> last;
    Iterator<Map.Entry<T, Entry<Y>>> cacheIterator;
    // 如果当前内存占用大于目标值,开始遍历 cache
    while (currentSize > size) {
      cacheIterator = cache.entrySet().iterator();
      last = cacheIterator.next();
      final Entry<Y> toRemove = last.getValue();
      // 重新计算内存占用
      currentSize -= toRemove.size;
      final T key = last.getKey();
      // 移除元素
      cacheIterator.remove();
      onItemEvicted(key, toRemove.value);
    }
  }

获取数据

我们再看看 LruCache#get() 方法的实现:

Java 复制代码
@Nullable  
public synchronized Y get(@NonNull T key) {  
   Entry<Y> entry = cache.get(key);  
   return entry != null ? entry.value : null;  
}

朴实无华,没有太多要说的,这里注意,它不会移除数据。

InternalCacheDiskCacheFactory

它是 Glide 中的默认磁盘缓存处理类,默认的缓存大小是 250MB,当超过最大的缓存后就会删除最旧的数据,直到当前的缓存小于最大的缓存。在它的内部的缓存的实现类是 DiskLruCache,相对于其他的内存缓存类要复杂一些,所以我这里要先大体说明一下他的工作方式。

首先 DiskLruCache 中会有一个 journal 来描述管理所有的缓存的状态,我这里举一个 journal 缓存文件的例子。

text 复制代码
libcore.io.DiskLruCache  
1  
100  
2  

CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054  
DIRTY 335c4c6028171cfddfbaae1a9c313c52  
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342  
REMOVE 335c4c6028171cfddfbaae1a9c313c52  
DIRTY 1ab96a171faeeee38496d8b330771a7a  
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234  
READ 335c4c6028171cfddfbaae1a9c313c52  
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6

首先看看 journal 文件的头部:

text 复制代码
libcore.io.DiskLruCache  
1  
100  
2

由上往下看:

  • libcore.io.DiskLruCache:用来标识 journal 文件。
  • 1:表示缓存协议的版本,我们目前使用的版本都是 1
  • 100:表示应用的版本。
  • 2:表示每一个缓存的 Key 对应多少个缓存文件,例子中是 2 个,但是 Glide 使用这个缓存时都是 1 个。

我们再来看看 journal 文件中的内容,他的格式都是状态 + Key 生成的 Hash 256 值(CLEAN 中还会接多个数字,后续会讲)。再来讲讲不同的状态表示缓存的什么意思:

  • CLEAN

    表示当前的缓存可以正常读取,如果一个 Key 对应多少缓存文件,后续就有多少个数字,比如我们的是 2,后续就还跟了两个数字,他们表示缓存文件的大小。

    以下面为例:
    CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 上诉的缓存文件就是 3400330d1dfc7f3f7f4b8d4d803dfcf6.03400330d1dfc7f3f7f4b8d4d803dfcf6.1 他们的大小依次是 832 bytes21054 bytes

  • DIRTY

    表示当前的 Key 对应的缓存文件正在被写入,这个状态时不允许读取,不允许再写入,只有当写入完成后状态才会被修改成 CLEAN

  • REMOVE

    表示当前的 Key 对应的缓存文件已经被删除。

  • READ

    表示有地方正在读取缓存,可以同时有多个地方读取。

当你理解了 journal 文件后,理解 DiskLruCache 就简单多了,在 DiskLruCache 初始化的时候会去检查 journal 文件,如果没有就创建一个新的,如果有旧的,就去去读它,其中读取到的缓存的状态保存到 Entry 对象中,可以理解为 Entry 就是 journal 反序列化后保存到内存中的信息。(达到某些条件后,还会通过 Entry 的信息更新 journal 文件的信息)

读取缓存:读取缓存比较简单,Entry 的对应的状态必须是 CLEAN,将 journal 文件中添加一条 READ 状态的数据,然后将 Entry 中对应的文件返回,同一个缓存可以同时被多个地方读取。

写入缓存:只有当当前的缓存正在被写入时不能再被写入,其他的情况都能够写入,写入时的状态会被修改成 DIRTY,修改完成后状态会被修改成 CLEAN 状态,如果我们在 journal 发现了 DIRTY 的状态,就表示正在写入或者上次写入失败了。

DiskLruCache 初始化

首先看看 InternalCacheDiskCacheFactory,它是继承于 DiskLruCacheFactory,我们先看看它的构造函数。

Java 复制代码
  public InternalCacheDiskCacheFactory(Context context) {
    this(
        context,
        DiskCache.Factory.DEFAULT_DISK_CACHE_DIR,
        DiskCache.Factory.DEFAULT_DISK_CACHE_SIZE);
  }

  public InternalCacheDiskCacheFactory(Context context, long diskCacheSize) {
    this(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, diskCacheSize);
  }

  public InternalCacheDiskCacheFactory(
      final Context context, final String diskCacheName, long diskCacheSize) {
    super(
        new CacheDirectoryGetter() {
          @Override
          public File getCacheDirectory() {
            File cacheDirectory = context.getCacheDir();
            if (cacheDirectory == null) {
              return null;
            }
            if (diskCacheName != null) {
              return new File(cacheDirectory, diskCacheName);
            }
            return cacheDirectory;
          }
        },
        diskCacheSize);
  }

代码非常简单,指定默认缓存大小为 250 MB,缓存的目录是应用私有目录下的 cache 目录,Glide 的缓存目录名是 image_manager_disk_cache

我们再来看看 DiskLruCacheFactory#build() 方法是怎样创建 DiskCache 实例的:

Java 复制代码
  @Override
  public DiskCache build() {
    File cacheDir = cacheDirectoryGetter.getCacheDirectory();

    if (cacheDir == null) {
      return null;
    }

    if (cacheDir.isDirectory() || cacheDir.mkdirs()) {
      return DiskLruCacheWrapper.create(cacheDir, diskCacheSize);
    }

    return null;
  }

朴实无华的代码,通过调用 DiskLruCacheWrapper#create() 方法创建 DiskCache 实例,继续追踪代码:

Java 复制代码
  public static DiskCache create(File directory, long maxSize) {
    return new DiskLruCacheWrapper(directory, maxSize);
  }
  protected DiskLruCacheWrapper(File directory, long maxSize) {
    this.directory = directory;
    this.maxSize = maxSize;
    this.safeKeyGenerator = new SafeKeyGenerator();
  }

朴实无华,看 DiskLruCacheWrapper 这个名字就知道,他是一个包裹类,实现 DiskCache 的也是另有其人。我帮大家找到了,看 DiskLruCacheWrapper#getDiskCache() 方法:

Java 复制代码
  private synchronized DiskLruCache getDiskCache() throws IOException {
    if (diskLruCache == null) {
      diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
    }
    return diskLruCache;
  }

我们看到了调用了 DiskLruCache#open() 方法:

Java 复制代码
  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);
    // 如果有上次更新失败了,使用备份文件来当当前的 journal 文件.
    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);
      }
    }

    // Prefer to pick up where we left off.
    DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    // 如果有上次的 journal 文件
    if (cache.journalFile.exists()) {
      try {
        // 解析 journal 文件
        cache.readJournal();
        // 移除上次 DIRTY 的更新失败的缓存文件
        cache.processJournal();
        return cache;
      } catch (IOException journalIsCorrupt) {
        System.out
            .println("DiskLruCache "
                + directory
                + " is corrupt: "
                + journalIsCorrupt.getMessage()
                + ", removing");
        cache.delete();
      }
    }
    
    // 没有上次的 journal 文件,创建一个新的 DiskLruCache 对象
    // Create a new empty cache.
    directory.mkdirs();
    cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    // 写入新的 journal 文件
    cache.rebuildJournal();
    return cache;
  }

这里要先简单说明一下 journal 文件的更新方式,更新时会将原来的 journal 文件修改成 journal.bak,新写入的文件是 journal.tmp,当写入完成后会将 journal.tmp 重新命名为 journal,然后删除 journal.bak 文件,所以更新成功后就不会有 journal.tmpjournal.bak 文件。

这里简单整理一下上面的代码:

  1. 判断是否有 journal.bak 文件,如果有就表示上次更新 journal 文件失败,需要将备份文件重新命名成 journal
  2. 判断是否有上次的 journal 文件,如果有的话首先通过 DiskLruCache#readJournal() 解析 journal 文件到 Entry;然后通过 DiskLruCache#processJournal() 方法删除上次写失败的缓存文件。
  3. 如果上次没有 journal 文件,会通过 DiskLruCache#rebuildJournal() 方法生成一个新的。

我们先来看看 DiskLruCache#readJournal() 方法是如何解析 journal 文件的:

Java 复制代码
  private void readJournal() throws IOException {
    StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
    try {
      String magic = reader.readLine();
      String version = reader.readLine();
      String appVersionString = reader.readLine();
      String valueCountString = reader.readLine();
      String blank = reader.readLine();
      // 检查 journal 文件头
      if (!MAGIC.equals(magic)
          || !VERSION_1.equals(version)
          || !Integer.toString(appVersion).equals(appVersionString)
          || !Integer.toString(valueCount).equals(valueCountString)
          || !"".equals(blank)) {
        throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
            + valueCountString + ", " + blank + "]");
      }

      int lineCount = 0;
      while (true) {
        try {
          // 读取 journal body 中的一条 entry 数据
          readJournalLine(reader.readLine());
          lineCount++;
        } catch (EOFException endOfJournal) {
          break;
        }
      }
      redundantOpCount = lineCount - lruEntries.size();

      // If we ended on a truncated line, rebuild the journal before appending to it.
      if (reader.hasUnterminatedLine()) {
      
        // journal 文件被破坏,重新写入 journal 文件
        rebuildJournal();
      } else {
        journalWriter = new BufferedWriter(new OutputStreamWriter(
            new FileOutputStream(journalFile, true), Util.US_ASCII));
      }
    } finally {
      Util.closeQuietly(reader);
    }
  }

首先校验 journal 头数据,然后解析 journal 文件中的内容,通过 readJournalLine() 方法来解析一行信息。如果 journal 文件被破坏,通过 rebuildJournal() 方法将 Entry 里面的信息重新写入 journal 文件。

我们看看 readJournalLine() 方法是如何解析 journal 的。

Java 复制代码
  private void readJournalLine(String line) throws IOException {
    int firstSpace = line.indexOf(' ');
    if (firstSpace == -1) {
      throw new IOException("unexpected journal line: " + line);
    }

    int keyBegin = firstSpace + 1;
    int secondSpace = line.indexOf(' ', keyBegin);
    final String key;
    if (secondSpace == -1) {
      key = line.substring(keyBegin);
      // 如果是 Remove 直接移除掉 Entry
      if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
        lruEntries.remove(key);
        return;
      }
    } else {
      key = line.substring(keyBegin, secondSpace);
    }
    // 获取对应的 Entry
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      // 没有的话创建一个新的
      entry = new Entry(key);
      lruEntries.put(key, entry);
    }

    if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
    
      // 如果是 Clean 会写入对应缓存文件的大小和数量
      String[] parts = line.substring(secondSpace + 1).split(" ");
      // 标记可读
      entry.readable = true;
      // 标记没有在写入
      entry.currentEditor = null;
      // 设置缓存文件大小
      entry.setLengths(parts);
    } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
      // 如果是 Dirty 标记正在写入
      entry.currentEditor = new Editor(entry);
    } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
      // 如果是 Read,没有做任何处理
      // This work was already done by calling lruEntries.get().
    } else {
      throw new IOException("unexpected journal line: " + line);
    }
  }

这里简单整理下 journal 中的状态和 Entry 中对应的关系:

  • REMOVE
    直接移除对应的 Entry
  • CLEAN
    标记 Entry#readabletrue,表示可读;标记 Entry#currentEditor 为空,表示没有正在写;添加对应的缓存文件的大小和数量。
  • DIRTY
    标记 Entry#currentEditor 不为空,表示正在写入,不可读。
  • READ
    不做任何处理,如果前面读取到这个 KeyClean 的,那么他后续就可读,如果是其他情况就不可读。

我们再来看看 processJournal() 方法是如何移除写入失败的缓存文件的:

Java 复制代码
  private void processJournal() throws IOException {
    // 如果有 journal.tmp 文件直接删除
    deleteIfExists(journalFileTmp);
    for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
      Entry entry = i.next();      
      if (entry.currentEditor == null) {
        // CLEAN
        // 计算 CLEAN 状态中的缓存文件的大小
        for (int t = 0; t < valueCount; t++) {
          size += entry.lengths[t];
        }
      } else {
        // DIRTY
        // 删除 DIRTY 状态中的所有的缓存文件
        entry.currentEditor = null;
        for (int t = 0; t < valueCount; t++) {
          deleteIfExists(entry.getCleanFile(t));
          deleteIfExists(entry.getDirtyFile(t));
        }
        i.remove();
      }
    }
  }

代码比较简单,如果是 CLEAN 的状态计算它的缓存文件的大小;如果是 DIRTY,删除它的所有的缓存文件。

再来看看 rebuildJournal() 方法是如何重写 journal 文件的:

Java 复制代码
  private synchronized void rebuildJournal() throws IOException {
    if (journalWriter != null) {
      closeWriter(journalWriter);
    }
    
    // 注意这个 writer 是写入到 tmp 文件中的
    Writer writer = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
    try {
      // 写入头部
      writer.write(MAGIC);
      writer.write("\n");
      writer.write(VERSION_1);
      writer.write("\n");
      writer.write(Integer.toString(appVersion));
      writer.write("\n");
      writer.write(Integer.toString(valueCount));
      writer.write("\n");
      writer.write("\n");
      
      // 写入 Entry
      for (Entry entry : lruEntries.values()) {
        if (entry.currentEditor != null) {
          writer.write(DIRTY + ' ' + entry.key + '\n');
        } else {
          writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
        }
      }
    } finally {
      closeWriter(writer);
    }

    if (journalFile.exists()) {
      // 将原来的 journal 文件命名成 journal.bak
      renameTo(journalFile, journalFileBackup, true);
    }
    // 将新写入 jounal.tmp 文件命名成 journal
    renameTo(journalFileTmp, journalFile, false);
    
    // 删除 journal.bak 文件.
    journalFileBackup.delete();

    journalWriter = new BufferedWriter(
        new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
  }

在上面其实我已经介绍过了是如何重写 journal 文件的,这里只是简单贴出代码,逻辑非常清晰且简单。

获取数据

我们看看 DiskLruCacheWrapper#get() 方法:

Java 复制代码
  public File get(Key key) {
    // 计算 key 的 hash 256 值
    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 {
      // 调用 DiskLruCache#get() 方法
      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;
  }

我们看到它调用了 DiskLruCache#get() 方法,我们继续跟踪:

Java 复制代码
  public synchronized Value get(String key) throws IOException {
    checkNotClosed();
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      return null;
    }
    
    // 必须可读
    if (!entry.readable) {
      return null;
    }
    
    // 校验所有的缓存文件必须存在
    for (File file : entry.cleanFiles) {
        // A file must have been deleted manually!
        if (!file.exists()) {
            return null;
        }
    }
    // 更新操作数
    redundantOpCount++;
    
    // 更新 journal 文件,状态设置为 READ
    journalWriter.append(READ);
    journalWriter.append(' ');
    journalWriter.append(key);
    journalWriter.append('\n');
    if (journalRebuildRequired()) {
      // 满足一定条件后触发清理任务
      executorService.submit(cleanupCallable);
    }
    // 将 Entry 中的需要的数据封装到 Value 对象中。
    return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
  }

目标的 Entry 必须是可读的,而且缓存的文件都存在,然后把 Entry 中的关键数据封装到 Value 对象中,这个过程还会更新 journal 文件,状态设置为 READ

缓存数据

我们继续看看 DiskLruCacheWrapper#put() 方法:

Java 复制代码
  @Override
  public void put(Key key, Writer writer) {
    String safeKey = safeKeyGenerator.getSafeKey(key);
    writeLocker.acquire(safeKey);
    try {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Put: Obtained: " + safeKey + " for for Key: " + key);
      }
      try {
        DiskLruCache diskCache = getDiskCache();
        // 获取当前 key 的数据
        Value current = diskCache.get(safeKey);
        if (current != null) {
          // 如果当前的 key 已经有数据跳过写入,直接返回.
          return;
        }
        
        // 调用 DiskLruCache#edit() 方法来生成一个对应的 Editor 对象
        DiskLruCache.Editor editor = diskCache.edit(safeKey);
        if (editor == null) {
          throw new IllegalStateException("Had two simultaneous puts for: " + safeKey);
        }
        try {
          // 只写入一个文件
          File file = editor.getFile(0);
          // 将参数中 writer 中的数据写入到文件中
          if (writer.write(file)) {
            // 通知 Editor 已经完成写入。  
            editor.commit();
          }
        } finally {
          editor.abortUnlessCommitted();
        }
      } catch (IOException e) {
        if (Log.isLoggable(TAG, Log.WARN)) {
          Log.w(TAG, "Unable to put to disk cache", e);
        }
      }
    } finally {
      writeLocker.release(safeKey);
    }
  }

首先会判断当前 key 是否已经有缓存的数据,如果有就跳过写入,直接返回。然后通过 DiskLruCache#edit() 方法来生成一个 Editor 对象来帮助写入,从 Editor 中获取写入的文件,然后将 Writer 中的数据写入到文件中,写入完成后通过 Editor#commit() 方法来完成这次的写入。

我们继续看看 DiskLruCache#edit() 方法的实现:

Java 复制代码
  public Editor edit(String key) throws IOException {
    return edit(key, ANY_SEQUENCE_NUMBER);
  }

  private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    checkNotClosed();
    // 获取对应的 Entry
    Entry entry = lruEntries.get(key);
    if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
        || entry.sequenceNumber != expectedSequenceNumber)) {
      return null; // Value is stale.
    }
    if (entry == null) {
    
      // 如果为空,创建一个新的
      entry = new Entry(key);
      lruEntries.put(key, entry);
    } else if (entry.currentEditor != null) {
      // 如果当前正在写,返回空
      return null; // Another edit is in progress.
    }
    // 创建 editor 对象
    Editor editor = new Editor(entry);
    // 标记 当前 Entry 正在写入
    entry.currentEditor = editor;

    // Flush the journal before creating files to prevent file leaks.
    // 在 journal 文件中添加 dirty 的状态
    journalWriter.append(DIRTY);
    journalWriter.append(' ');
    journalWriter.append(key);
    journalWriter.append('\n');    
    // 立即写入到 journal 文件中
    flushWriter(journalWriter);
    return editor;
  }

这里要注意在 journal 写入 DIRTY 状态时会立即刷新到 journal 文件中,而之前的 READ 状态就没有立即刷新到 journal 文件中。

我们继续看看 Emitor#commit() 方法是如何处理写入缓存完成的:

Java 复制代码
    public void commit() throws IOException {
      completeEdit(this, true);
      committed = true;
    }

它会调用 DiskLruCache#completeEdit() 方法:

Java 复制代码
  private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    Entry entry = editor.entry;
    if (entry.currentEditor != editor) {
      throw new IllegalStateException();
    }

    // ...

    for (int i = 0; i < valueCount; i++) {
      File dirty = entry.getDirtyFile(i);
      if (success) {
        if (dirty.exists()) {
          // 将 写入的 dirty 文件重新命名成 clean 文件
          File clean = entry.getCleanFile(i);
          dirty.renameTo(clean);
          long oldLength = entry.lengths[i];
          long newLength = clean.length();
          entry.lengths[i] = newLength;
          // 重新计算缓存占用的大小。
          size = size - oldLength + newLength;
        }
      } else {
        deleteIfExists(dirty);
      }
    }

    redundantOpCount++;
    entry.currentEditor = null;
    if (entry.readable | success) {
      // journal 写入成功将状态修改成 CLEAN
      entry.readable = true;
      journalWriter.append(CLEAN);
      journalWriter.append(' ');
      journalWriter.append(entry.key);
      journalWriter.append(entry.getLengths());
      journalWriter.append('\n');

      if (success) {
        entry.sequenceNumber = nextSequenceNumber++;
      }
    } else {
      // 写入失败,移除对应的 Entry
      lruEntries.remove(entry.key);
      // journal 写入状态 REMOVE
      journalWriter.append(REMOVE);
      journalWriter.append(' ');
      journalWriter.append(entry.key);
      journalWriter.append('\n');
    }
    // 同步 journal 文件.
    flushWriter(journalWriter);
    
    // 如果当前的缓存达到最大值,提交 cleanupCallable 任务清空旧的数据。
    if (size > maxSize || journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }
  }

这里要再说明一下直接写入的缓存文件是一个 tmp 文件,只有当真正地 commit 时,才会把写入完成的 tmp 文件修改成 clean 的文件。当缓存达到最大值后会通过 cleanupCallable 异步任务来执行清理操作。

Java 复制代码
  private final Callable<Void> cleanupCallable = new Callable<Void>() {
    public Void call() throws Exception {
      synchronized (DiskLruCache.this) {
        if (journalWriter == null) {
          return null; // Closed.
        }
        // 检查是否达到最大缓存值
        trimToSize();
        if (journalRebuildRequired()) {
          // 重建 journal 文件
          rebuildJournal();
          redundantOpCount = 0;
        }
      }
      return null;
    }
  };

我们继续看看 trimToSize() 方法的实现:

Java 复制代码
  private void trimToSize() throws IOException {
    // 当当前占用缓存大于最大值时,执行循环
    while (size > maxSize) {
      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
      // 删除目标 entry
      remove(toEvict.getKey());
    }
  }
  
  public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    Entry entry = lruEntries.get(key);
    if (entry == null || entry.currentEditor != null) {
      return false;
    }
    
    // 删除所有的 Entry 中的 Clean 文件.
    for (int i = 0; i < valueCount; i++) {
      File file = entry.getCleanFile(i);
      if (file.exists() && !file.delete()) {
        throw new IOException("failed to delete " + file);
      }
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }

    redundantOpCount++;
    // 更新 journal 文件 REMOVE.
    journalWriter.append(REMOVE);
    journalWriter.append(' ');
    journalWriter.append(key);
    journalWriter.append('\n');
    
    // 移除对应的 Entry.
    lruEntries.remove(key);

    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return true;
  }

上面的代码比较简单,我就不再细说了,你都能够看到这里,上面的代码你肯定能看懂。

最后

本篇文章主要讲了 Glide 内存缓存的实现和磁盘缓存的实现,我认为 Glide 的优秀很大于部分就是得益于它的优秀的内存缓存和磁盘缓存,特别是磁盘缓存,很多人在开发中也写过,但是总是有各种各样的问题,通过看 Glide 的源码,我们了解了顶级的开发者是如何写磁盘文件缓存的,Glide 为我们提供了一个非常好的样板代码,我们也可以去尝试模仿他。当你读懂了 Glide 的缓存处理后,相信你一定会有不小的收获。

相关推荐
MiyamuraMiyako13 分钟前
从 0 到发布:Gradle 插件双平台(MavenCentral + Plugin Portal)发布记录与避坑
android
NRatel1 小时前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥3 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走4 小时前
创建自定义语音录制View
android·前端
用户2018792831674 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831674 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker5 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong6 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil7 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin
程序员码歌13 小时前
【零代码AI编程实战】AI灯塔导航-总结篇
android·前端·后端