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 的缓存处理后,相信你一定会有不小的收获。

相关推荐
丘狸尾11 分钟前
[cisco 模拟器] ftp服务器配置
android·运维·服务器
van叶~2 小时前
探索未来编程:仓颉语言的优雅设计与无限可能
android·java·数据库·仓颉
Crossoads6 小时前
【汇编语言】端口 —— 「从端口到时间:一文了解CMOS RAM与汇编指令的交汇」
android·java·汇编·深度学习·网络协议·机器学习·汇编语言
li_liuliu7 小时前
Android4.4 在系统中添加自己的System Service
android
C4rpeDime9 小时前
自建MD5解密平台-续
android
鲤籽鲲11 小时前
C# Random 随机数 全面解析
android·java·c#
m0_5485147715 小时前
2024.12.10——攻防世界Web_php_include
android·前端·php
凤邪摩羯15 小时前
Android-性能优化-03-启动优化-启动耗时
android
凤邪摩羯15 小时前
Android-性能优化-02-内存优化-LeakCanary原理解析
android
喀什酱豆腐16 小时前
Handle
android