Glide相信大家并不陌生,我项目里面用的也是Glide,但是一直在用,一直也听别人说它好,但是具体哪儿好呢?今天我就来跟大家分享一下Glide的优点。
作为一个图片框架,最核心的就是其缓存机制,这里我们就来分析一下Glide的缓存机制。Glide采用的是二级缓存:
1.磁盘缓存和内存缓存。磁盘缓存主要是为了防止应用重复从网上下载图片,浪费资源和流量。
2.内存缓存主要是为了防止应用重复的读取本地图片到内存中,从而避免出现大量图片重复创建/回收的问题,从而尽可能的避免内存抖动,减少卡顿。
3.内存缓存分为两级:采用LRUCache的内存缓存(缓存访问过的图片) 和 采用WeakReference的内存缓存(缓存正在使用的图片)
缓存KEY
大家都知道Glide在第一加载图片的时候没有Picasso快,但是在图片已经缓存下来的情况下Glide比Picasso要快的多,这是为什么呢?这是因为Glide缓存的图片是裁剪缩放后的图片,即Glide缓存的图片与控件的宽高有关,就算是同一张图片,如果在界面上显示控件的大小不一样,缓存的依然是不同的两份。Glide首次下载图片后会对图片进行缩放裁剪然后缓存到本地,而Picasso加载到图片后直接进行缩放然后展示出来,所以比Glide要快,而在图片已经加载的情况下,Glide直接拿本地缓存进行展示,不需要再次对图片进行裁剪缩放,所以会比Picasso快的多。那么从哪儿可以看出Glide缓存的图片与图片控件的宽高有关系呢?当然是看生成key的地方咯,生成key的地方在Engine.java里面,我们看看源码
public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId();
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
EngineJob current = jobs.get(key);
if (current != null) {
current.addCallback(cb);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority);
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}
从以上代码可以看出生成EngineKey是通过EngineKeyFactory来生成的,传递的参数有id和width、height等,id就是图片的url,width、height就是图片控件的宽高
缓存
开篇我们说到Glide采用的是两级内存缓存,一个LRUCache缓存访问过的图片,一个WeakReference缓存正在使用的图片。那么这里不知道大家有没有这样一个疑问,为什么要弄两个缓存,直接用一个LRUCache不就行了?哈哈哈,这里就是Glide高级的地方,如果只用一个LRUCache的话,可能会出现正在使用的Bitmap会被回收的情况。假设现在加载了20张图片,那么最后加载的图片自然会在LRUCache的最前面,也就是最近使用的,而正在使用的图片可能会排在LRUCache的后面,也就是说可能会被回收掉,所以这里采用WeakReference来缓存正在使用的图片。上面load方法里面有这样一段代码,就是load的时候会先调用loadFromCache,如果获取不到,会继续调用loadFromActiveResources
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) {
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
loadFromCache()顾名思义就是从内存缓存里面获取,loadFromActiveResources()就是从正在使用的缓存里面获取。我们看看这两个方法的源码
private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> cached = getEngineResourceFromCache(key);
if (cached != null) {
cached.acquire();
// *** 将其放入WeakReference中缓存
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
}
return cached;
}
@SuppressWarnings("unchecked")
private EngineResource<?> getEngineResourceFromCache(Key key) {
// *** 注意,此处用的是cache.remove(key)来获取缓存的,也就是说获取到缓存后会将其移除LRUCache
Resource<?> cached = cache.remove(key);
final EngineResource result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource) cached;
} else {
result = new EngineResource(cached, true /*isCacheable*/);
}
return result;
}
private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = null;
WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
if (activeRef != null) {
active = activeRef.get();
if (active != null) {
// *** 每次调用acquire方法访问计数器acquired都会+1 每次调用release方法的时候-1
active.acquire();
} else {
activeResources.remove(key);
}
}
return active;
}
每次调用loadFromCache()的时候,使用的是MemoryCache.remove(key)可以方法获取的缓存,也就是获取到缓存后会将它从LRUCache里面移除,这样做是为了防止正在被使用的图片被LRUCache清除掉。那么问题来了,一张正再被使用的图片会从LRUCache里面清除,那如果一直滑动,图片不可见的时候岂不是缓存都没有了,因为每次访问后缓存都会从LRUCache里面清除?说到这里不知道大家还记不记得开篇我有提到Glide的内存缓存采用的是LRUCache和WeakReference两级缓存,正在被使用的图片会被WeakReference引用起来,上面代码里面我已经注释了,即loadFromCache()获取到缓存后会调用
activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
那么问题依然存在,只是从LRUCache里面取出缓存并且移除了,而正在使用的图片并没有放进LRUCache里面啊。不知道大家有没有注意到上面代码里面我写了一句注释"每次调用acquire方法会使得对象引用计数器+1,每次调用release引用计数器-1",那么我们就来看看引用计数器-1的代码(大家看注释即可,无需解释)
// Engine.java的release方法
public void release(Resource resource) {
Util.assertMainThread();
if (resource instanceof EngineResource) {
((EngineResource) resource).release();
} else {
throw new IllegalArgumentException("Cannot release anything but an EngineResource");
}
}
// 上面调用的EngineResource的release方法
void release() {
if (acquired <= 0) {
throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
}
if (!Looper.getMainLooper().equals(Looper.myLooper())) {
throw new IllegalThreadStateException("Must call release on the main thread");
}
if (--acquired == 0) {
//acquired为0的时候说明图片没有正在使用
listener.onResourceReleased(key, this);
}
}
// Engine.java实现了上面调用的ResourceListener接口
public void onResourceReleased(Key cacheKey, EngineResource resource) {
Util.assertMainThread();
activeResources.remove(cacheKey);
if (resource.isCacheable()) {
//也就是说暂时不使用的时候会将正在使用的图片放入LRUCache
cache.put(cacheKey, resource);
} else {
resourceRecycler.recycle(resource);
}
}
那么从网络加载图片完成后是如何通知控件的呢,这里采用的是Handler--Message机制,下载图片属于网络请求,必然是在子线程执行,子线程图片加载完毕需要更新UI,自然是通过Handler发送消息来切换到主线程。这里图片加载完毕后是在EngineJob.java里面,我们来看看代码
public void onResourceReady(Resource<?> resource) {
this.resource = resource;
MAIN_THREAD_HANDLER.obtainMessage(1, this).sendToTarget();
}
private void handleResultOnMainThread() {
if(this.isCancelled) {
this.resource.recycle();
} else if(this.cbs.isEmpty()) {
throw new IllegalStateException("Received a resource without any callbacks to notify");
} else {
this.engineResource = this.engineResourceFactory.build(this.resource, this.isCacheable);
this.hasResource = true;
this.engineResource.acquire();
// 这里的listener就是Engine.class
this.listener.onEngineJobComplete(this.key, this.engineResource);
Iterator i$ = this.cbs.iterator();
while(i$.hasNext()) {
ResourceCallback cb = (ResourceCallback)i$.next();
if(!this.isInIgnoredCallbacks(cb)) {
this.engineResource.acquire();
cb.onResourceReady(this.engineResource);
}
}
this.engineResource.release();
}
}
// Engine.java里面的onEngineJobComplete回调
public void onEngineJobComplete(Key key, EngineResource<?> resource) {
Util.assertMainThread();
if(resource != null) {
resource.setResourceListener(key, this);
if(resource.isCacheable()) {
// ***在这里写入缓存
this.activeResources.put(key, new Engine.ResourceWeakReference(key, resource, this.getReferenceQueue()));
}
}
this.jobs.remove(key);
}
磁盘缓存
上面我们分析了两级内存缓存,那么磁盘缓存在哪里呢,在EngineRunnable里面,
private Resource<?> decode() throws Exception {
return this.isDecodingFromCache()?this.decodeFromCache():this.decodeFromSource();
}
private Resource<?> decodeFromCache() throws Exception {
Resource result = null;
try {
//首先从RESULT缓存中取
result = this.decodeJob.decodeResultFromCache();
} catch (Exception var3) {
if(Log.isLoggable("EngineRunnable", 3)) {
Log.d("EngineRunnable", "Exception decoding result from cache: " + var3);
}
}
if(result == null) {
//如果RESULT缓存中没有再从SOURCE缓存中获取
result = this.decodeJob.decodeSourceFromCache();
}
return result;
}
private Resource<?> decodeFromSource() throws Exception {
return this.decodeJob.decodeFromSource();
}
以上decodeResultFromCache 对应DiskCacheStrategy.RESULT decodeSoureFromCache对应DiskCacheStrategy.SOURCE
讲到这里相信大家对Glide的缓存逻辑已经非常清楚了,如果对压缩感兴趣可以查看DownSampler.java,具体细节大家可以自行翻阅源码