一篇文章搞定《图片框架Glide的三级缓存》
前言
Glide的内容太庞大了,弄在一起简直受不了,三级缓存单拿出来说一说吧。
首先三级缓存是Glide中非常重要的缓存机制,也是这种缓存机制才让我们的图片加载的效率,性能如此的高。
下面我们来看看具体的三级缓存内容
三级缓存的读取
首先Glide的三级缓存包含哪些呢?不同于常见的三级缓存(内存,本地,网络)
Glide的三级缓存描述的是以下三级:(当然网络缓存肯定还有,只是Glide所说的三级缓存不包含网络缓存)
获取缓存的源头(前两级,活动缓存和内存缓存)
他发生在Egine.load() 方法中,也就是 Glide 图片加载的核心方法中。(如果不知道这个方法,肯定是没看之前文章的)
Egine.load() 除了创建我们非常重要的EngineJob还有DecodeJob就是我们的缓存策略了。
哦对了,还有一个EngineKey的生成(ps:就是利用多个参数构建的EngineKey的实体,作为缓存中HashMap的Key键)
public <R> Engine.LoadStatus load(GlideContext glideContext, Object model, Key signature, int width, int height, Class<?> resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<?>, Transformation<?>> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0L;
// 1、EngineKey 的生成
EngineKey key = this.keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options);
EngineResource memoryResource;
synchronized(this) {
// 2、活动缓存和内存缓存的加载、获取(这里只是两级的)
memoryResource = this.loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
// 3、EngineJob和DecodeJob的生成(第三级磁盘缓存在DecodeJob中)
return this.waitForExistingOrStartNewJob(glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime);
}
}
//回调 图片资源 (ps:当)
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE, false);
return null;
}
loadFromMemory(这里只是活动缓存和内存缓存)(磁盘缓存在下面会说)
@Nullable
private EngineResource<?> loadFromMemory(EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
} else {
// 1、先从活动缓存中获取
EngineResource<?> active = this.loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
} else {
// 2、从内存中获取
EngineResource<?> cached = this.loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
} else {
return null;
}
}
}
}
获取缓存的源头(第三级,磁盘缓存)
上面说到第三级磁盘缓存在DecodeJob中。为什么分开说呢? 因为磁盘缓存的获取藏得比较深了
if (memoryResource == null) {
// 3、EngineJob和DecodeJob的生成(第三级磁盘缓存在DecodeJob中)
return this.waitForExistingOrStartNewJob(glideContext, model, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, options, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache, cb, callbackExecutor, key, startTime);
}
让我们来具体看一下:
上面前面两级缓存都没有获取到memoryResource 。这个时候调用了waitForExistingOrStartNewJob其实大家猜也猜得到,磁盘缓存在这里。这里稍微细说一下,别嫌弃代码多
waitForExistingOrStartNewJob
private <R> Engine.LoadStatus waitForExistingOrStartNewJob(GlideContext glideContext, Object model, Key signature, int width, int height, Class<?> resourceClass, Class<R> transcodeClass, Priority priority, DiskCacheStrategy diskCacheStrategy, Map<Class<?>, Transformation<?>> transformations, boolean isTransformationRequired, boolean isScaleOnlyOrNoTransform, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean useAnimationPool, boolean onlyRetrieveFromCache, ResourceCallback cb, Executor callbackExecutor, EngineKey key, long startTime) {
.....
//获取缓的EngineJob
.....
//创建新的EngineJob、DecodeJob
EngineJob<R> engineJob = this.engineJobFactory.build(key, isMemoryCacheable, useUnlimitedSourceExecutorPool, useAnimationPool, onlyRetrieveFromCache);
DecodeJob<R> decodeJob = this.decodeJobFactory.build(glideContext, model, key, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations, isTransformationRequired, isScaleOnlyOrNoTransform, onlyRetrieveFromCache, options, engineJob);
this.jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
//加载任务的启动
engineJob.start(decodeJob);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new Engine.LoadStatus(cb, engineJob);
}
}
engineJob.start(decodeJob);
这没什么说的,添加decodeJob任务并执行。
public synchronized void start(DecodeJob<R> decodeJob) {
this.decodeJob = decodeJob;
GlideExecutor executor = decodeJob.willDecodeFromCache() ? this.diskCacheExecutor : this.getActiveSourceExecutor();
executor.execute(decodeJob);
}
哪执行线程,会执行什么呢? 那肯定是run啊!!!还用说(简化一下只看磁盘缓存相关)
public void run() {
.....
try {
if (!this.isCancelled) {
//主要方法
this.runWrapped();
return;
}
} catch (CallbackException var7) {
runWrapped()
这个方法中,代表着加载的三个阶段(代码不细看了,我直接描述一下)
- 一阶段:初始化阶段,调用getNextStage()方法获取下一个阶段,调用getNextGenerator()方法获取下一个生成器,再调用runGenerators()方法执行生成器。
- 二阶段:切换到源服务阶段,调用runGenerators()方法执行生成器。
- 三阶段:解码数据阶段,调用decodeFromRetrievedData()方法解码获取到的数据。
private void runWrapped() {
switch(this.runReason) {
//1、一阶段
case INITIALIZE:
this.stage = this.getNextStage(DecodeJob.Stage.INITIALIZE);
this.currentGenerator = this.getNextGenerator(); //关注点一
this.runGenerators(); //关注点二
break;
//2、二阶段
case SWITCH_TO_SOURCE_SERVICE:
this.runGenerators();
break;
//3、三阶段
case DECODE_DATA:
this.decodeFromRetrievedData();
break;
default:
throw new IllegalStateException("Unrecognized run reason: " + this.runReason);
}
}
我们获取图片那肯定是一阶段:初始化阶段,所以我们看关注点一getNextGenerator()
private DataFetcherGenerator getNextGenerator() {
switch(this.stage) {
case RESOURCE_CACHE:
//磁盘缓存中获取修改后的图片
return new ResourceCacheGenerator(this.decodeHelper, this);
case DATA_CACHE:
//磁盘缓存中获取原始图片
return new DataCacheGenerator(this.decodeHelper, this);
case SOURCE:
//直接网络请求图片
return new SourceGenerator(this.decodeHelper, this);
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: " + this.stage);
}
}
这里只是创造出,三个不同情景下的请求对象而已。具体使用,在(关注点二 runGenerators()方法中)
private void runGenerators() {
........
while(!this.isCancelled && this.currentGenerator != null
&& !(isStarted = this.currentGenerator.startNext())) {
.........
}
......
}
紧接着执行了currentGenerator.startNext()
ResourceCacheGenerator.startNext()
public boolean startNext() {
...
currentKey =
new ResourceCacheKey
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions()); //创建图片资源的key
cacheFile = helper.getDiskCache().get(currentKey); //获取磁盘缓存
... //没有磁盘缓存,
return started;
}
首先为当前图片创建一个用于标识图片唯一性的 key,接着很明显了,调用helper.getDiskCache().get(currentKey) 就是从我们的磁盘缓存中获取图片资源,由于这里是 ResourceCacheGenerator 的 startNext(),所以获取到的资源是经过压缩或者转换后的图片。
要获取未经过压缩及转换的图片的话如何获取呢?首先需要我们在设置 RequestOptions 的 diskCacheStrategy() 时设置的 DiskCacheStrategy 类型是可以缓存原始图片的(ALL、DATA、AUTOMATIC 都可以缓存原始图片),接着 Glide 内部通过调用 DataCacheGenerator 的 startNext() 方法就能获取到原始的图片。
活动缓存详解
- 原理:弱引用的HashMap
Map<Key, ActiveResources.ResourceWeakReference> activeEngineResources;
- 范围:当前Activity的生命周期中
- 作用:分担内存缓存压力、及时的释放内存。
- 拿取优先级:优先从活动缓存获取
- 源码解析证明
获取EngineResource<?> loadFromActiveResources(Key key)
@Nullable
private EngineResource<?> loadFromActiveResources(Key key) {
EngineResource<?> active = this.activeResources.get(key);
if (active != null) {
active.acquire();
}
return active;
}
@Nullable
synchronized EngineResource<?> get(Key key) {
ActiveResources.ResourceWeakReference activeRef =
(ActiveResources.ResourceWeakReference)this.activeEngineResources.get(key);
if (activeRef == null) {
return null;
} else {
EngineResource<?> active = (EngineResource)activeRef.get();
if (active == null) {
this.cleanupActiveReference(activeRef);
}
return active;
}
}
可以看到就是从弱引用Map中获取。(怎么放的别着急,后面会说。这里只是将活动缓存概念说一下)
内存缓存详解
- 原理:LruCache是利用LinkedHashMap的LRU进行管理的,并进行包装提供MemoryCache接口类的一些方法,比如put、remove
public class LruCache<T, Y> {
private final Map<T, LruCache.Entry<Y>> cache = new LinkedHashMap(100, 0.75F, true);
- 范围:某个App范围,应用完全退出就不存在
- 作用:加快数据读取、减少磁盘IO操作和网络请求
- 拿取优先级:活动缓存没有就在内存缓存中寻找
- 源码解析证明
获取EngineResource<?> cached = this.loadFromCache(key);
private EngineResource<?> loadFromCache(Key key) {
//1、步骤一:获取资源
EngineResource<?> cached = this.getEngineResourceFromCache(key);
if (cached != null) {
//2、步骤二:图片使用次数计数
cached.acquire();
//3、步骤三:给活动缓存
this.activeResources.activate(key, cached);
}
return cached;
}
步骤一:获取资源getEngineResourceFromCache
分为如下两步:
- 通过Key获取图片资源,并移除(这个是LruCache管理的功劳,后面我们来手写LRU算法!!!)
- 包装EngineResource并返回
private EngineResource<?> getEngineResourceFromCache(Key key) {
//1、通过Key获取图片资源,并移除
Resource<?> cached = this.cache.remove(key);
EngineResource result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
result = (EngineResource)cached;
} else {
result = new EngineResource(cached, true, true, key, this);
}
return result;
}
步骤二:对图片的使用进行计数
步骤三:把图片缓存通过弱引用包装,放到活动缓存的Map中进行管理
synchronized void activate(Key key, EngineResource<?> resource) {
ActiveResources.ResourceWeakReference toPut = new ActiveResources.ResourceWeakReference(key, resource, this.resourceReferenceQueue, this.isActiveResourceRetentionAllowed);
ActiveResources.ResourceWeakReference removed = (ActiveResources.ResourceWeakReference)this.activeEngineResources.put(key, toPut);
if (removed != null) {
removed.reset();
}
}
OK!内存缓存也说完了,并且还解答了活动缓存时用哪里来了,就是在内存缓存中,被使用了,就会将缓存交给活动缓存区管理(后面会将活动缓存的作用和设计初衷)
磁盘缓存详解
- 原理:DiskLruCache是利用LinkedHashMap的LRU进行管理的,利用DiskCache接口和DiskLruCacheWrapper包装类进行使用。
public final class DiskLruCache implements Closeable {
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true);
当前管理的DiskLruCache的LRU的LinkedHashMap中的Key和Value分别为:
Key:表示缓存条目的键,即每个缓存条目的唯一标识(根据URL获取的)。
Value:表示缓存条目的具体内容,是一个Entry对象,其中包含了缓存条目的文件路径、缓存大小、缓存的读写锁等信息。
- 范围:整个系统,只要不删除数据,就一直存在
- 作用:进行永久性保存
- 拿取优先级:内存缓存没有,就去磁盘缓存读取
- 源码证明:已经在上面的获取缓存源头(第三级,磁盘缓存中进行了详细的说明)
public boolean startNext() {
//1、磁盘缓存中获取
cacheFile = helper.getDiskCache().get(currentKey);
if (cacheFile != null) {
sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
while (!started && hasNextModelLoader()) {
...
// 2、网络加载
loadData = modelLoader.buildLoadData(
cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
....
}
当磁盘缓存没有获取到,就需要进行网络加载了。
三级缓存的写入
我们从网络请求一张图片开始吧。
磁盘缓存写入
因为磁盘缓存中没有该图片,从而引发起的网络请求返回了图片:
public void loadData(@NonNull Priority priority,
@NonNull DataCallback<? super InputStream> callback) {
long startTime = LogTime.getLogTime();
try {
InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders()); //进行网络请求
callback.onDataReady(result); //调用 SourceGenerator的onDataReady()方法
}
...
}
这里通过InputStream流从网络中获取了图片,回调给了onDataReady
onDataReady做了两件事
- 赋值dataToCache对象图片数据,回调cb.reschedule()触发SourceGenerator 的 startNext() 方法进行缓存原图
- 直接回调给onDataFetcherReady(不需要缓存的话)
public void onDataReady(Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
//如果请求返回的数据 data 不为空且需要缓存原始数据,就将 data 赋值给dataToCache,
//接着调用 cb.reschedule() 会再一次进入到 SourceGenerator 的 startNext() 方法
if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
dataToCache = data;
cb.reschedule();
} else {
cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
loadData.fetcher.getDataSource(), originalKey);
}
}
紧接着,在磁盘获取的时候 getNextGenerator() 如果返回的是 SourceGenerator 时,表明需要去请求图片,进入 SourceGenerator 的 startNext() 方法:(这个源码上面在获取缓存源头(第三级中有说到))
case SOURCE:
//直接网络请求图片
return new SourceGenerator(this.decodeHelper, this);
public boolean startNext() {
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
cacheData(data); //在这里
}
...
}
那么看看cacheData做了什么
- 编码(压缩或转换)图片encoder
- 写入磁盘缓存(原图)
- 创建DataCacheGenerator写入压缩、编码后的图片到磁盘缓存中
private void cacheData(Object dataToCache) {
long startTime = LogTime.getLogTime();
try {
Encoder<Object> encoder = helper.getSourceEncoder(dataToCache);
DataCacheWriter<Object> writer =
new DataCacheWriter<>(encoder, dataToCache, helper.getOptions());
originalKey = new DataCacheKey(loadData.sourceKey, helper.getSignature());
// 写入磁盘缓存
helper.getDiskCache().put(originalKey, writer);
...
} finally {
loadData.fetcher.cleanup();
}
//
sourceCacheGenerator =
new DataCacheGenerator(Collections.singletonList(loadData.sourceKey), helper, this);
}
这样就把一个网络请求的图片写入到了磁盘缓存
活动缓存写入
那么内存缓存是什么时候写入的呢?
当我们写入磁盘后,就可以从磁盘获取到图片,从而执行前面说到的onResourceReady方法
public void onResourceReady(Resource<R> resource, DataSource dataSource) {
this.resource = resource;
this.dataSource = dataSource;
MAIN_THREAD_HANDLER.obtainMessage(MSG_COMPLETE, this).sendToTarget();
}
回调handleResultOnMainThread()方法
private static class MainThreadCallback implements Handler.Callback {
@Synthetic
@SuppressWarnings("WeakerAccess")
MainThreadCallback() { }
@Override
public boolean handleMessage(Message message) {
EngineJob<?> job = (EngineJob<?>) message.obj;
switch (message.what) {
case MSG_COMPLETE:
job.handleResultOnMainThread();
......
}
return true;
}
}
那我们来到handleResultOnMainThread() 方法,进去看看都做了什么:
- 第一步:使用计数+1,为了放入活动缓存中准备
- 第二步:回调给EngineJob
- 第三步: 使用计数-1,为了放入活动缓存中准备
void handleResultOnMainThread() {
...
engineResource = engineResourceFactory.build(resource, isCacheable);
hasResource = true;
//第一步:使用计数,为了放入活动缓存中准备
engineResource.acquire();
//第二步:回调给EngineJob
listener.onEngineJobComplete(this, key, engineResource);
for (int i = 0, size = cbs.size(); i < size; i++) {
ResourceCallback cb = cbs.get(i);
if (!isInIgnoredCallbacks(cb)) {
engineResource.acquire();
cb.onResourceReady(engineResource, dataSource);
}
}
//第三步:使用计数,为了放入活动缓存中准备
engineResource.release();
release(false /*isRemovedFromQueue*/);
}
先来看第二步:回调给EngineJob都做了什么?
很明显,如果 resource 不为空调用 activeResources.activate(),这个方法就是将我们这里的 resource 存入了弱引用缓存中。
public void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
Util.assertMainThread();
if (resource != null) {
resource.setResourceListener(key, this);
if (resource.isCacheable()) {
activeResources.activate(key, resource);
}
}
jobs.removeIfCurrent(key, engineJob);
}
void activate(Key key, EngineResource<?> resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key,
resource,
getReferenceQueue(),
isActiveResourceRetentionAllowed);
ResourceWeakReference removed = activeEngineResources.put(key, toPut); //传入弱引用缓存
if (removed != null) {
removed.reset();
}
}
至此弱引用缓存也被写入。
内存缓存写入
那么内存缓存又是什么时候写入的呢?
其实这个在前面已经讲到了。
engineResource 的 acquire() 和 release() 方法:
acquire() 每调用一次引用计数 acquired 加1
release() 方法每调用一次 acquired 减1
引用计数 acquired 表示当前正在使用资源的使用者数,大于0表示资源正在使用中,值为0表示没有使用者使用此刻就需要将它写入内存缓存中,release() 中调用 onResourceReleased() 将没有使用的资源写入内存缓存,仍然又回到了 Engine 中的 onResourceReleased() 方法:
private final MemoryCache cache;
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
Util.assertMainThread();
activeResources.deactivate(cacheKey); //先从弱引用缓存中移除
if (resource.isCacheable()) {
cache.put(cacheKey, resource); //写入内存缓存
} else {
resourceRecycler.recycle(resource);
}
}
由于这个图片当前已没有使用者,调用 activeResources.deactivate() 先把它从弱引用缓存中清除,然后就是将数据写入内存缓存,cache 是 MemoryCache 类型的,MemoryCache 是一个接口类它的实现者就是 LruCache。
由此可见,正在使用的图片使用 activeResources 以弱引用的方式保存起来,Glide 给图片设置了一个引用计数变量 acquired 用于统计图片当前的引用数,acquired 为0即为图片没有使用者,就将图片从弱引用缓存中移除然后保存到 LruCache 中。也就是内存缓存中。
总结:
我们从三级缓存读取的源头、到三级缓存的获取、到三级缓存的写入都分析了。
简单总结来说:
- 缓存读取顺序:
活动缓存 -> LruCache内存缓存 -> DiskLruCache磁盘缓存 - 缓存写入顺序:
DiskLruCache 磁盘缓存原图 -> 活动缓存 -> LruCache内存缓存(acquired为0时加入)
活动缓存(弱引用缓存)的作用
通过下面三个功能,解释他的作用
功能一:
首先通过上面的知识,我们知道活动缓存的写入有如下两个时机:
- 网络请求后、加入磁盘缓存、之后在使用时被engineResource.acquire(); 计数+1后加入到活动缓存中
- 在内存缓存LRU中获取到缓存后、在使用时被engineResource.acquire(); 计数+1后加入到活动缓存中
只有在engineResource.release(); 计数-1后加入到内存缓存中。不然每次都加载到内存缓存中,会让内存缓存中超负荷,压力很大的呢!!!!
功能二:
采用弱引用对象引用的方式,在GC回收时会立马被回收,减少内存泄漏的情况发生。
功能三:
防止LRU回收当前正在使用的对象,算法遵循近期使用频率和最近使用时间的原则。
基本思想是,较长时间未被使用的页面会被认为是最近最少使用的,因此应该最先被淘汰出去。
在使用频繁大量的图片时会出现回收正在使用的图片的情况。
总结:
- 减轻内存缓存压力
- 减少内存泄漏问题
- 防止LRU回收正在使用的资源
缓存的配置使用
自定义一些缓存的大小
@GlideModule
public final class CustomGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// 设置缓存大小为20mb
int memoryCacheSizeBytes = 1024 * 1024 * 20; // 20mb
// 设置内存缓存大小
builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
// 根据SD卡是否可用选择是在内部缓存还是SD卡缓存
if(SDCardUtils.isSDCardEnable()){
builder.setDiskCache(new ExternalPreferredCacheDiskCacheFactory(context, "HYManagerImages", memoryCacheSizeBytes));
}else {
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, "HYManagerImages", memoryCacheSizeBytes));
}
}
// 针对V4用户可以提升速度
@Override
public boolean isManifestParsingEnabled() {
return false;
}
}
Glide跳过内存缓存
GlideApp.with(context)
.load(url)
.skipMemoryCache(true)//默认为false
.dontAnimate()
.centerCrop()
.into(imageView);
Glide磁盘缓存策略配置
GlideApp.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.dontAnimate()
.centerCrop()
.into(imageView);
/*默认的策略是DiskCacheStrategy.AUTOMATIC
DiskCacheStrategy有五个常量:
DiskCacheStrategy.ALL 使用DATA和RESOURCE缓存远程数据,仅使用RESOURCE来缓存本地数据。
DiskCacheStrategy.NONE 不使用磁盘缓存
DiskCacheStrategy.DATA 在资源解码前就将原始数据写入磁盘缓存
DiskCacheStrategy.RESOURCE 在资源解码后将数据写入磁盘缓存,即经过缩放等转换后的图片资源。
DiskCacheStrategy.AUTOMATIC 根据原始图片数据和资源编码策略来自动选择磁盘缓存策略。*/
缓存清理
//磁盘缓存清理(子线程)
GlideApp.get(context).clearDiskCache();
//内存缓存清理(主线程)
GlideApp.get(context).clearMemory();
手写LRU算法
对Android中LRU算法的解释
LRU算法遵循近期使用频率和最近使用时间的原则。基本思想是,较长时间未被使用的页面会被认为是最近最少使用的,因此应该最先被淘汰出去。
LRU算法在Android中的实现是使用了Api中new LinkedHashMap
private final Map<T, LruCache.Entry<Y>> cache = new LinkedHashMap(100, 0.75F, true);
解释一下他的三个参数:
- 参数一(100):容量参数,表示该LinkedHashMap可以存储的最大元素数量。当超过这个数量时,LinkedHashMap会自动按照LRU算法原则淘汰最近最少使用的元素。
- 参数二(0.75F):加载因子,表示元素填充到容量的百分比。当达到这个百分比时,LinkedHashMap将会进行扩容操作,以减少哈希冲突的概率。
- 参数三(true):访问顺序参数,如果将此参数设置为true,则LinkedHashMap将会按照访问顺序(最近访问的放在末尾,最早访问的放在头部)进行排序;如果将其设置为false,则LinkedHashMap将按照插入顺序进行排序。
也就是通过调整容量、加载因子和访问顺序等参数,可以控制缓存的大小和淘汰策略,用于优化对于最近使用频率高的元素的快速访问。
LRU算法的实现
让我们来手写对LRU算法的实现吧:
实现之前需要明确的几个点:
LRU 缓存机制可以通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
- 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的。
- 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置。
这样一来,我们首先使用哈希表进行定位,找出缓存项在双向链表中的位置,随后将其移动到双向链表的头部,即可在 O(1)O(1)O(1) 的时间内完成 get 或者 put 操作。具体的方法如下:
- 对于 get 操作,首先判断 key 是否存在:
- 如果 key 不存在,则返回 −1-1−1;
- 如果 key 存在,则 key 对应的节点是最近被使用的节点。通过哈希表定位到该节点在双向链表中的位置,并将其移动到双向链表的头部,最后返回该节点的值。
- 对于 put 操作,首先判断 key 是否存在:
- 如果 key 不存在,使用 key 和 value 创建一个新的节点,在双向链表的头部添加该节点,并将 key 和该节点添加进哈希表中。然后判断双向链表的节点数是否超出容量,如果超出容量,则删除双向链表的尾部节点,并删除哈希表中对应的项;
- 如果 key 存在,则与 get 操作类似,先通过哈希表定位,再将对应的节点的值更新为 value,并将该节点移到双向链表的头部。
上述各项操作中,访问哈希表的时间复杂度为 O(1)O(1)O(1),在双向链表的头部添加节点、在双向链表的尾部删除节点的复杂度也为 O(1)O(1)O(1)。
而将一个节点移到双向链表的头部,可以分成「删除该节点」和「在双向链表的头部添加节点」两步操作,都可以在 O(1)O(1)O(1) 时间内完成。
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
}
总结
加油!!!!