注册

Glide源码解析

本次源码解析基于4.12.0,如有描述错误,请大佬们评论指出。

一、Glide的用法

 // RecyclerView中加载图片
@Override
public void onBindViewHolder(PhotoViewHolder holder, int position) {
GlideApp.with(holder.itemView).load(list.get(position))
.transform(new RoundedCorners(40))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.placeholder(R.drawable.ic_launcher)
.error(R.drawable.ic_launcher)
.into(holder.imageView);
}

二、Glide一些面试常考点

  • 2.1、 Glide如何感知Application、Activity、Fragment的生命周期?

Q:先问下如果你想感知application的那几个内存不足的方法,你会怎么做。
ComponentCallbacks2是系统提供的类。 image.png image.png

Application类管理这些订阅者,方法回调时,遍历通知。 image.png

Glide中的trimMemory收到事件通知后的已做处理,不需要我们自己再去清理Glide的资源占用。 image.png

Q:页面如果有ImageView,加载图片时立马页面关闭/返回,此时应该停止加载,Glide如何感知Activity/Fragment的onDestroy呢?

当然是在对应的Act或者Fragment中插入空白SupportRequestManagerFragment实现。

GlideApp.with(activity).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);
GlideApp.with(fragment).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);

image.png

image.png 如果说我们的Activity有个ImageView,它里面有两个Fragment也加载ImageView,按照规范,with方法应该是基于ImageView所处的context来决定,该传Act就传Act,该传Fragment就传Framgent没错,这样一来,Glide就嵌入3个SupportRequestManagerFragment进入了我们的页面。有点厉害哦。

但是如果Fragment里面不小心写成了下面这样

//fragment中的ImageView
GlideApp.with(view).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);

用一个简单的demo模拟这种场景,Glide从View去找Fragment竟然找不到, \color{#ff0000}{既然找不到View所在的Fragment容器,那就只能用跟Activity保持一致了}既然找不到View所在的Fragment容器,那就只能用跟Activity保持一致了 所以小伙子们写with方法的时候要注意啊。(记得之前我用过kotlin中Fragment拓展方法,可以通过View找到其所在的Fragment)

image.png image.png

  • 2.2、 Glide的MemoryCache(LruResourceCache)和LruBitmapPool以及DiskLruCache默认size多大呢?

    final int MEMORY_CACHE_TARGET_SCREENS = 2;
final int BITMAP_POOL_TARGET_SCREENS =Build.VERSION.SDK_INT < Build.VERSION_CODES.O ? 4 : 1;
int widthPixels =context.getResources().getDisplayMetrics().widthPixels;
int heightPixels =context.getResources().getDisplayMetrics().heightPixels;
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
//8及其8以上4张图图片的size
int targetBitmapPoolSize = Math.round(screenSize * BITMAP_POOL_TARGET_SCREENS);
//2张屏幕大小的size
int targetMemoryCacheSize = Math.round(screenSize * MEMORY_CACHE_TARGET_SCREENS);

int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;
String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
File cacheDirectory = context.getCacheDir();

LruResourceCache默认: 只有2张屏幕大小的图片size
LruBitmapPool默认: 只有1 or 4张的屏幕大小的Bitmap可以复用;
DiskLruCache默认: 在内置SD卡且占用空间250M

image.png

  • 2.3、 三级缓存的添加和移除发生在什么时机?

    弱引用缓存(ResourceWeakReference)   内存缓存(LruResourceCache)   磁盘缓存(DiskLruCache)

    同一个Bitmap一旦从LruResourceCache取出(remove)了,那它就只有弱引用缓存了,如果弱引用清除时,就是LruCache加入缓存时,看样子二者不能共存?

    目前测试的结果:一旦图片加载ok了,先加入弱引用缓存,如果是recyclerView列表,item复用,ImageView会被into很多次,该Bitmap对应的弱引用早就没了,此时Bitmap会加入LruResourceCache。如果不是列表是页面加载的ImageView,当页面关闭时,Bitmap的弱引用清除,此时会加入LruResourceCache。如果LruResourceCache内存不够,那就进行trim。

    DiskLruCache下文讲。

  • 2.4、 Glide如何区分一个Url的内容是png,还是jpg,还是gif的呢?

先让大家看一下内容,后面会贯通讲下。

image.png

  • 2.5、  设置BitmapFactory.Options.inBitmap作用

BitmapFactory.Options.inBitmap = lruBitmapPool.getDirty(width, height, expectedConfig)
inBitmap表示要复用Bitmap,该Bitmap就来自LruBitmapPool,由于这个池本身容纳的bitmap数量有限,能提供还好,不能提供它还是直接createBitmap(新创建)返回Bitmap。

Q:如果BitmapPool中的有Bitmap存在,是不是一定可以复用?有没有啥限制?

@Override
public void reschedule() {
runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
getActiveSourceExecutor().execute(job);
}

image.png

第二次调用SourceGenerator的startNext就准备缓存到磁盘,这个缓存的就是源数据。

private void cacheData(Object dataToCache) {
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);
}

存了之后,就用NIO的方式读取刚刚缓存在磁盘里面的文件,这一套操作,是不是有点慢了,先存到本地再读取file获取ByteBuffer。

image.png

  • 3.2.3、根据DirectByteBuffer解码出Resource(Bitmap)

我们这里load进去的url就是一张图片,对应三条解码路径:

  • DirectByteBuffer->GifDrawable->Drawable
  • DirectByteBuffer->Bitmap->Drawable
  • DirectByteBuffer->BitmapDrawable->Drawable

但是不确定是哪一条,那就都试试,发现每次都从gif类型(ByteBufferGifDecoder)开始,不知是不是特意为之,如果类型不匹配就换下一个。

private Resource<ResourceType> decodeResourceWithList(DataRewinder<DataType> rewinder....)
throws GlideException {
Resource<ResourceType> result = null;
for (int i = 0, size = decoders.size(); i < size; i++) {
ResourceDecoder<DataType, ResourceType> decoder = decoders.get(i);
try {
DataType data = rewinder.rewindAndGet();
if (decoder.handles(data, options)) { //gif类型需要通过获取文件类型判断,bitmap则直接true
data = rewinder.rewindAndGet(); //重置buffer读取的位置到起始位置
result = decoder.decode(data, width, height, options);
}
} catch (IOException | RuntimeException | OutOfMemoryError e) {
exceptions.add(e);
}
if (result != null) {
break;
}
}
if (result == null) {
throw new GlideException(failureMessage, new ArrayList<>(exceptions));
}
return result;
}

image.png

解析ByteBuffer的文件类型关键代码来了:

// DefaultImageHeaderParser
@NonNull
private ImageType getType(Reader reader) throws IOException {
try {
final int firstTwoBytes = reader.getUInt16();
// JPEG.类型读取两个字节就可以判断了
if (firstTwoBytes == EXIF_MAGIC_NUMBER) {
return JPEG;
}
//gif要读3字节
final int firstThreeBytes = (firstTwoBytes << 8) | reader.getUInt8();
if (firstThreeBytes == GIF_HEADER) {
return GIF;
}
//png要读4字节
final int firstFourBytes = (firstThreeBytes << 8) | reader.getUInt8();
if (firstFourBytes == PNG_HEADER) {
reader.skip(25 - 4);
try {
int alpha = reader.getUInt8();
return alpha >= 3 ? PNG_A : PNG;
} catch (Reader.EndOfFileException e) {
return PNG;
}
}
//更多其他类型不列举了
// WebP (reads up to 21 bytes).
......
return UNKNOWN;
}
}

每一次尝试,缓冲区都会读一些字节,下次尝试还是要从头开始,此时就需要重置位置为0,所以搞了个ByteBufferRewinder(rewind--倒带)来干这事。 image.png

很明显,我们这个不是Gif图,那就换下一个试试ByteBufferBitmapDecoder。 image.png

先将ByteBuffer转换InputStream,看到InputStream,是不是跟Bitmap很近了,它先获取流中Bitmap的宽高和是否有旋转角度,以及是否配置Target.SIZE_ORIGINAL来调整目标宽高,一般来说,图片无旋转,且图片没有显式配置是Target.SIZE_ORIGINAL,那么目标宽高就是我们之前获取的宽高(不记得了就看上面的)。

然后再次检测文件类型(不明白之前已经尝试gif类型判断时,已经得出了图片类型,但是它没保存,此时还要再获取一次,差评!),基于scaleType综合考虑采样率,代码太多了,就不贴了。在流保存Bitmap之前,设置Bitmap走复用。

private static void setInBitmap(
BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height)
{
.....
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
.....
}
.......//一系列配置整完后 bitmap操作开始了
Bitmap downsampled = BitmapFactory.decodeStream(dataRewinder.rewindAndGet(), null, options);
callbacks.onDecodeComplete(bitmapPool, downsampled);
Bitmap rotated = null;
if (downsampled != null) {
downsampled.setDensity(displayMetrics.densityDpi);
//开始旋转Bitmap了,又是很好的可以抄袭的地方,以后有旋转bitmap的场景也这么干
rotated = TransformationUtils.rotateImageExif(bitmapPool, downsampled, orientation);
if (!downsampled.equals(rotated)) {
bitmapPool.put(downsampled);
}
}
return rotated;

image.png

  • 3.2.4、目标bitmap获取到,还要transform下,就是我们设置的什么圆角操作等啦。

public Resource<Transcode> decode(....){
//Resource<ResourceType> 就是 Resource<Bitmap>--->相当于拿到bitmap
Resource<ResourceType> decoded = decodeResource(rewinder, width, height, options);
//对bitmap做转换
Resource<ResourceType> transformed = callback.onResourceDecoded(decoded);
return transcoder.transcode(transformed, options);
}

callback.onResourceDecoded(decoded)很关键

<Z> Resource<Z> onResourceDecoded(DataSource dataSource, @NonNull Resource<Z> decoded) {
Class<Z> resourceSubClass = (Class<Z>) decoded.get().getClass();
Transformation<Z> appliedTransformation = null;
Resource<Z> transformed = decoded;
//磁盘缓存策略在这里发挥作用
if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
//选取其中一个跟Bitmap匹配的Transformation操作
appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
//应用操作
transformed = appliedTransformation.transform(glideContext, decoded, width, height);
}
//应用完之后,旧的bitmap直接让其回收
if (!decoded.equals(transformed)) {
decoded.recycle();
}
......
//DiskCacheStrategy.DATA的isResourceCacheable默认就是false了
//DiskCacheStrategy.AUTOMATIC经过了几重不明所以的判断,isFromAlternateCacheKey=false,导致也是false
//但是不影响,因为之前已经在本地缓存过一次源数据了
//所以这里专门为DiskCacheStrategy.RESOURCE和DiskCacheStrategy.ALL使用
if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource, encodeStrategy)) {
.....
final Key key;
switch (encodeStrategy) {
case SOURCE: //源数据,,不太可能会走这个逻辑
key = new DataCacheKey(currentSourceKey, signature);
break;
case TRANSFORMED: //转换后的bitmap对应的key
key = new ResourceCacheKey(decodeHelper.getArrayPool(), currentSourceKey, signature,
width, height,appliedTransformation, resourceSubClass, options);
break;
.....
}
LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
//拿到key,但是没有做缓存操作,因为defer是延迟处理的,后面会很快存转换后的数据到磁盘
deferredEncodeManager.init(key, encoder, lockedResult);
result = lockedResult;
}
return result;
}

image.png image.png 圆角的处理,以后有这种需求,也这么干。

  • 3.2.5、通知bitmap就绪了且按需保存转换的数据到磁盘。

 private void decodeFromRetrievedData() {
Resource<Bitmap> nresource = decodeFromData(currentFetcher, currentData, currentDataSource);
notifyEncodeAndRelease(resource, currentDataSource, isLoadingFromAlternateCacheKey);
}

//resource就是bitmap
private void notifyEncodeAndRelease(Resource<Bitmap> resource, DataSource dataSource, boolean isLoadedFromAlternateCacheKey) {
if (resource instanceof Initializable) {
// bitmap.prepareToDraw(); 预先将bitmap加载到gpu上
((Initializable) resource).get().prepareToDraw();
}
....
//通知engine以及回调给用户onResourceReady
....
//这里真正开始写入转换的后的数据
if (deferredEncodeManager.hasResourceToEncode()) {
deferredEncodeManager.encode(diskCacheProvider, options);
}
.....
}

//deferredEncodeManager //这里真正开始写入转换的后的数据
void encode(DiskCacheProvider diskCacheProvider, Options options) {
GlideTrace.beginSection("DecodeJob.encode");
try {
//bitmap缓存为file
diskCacheProvider.getDiskCache().put(key, new DataCacheWriter<>(encoder, toEncode, options));
} finally {
toEncode.unlock();
GlideTrace.endSection();
}
}
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Z> transition) {
if (transition == null || !transition.transition(resource, this)) {
//终于看到ImageView显示图片了
imageView.setImageBitmap(resource);
} else {
maybeUpdateAnimatable(resource);
}
}
//DiskCache进行put时,就会调用DataCacheWriter的wirte方法
//wirte方法就调用encoder的encode方法,将bitmap缓存到文件
public boolean encode( Resource<Bitmap> resource, File file, Options options) {
final Bitmap bitmap = resource.get();
Bitmap.CompressFormat format = getFormat(bitmap, options);
try {
int quality = options.get(COMPRESSION_QUALITY);
boolean success = false;
OutputStream os = null;
try {
os = new FileOutputStream(file);
if (arrayPool != null) {
os = new BufferedOutputStream(os, arrayPool);
}
bitmap.compress(format, quality, os);
os.close();
success = true;
} catch (IOException e) {
....
} finally {
if (os != null) {
try {
os.close();
} catch (IOException e) {
// Do nothing.
}
}
}
return success;
} finally {
GlideTrace.endSection();
}
}

大家仔细看下上面代码的注释。 当看到imageView.setImageBitmap(bitmap) 后,整个逻辑就走完了。

四、从以上加载流程来提出问题

  • 4.1、 DiskCacheStrategy.RESOURCE、DiskCacheStrategy.DATA、DiskCacheStrategy.AUTOMATIC有啥区别?

尽管DiskCacheStrategy.AUTOMATIC是默认,听说很智能,智能个鬼,从简单加载url显示bitmap来看,我暂时看不出它跟DiskCacheStrategy.DATA有啥子区别。
DiskCacheStrategy.RESOURCE:只缓存bitmap转换后的数据到磁盘,在SourceGenerator的startNextLoad去加载网络资源,下载回调返回的是流数据,直接拿着数据流,去解析,解析ok,最后转成Bitmap,bitmap根据用户设置的transform或者默认transform做一次转换,最后将转换后的bitmap缓存到磁盘。
DiskCacheStrategy.DATA:只缓存源数据到磁盘。在SourceGenerator的startNextLoad去加载网络资源,下载回调返回的是流数据,然后将流数据缓存以源数据缓存到磁盘,然后将本地的磁盘缓存的源数据file使用NIO读取为DirectByteBuffer,然后对这个byteBuffer进行一系列的解析处理:可以解析,就将bytebuffer转成inputStream,最后转成bitmap,后面流程差不多一样了。

如果让我选择磁盘缓存策略,我会优先选DiskCacheStrategy.RESOURCE,至少在我看来从默认的设置AUTOMATIC没看到优点。不知道有没有啥副作用啊。

  • 4.2、 实际场景中弱引用、MemoryCache添加移除时机?

首次从网络加载图片,当bitmap一切就绪,在ImageView上设置Bitmap时会发通知完成回调,此时资源bitmap的弱引用会被添加,,,此时LruCache中没有Bitmap哦,不要以为bitmap此时也加入到LruCache中了。

public synchronized void onEngineJobComplete(....) {
if (resource != null && resource.isMemoryCacheable()) {
//此时加入弱引用缓存中
activeResources.activate(key, resource);
}
.....
}

那LruCache的添加操作在何时呢?当资源释放的时候,比如我们的页面(含有Glide加载ImageVeiw)关闭,或者recyclerView的Item列表滑动复用item时,会触发弱引用的清除和LruCache对资源的添加。

@Override
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
//资源释放的时候,就清空弱引用先将它放入队列里面的
activeResources.deactivate(cacheKey);
if (resource.isMemoryCacheable()) {
//资源释放的时候,弱引用清楚,此时Lru缓存加入进去
cache.put(cacheKey, resource);
}
.....
}
synchronized void deactivate(Key key) {
ResourceWeakReference removed = activeEngineResources.remove(key);
if (removed != null) {
removed.reset();
}
}

页面关闭/返回 image.png

recyclerView列表滑动 image.png

那MemoryCache缓存中何时取出,又是何时添加的
其实就是在发起请求前,Engine先从内存缓存中取,有就直接通知回调,没有就走后面一系列流程。

private EngineResource<?> loadFromMemory(EngineKey key...) {
if (!isMemoryCacheable) { //跳过缓存
return null;
}
//从弱引用ResourceWeakReference中查找
EngineResource<?> active = = activeResources.get(key);
if (active != null) {
active.acquire(); //资源被使用,引用++
return active;
}
//从MemoryCache中找,找出来就是从LruCache中移除,remove的返回值就是啊
EngineResource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
result = (EngineResource<?>) cached;
} else {
result = new EngineResource<>( cached, true,true, key,this);
}
if (result != null) {
//资源被使用,引用++ 且 添加到弱引用中
result.acquire();
activeResources.activate(key, result);
return result;
}
return null;
}

看样子,资源弱引用存在,那LruResourceCache就不可能存在这个资源,二者属于不同阶段的一个相互补充,没得交集。

五、后续

本期只是针对load(url)做了一个简单的操作流转的记录,这个记录贯穿了一系列的知识点,对Glide的了解还是比较浅,后续对其ModelLoader、Gif、video加载这块,也做个补充吧。

0 个评论

要回复文章请先登录注册