Glide 源码阅读笔记(一)
Glide
是 Android
中的老牌网络图片加载库,虽然年纪比较大,但是它和很多新生代的网络图片加载库相比在各方面依然不落下风。它的友好的使用 API
、优秀的内存管理和对 Android
中特有组建生命周期处理等等优点依然是许多 Android
开发者目前会优先选择使用 Glide
的理由。同样 Glide
中的优秀设计在我们实际开发中也是有可以借鉴的地方,所以从本篇文章开始分析 Glide
的源码。
简单使用
添加依赖
// ...
// 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")
// ...
初始化
// 自定义 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
。
简单加载图片
// ...
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()
方法,我们以它作为第一个分析的方法:
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()
方法的实现:
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()
方法的实现:
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()
方法的逻辑:
@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()
方法:
- 解析
Manifest
中的Module
实例,该方法已经废弃,推荐使用我在demo
中通过注解的方式来实现自定义配置。 - 通过注解生成的代码,来过滤
Manifest
中的Module
。 - 将构建
Glide
的GlideBuilder
对象通过applyOptins()
方法传递给每一个Manifest
中的Module
和注解生成的Module
,这里就完成了对Glide
构建时的自定义配置。 - 最后通过
GlideBuilder#build()
方法完成对Glide
实例的创建。 - 通过
ApplicationContext#registerComponentCallbacks()
监听应用配置改变和低内存的状态,然后通知给Glide
实例。
Glide 中处理缓存的对象
我们继续看看 GlideBuilder#build()
方法的源码。
@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
:在有网络状态监听权限的情况下,监听网络状态。BitmapPool
:Bitmap
的缓存,在Android 8
及其以上版本缓存大小是 0;在Android 8
以下版本缓存的大小是 4 倍屏幕大小图片所需要的内存。ArrayPool
:用来存储int
数组和byte
数组的缓存,它的大小是 4MB;如果是低内存设备内存大小是 2MB。MemoryCache
:Resource
对象的缓存,在Glide
中有很多的对象都是Resource
,它的缓存大小是 2 倍屏幕大小图片所需要的内存。DiskCacheFactory
:本地磁盘缓存处理。Engine
:请求的引擎RequestManagerRetriever
:用于查找对应Context
的RequestManager
。
本节内容只讨论和缓存(内存缓存和磁盘缓存)相关的对象,其他的相关的对象后续的文章还会继续讨论。
MemorySizeCalculator
计算各种内存缓存的大小,包括 BitmapPool
,ArrayPool
与 MemoryCache
等。
我们看看 MemorySizeCalculator
对象的构造函数怎么计算各种内存的大小的:
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
LruBitmapPool
是 BitmapPool
的实现,缓存大小是 4 倍屏幕大小图片占用内存,我们来分析一下它的工作原理。
缓存数据
我们看看 LruBitmapPool#get()
方法的实现:
@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();
}
简单梳理一下上面的逻辑:
- 检查
Bitmap
的状态,必须满足以下的状态才可以缓存:
- 不为空
- 没有被回收
- 可以被修改
- 内存占用大小,小于当前
BitmapPool
的最大缓存值 - 是允许被缓存的
Bitmap
配置。
- 通过
strategy#put()
方法完成缓存,strategy
的实现类通常是SizeConfigStrategy
。 - 更新当前缓存的
Bitmap
数量和占用的缓存大小。 - 通过
evict()
方法,检查是否达到最大的缓存值,如果达到了最大的缓存值,清除最旧的Bitmap
。
我们先看看 evict()
方法是如何实现的:
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()
方法是如何插入缓存的:
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);
}
解释一下上面代码:
- 通过
Bitmap
大小和配置计算Key
,KeyPool
中计算也非常简单,它的内部最多缓存 20 个Key
,如果没有缓存就创建一个新的,Key
的hashCode
是根据Bitmap
的配置与大小来计算的。 - 通过
GroupedLinkedMap
来缓存Bitmap
,它是类似于HashMap
,不过和HashMap
不同的是:HashMap
会移除相同Key
的数据;GroupedLinkedMap
不会移除相同Key
的数据,而是把相同Key
的数据以列表的形式串联起来。HashMap
的get()
方法也不会移除原来的数据,而GroupedLinkedMap
会移除获取到的数据,后面我们会看GroupedLinkedMap
的代码。 - 重新计算当前配置的
Bitmap
数量,存储在sortedSizes
中,它是一个HashMap
,Key
是Bitmap.Config
,Value
是一个Map<Integer, Integer>
(其中Key
是Bitmap
的大小,Value
是对应的数量)。
我们再来看看 GroupedLinkedMap#put()
方法是如何缓存数据的:
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()
方法插入到链表的尾部,我们来看看它的实现:
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()
方法,我们再看看它的实现:
@Override
@Nullable
public Bitmap removeLast() {
Bitmap removed = groupedMap.removeLast();
if (removed != null) {
int removedSize = Util.getBitmapByteSize(removed);
decrementBitmapOfSize(removedSize, removed);
}
return removed;
}
我们看到它又调用了 GroupedLinkedMap#removeLast()
方法:
@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()
方法看起:
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()
方法的实现:
@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()
的实现:
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()
的实现:
private void makeHead(LinkedEntry<K, V> entry) {
// 移除当前 entry
removeEntry(entry);
// 前一个节点指向 head
entry.prev = head;
// 后一个节点指向当前 head 的后一个节点
entry.next = head.next;
// 更新 entry 前后节点的状态
updateEntry(entry);
}
LruArrayPool
LruArrayPool
和 LruBitmapPool
工作的方式非常的类似,当你理解了 LruBitmapPool
后,理解 LruArrayPool
是非常的简单,它是用来缓存 int
数组和 byte
数组的缓存池。
缓存数据
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();
}
简单整理下上面的代码:
- 通过
getAdapterFromType()
方法查找对应数组的Adapter
对象。 - 计算数组的内存占用大小。
- 如果数组过大,就跳过缓存,直接退出方法;反之,继续后续的流程。
- 通过数组长度和
arrayClass
来计算key
。 - 通过
GroupedLinkedMap#put()
方法来缓存,前面已经分析过了。 - 更新缓存的数量。
- 更新缓存占用大小。
- 计算是否达到缓存上限,达到上限后移除最久的缓存,直到没有超过上限为止。
我们再看看 getAdapterFromType()
方法的实现:
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[]
。
获取数据
@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()
方法的实现:
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()
方法的实现:
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()
方法的实现:
@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
缓存文件的例子。
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
文件的头部:
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.0
和3400330d1dfc7f3f7f4b8d4d803dfcf6.1
他们的大小依次是832 bytes
和21054 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
,我们先看看它的构造函数。
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
实例的:
@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
实例,继续追踪代码:
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()
方法:
private synchronized DiskLruCache getDiskCache() throws IOException {
if (diskLruCache == null) {
diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);
}
return diskLruCache;
}
我们看到了调用了 DiskLruCache#open()
方法:
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.tmp
与 journal.bak
文件。
这里简单整理一下上面的代码:
- 判断是否有
journal.bak
文件,如果有就表示上次更新journal
文件失败,需要将备份文件重新命名成journal
。 - 判断是否有上次的
journal
文件,如果有的话首先通过DiskLruCache#readJournal()
解析journal
文件到Entry
;然后通过DiskLruCache#processJournal()
方法删除上次写失败的缓存文件。 - 如果上次没有
journal
文件,会通过DiskLruCache#rebuildJournal()
方法生成一个新的。
我们先来看看 DiskLruCache#readJournal()
方法是如何解析 journal
文件的:
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
的。
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#readable
为true
,表示可读;标记Entry#currentEditor
为空,表示没有正在写;添加对应的缓存文件的大小和数量。DIRTY
标记Entry#currentEditor
不为空,表示正在写入,不可读。READ
不做任何处理,如果前面读取到这个Key
是Clean
的,那么他后续就可读,如果是其他情况就不可读。
我们再来看看 processJournal()
方法是如何移除写入失败的缓存文件的:
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
文件的:
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()
方法:
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()
方法,我们继续跟踪:
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()
方法:
@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()
方法的实现:
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()
方法是如何处理写入缓存完成的:
public void commit() throws IOException {
completeEdit(this, true);
committed = true;
}
它会调用 DiskLruCache#completeEdit()
方法:
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
异步任务来执行清理操作。
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()
方法的实现:
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
的缓存处理后,相信你一定会有不小的收获。
最后
如果想要成为架构师或想突破20~30K薪资范畴,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。
如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。
欢迎大家一键三连支持,若需要文中资料,直接扫描文末CSDN官方认证微信卡片免费领取↓↓↓(文末还有ChatGPT机器人小福利哦,大家千万不要错过)
PS:群里还设有ChatGPT机器人,可以解答大家在工作上或者是技术上的问题