前言
上一篇文章讲了Glide的基本用法和Glide源码主要执行流程,如果没有看过请戳这里Glide加载图片基本流程分析。这篇文章我们重点讲解一下Glide对缓存的处理,以及它的优点。
流程总览
//根据当前设备 (手机) 的缓存大小常数和设备的屏幕密度,宽度和高度智能计算应该分配多少内存
if (memorySizeCalculator == null) {
memorySizeCalculator = new MemorySizeCalculator.Builder(context).build();
}
//因为创建Bitmap的代价很昂贵,Bitmap池为了高效复用Bitmap
if (bitmapPool == null) {
int size = memorySizeCalculator.getBitmapPoolSize();
if (size > 0) {
bitmapPool = new LruBitmapPool(size);
} else {
bitmapPool = new BitmapPoolAdapter();
}
}
/内存缓存
if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
//磁盘缓存
if (diskCacheFactory == null) {
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
}
MemorySizeCalculator 分配内存计算器
分别计算 BitmapPool 、ArrayPool 和 MemoryCache 应该分配的内存大小。
//默认为 4MB,如果是低内存设备则在此基础上除以二
arrayPoolSize =
isLowMemoryDevice(builder.activityManager)
? builder.arrayPoolSizeBytes / LOW_MEMORY_BYTE_ARRAY_POOL_DIVISOR
: builder.arrayPoolSizeBytes;
//其中会先获取当前进程可使用内存大小,
//然后通过判断是否是否为低内存设备乘以相应的系数,
//普通设备是乘以 0.4,低内存为 0.33,这样得到的是 Glide 可使用的最大内存阈值 maxSize
int maxSize =
getMaxSize(
builder.activityManager, builder.maxSizeMultiplier, builder.lowMemoryMaxSizeMultiplier);
int widthPixels = builder.screenDimensions.getWidthPixels();
int heightPixels = builder.screenDimensions.getHeightPixels();
//计算一张格式为 ARGB_8888 ,大小为屏幕大小的图片的占用内存大小
//BYTES_PER_ARGB_8888_PIXEL 值为 4
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);
int targetMemoryCacheSize = Math.round(screenSize * builder.memoryCacheScreens);
//去掉 ArrayPool 占用的内存后还剩余的内存
int availableSize = maxSize - arrayPoolSize;
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);
}
第一级缓存 ActiveResources
语义上表示正在活跃的资源,Engine.load方法首先去ActiveResources中获取缓存。因为其生命周期较短,所以没有大小的限制。ActiveResources 使用一个 Map<Key, WeakReference<EngineResource<?>>> 来存储缓存资源,和一个ReferenceQueue用来跟踪(在IdleHandler回调中处理,不了解IdleHandler是什么的,可以看看这篇Android消息机制详解) WeakReference是否被回收,如果回收则将其移除,
@Override
public boolean queueIdle() {
ResourceWeakReference ref = (ResourceWeakReference) queue.poll();
if (ref != null) {
activeResources.remove(ref.key);
}
return true;
}
BitmapPool
LruBitmapPool
LruBitmapPool主要做一些缓存大小配置、日志记录等操作。主要的缓存实现是交给LruPoolStrategy来完成的。
LruPoolStrategy是一个接口,查看源码看具体的实现类,
//在LruBitmapPool构造函数初始化时调用
private static LruPoolStrategy getDefaultStrategy() {
final LruPoolStrategy strategy;
//可以看出Android对具体的缓存策略做了兼容
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}
return strategy;
}
因为现在大部分Android手机的操作系统都运行在KitKat之上,我们主要来分析一下SizeConfigStrategy。
SizeConfigStrategy
将Bitmap的字节大小和Config一起作为key,将这个Key与Bitmap按照K-V的方式存入GroupedLinkedMap中。还有一个HashMap对象sortedSizes,记录每个Bitmap的size对应在当前缓存中的个数,put 时加一,get 时减一。
GroupedLinkedMap链表是为了支持 LRU 算法,最常使用的 Bitmap 都会移动到链表的前端,使用次数越少就越靠后,当调用 removeLast 方法时就直接调用链表最后一个元素的 removeLast 方法移除元素。
BitmapPoolAdapter
空实现,每次get都创建一个新的Bitmap
MemoryCache 内存缓存
内存的缓存也是使用了LRU算法,LruCache 中使用LinkedHashMap集合来缓存数据。在LruResourceCache有一个监听回调ResourceRemovedListener,Engine就是通过它对Bitmap进行回收操作的。
InternalCacheDiskCacheFactory 磁盘缓存
从源码得知磁盘缓存的目录地址为context.getCacheDir()(比如/data/data/com.xxx.xxx/cache/)下的image_manager_disk_cache,这个目录是固定的。默认的缓存大小为250 MB。InternalCacheDiskCacheFactory类最终会创建一个DiskLruCacheWrapper,它实现了DiskCache接口,
SafeKeyGenerator 类将 Key 对象转换为字符串 (SHA-256 散列码) 。在向磁盘写入文件时,使用重入锁来保证并发安全,实现类是DiskCacheWriteLocker。这个wrapper是一个包装类,真正核心的写入代码在DiskLruCache中。
DiskLruCache
因为使用的是LRU算法,如果不能将这种顺序进行持久化,那么应用重启就会丢失上次的顺序。Glide使用一种自定义的日志文件journal完成,一种典型的journal文件内容如下,
libcore.io.DiskLruCache
1
1
1
CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
DIRTY 335c4c6028171cfddfbaae1a9c313c52
CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
REMOVE 335c4c6028171cfddfbaae1a9c313c52
DIRTY 1ab96a171faeeee38496d8b330771a7a
CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
READ 335c4c6028171cfddfbaae1a9c313c52
READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
其中,开头的libcore.io.DiskLruCache是一个静态常量,用来标识日志文件(类似于文件格式中开头的魔数)。之后的三个数字分别代表,磁盘缓存的版本,应用程序的版本,值计数。
文件中的每个后续行都是高速缓存条目状态的记录。 每行包含以空格分隔的值:状态,键 (SHA-256 散列码) 和可选的特定于状态的值。
- DIRTY行跟踪正在积极创建或更新条目。每个成功的DIRTY动作之后都应执行CLEAN或REMOVE动作。没有匹配的CLEAN或REMOVE的DIRTY行表明可能需要删除临时文件。
- CLEAN行跟踪已成功发布并可以读取的缓存条目。发布行之后是每个值的长度。
- READ线跟踪对LRU的访问。
- REMOVE行跟踪已删除的条目。
总结
Glide的缓存大致分为三层,ActiveResources、MemoryCache(内存缓存)、InternalCacheDiskCacheFactory(磁盘缓存),他们的共同点是都用到了LRU算法。正是因为Glide的这种特点,才使得调用方实现业务逻辑变得简单、轻松。