本次源码解析基于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是系统提供的类。
Application类管理这些订阅者,方法回调时,遍历通知。
Glide中的trimMemory收到事件通知后的已做处理,不需要我们自己再去清理Glide的资源占用。
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);
复制代码
如果说我们的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竟然找不到, 所以小伙子们写with方法的时候要注意啊。(记得之前我用过kotlin中Fragment拓展方法,可以通过View找到其所在的Fragment)
-
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。
-
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的呢?
先让大家看一下内容,后面会贯通讲下。
-
2.5、 设置BitmapFactory.Options.inBitmap作用
BitmapFactory.Options.inBitmap = lruBitmapPool.getDirty(width, height, expectedConfig)
inBitmap表示要复用Bitmap,该Bitmap就来自LruBitmapPool,由于这个池本身容纳的bitmap数量有限,能提供还好,不能提供它还是直接createBitmap(新创建)返回Bitmap。
Q:如果BitmapPool中的有Bitmap存在,是不是一定可以复用?有没有啥限制?
//缓冲池策略
private static LruPoolStrategy getDefaultStrategy() {
final LruPoolStrategy strategy;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}
return strategy;
}
//SizeConfigStrategy 高版本就是size config做key
public boolean equals(Object o) {
if (o instanceof Key) {
Key other = (Key) o;
return size == other.size && Util.bothNullOrEqual(config, other.config);
}
return false;
}
//AttributeStrategy 低版本严格按照宽高config做key
public boolean equals(Object o) {
if (o instanceof Key) {
Key other = (Key) o;
return width == other.width && height == other.height && config == other.config;
}
return false;
}
public Bitmap getDirty(int width, int height, Bitmap.Config config) {
final Bitmap result = strategy.get(width, height, config != null ? config : Bitmap.Config.ARGB_8888);
if (result == null) {
......
} else {
.....
currentSize -= strategy.getSize(result);
tracker.remove(result); //如果bitmap被选中,那么一定会从池中remove出来
bitmap.setHasAlpha(true); //选择的bitmap要做点处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
bitmap.setPremultiplied(true); //虽不知道有啥用,以后如果有复用bitmap场景,也这么干
}
} //找不到合适的bitmap,那就创建一个新的
if (result == null) {
result = Bitmap.createBitmap(width, height, config != null ? config : Bitmap.Config.ARGB_8888);
}
return result;
}
复制代码
LruBitmapPool根据系统版本,来挑选合适的bitmap,低于5.0版本有着严格的限制(宽、高、config要完全匹配才行),5.0及其以上,保证size、config 一致就行。config就是我们说的Bitmap.Config.ARGB_8888这种。
// 获取bitmap的size
bitmap.getAllocationByteCount() //优先选用这个
bitmap.getHeight() * bitmap.getRowBytes()
复制代码
如果LruBitmapPool没有找到合适,那就create新Bitmap,往往就是这里创建一个新Bitmap分配内存时导致OOM。有没得可以避免的方法呢???奔溃来自6.0机子
三、从首次加载网络图片来看Glide的处理流程
GlideApp.with(view).load("https://t7.baidu.com/it/u=3652245443,3894439772&fm=193&f=GIF").into(view);
复制代码
GlideApp是通过Glide的注解生成的,可以通过注解配置使用okhttp去下载图片,也可以配置一些全局设置,懒得设置就用默认的也行,具体可以参看Glide官方demo,很全,(不要说不会,不会,就是你没下载官方demo)
GlideApp.with(view) 就是基于view去获取对应的RequestManager,如果在子线程调用这个,那就是 ,如果是其他比如Act或者Fragment的话,就是RequestManager的一个实例。Application对应的RequestManager关注的是Application的生命周期那几个内存不足的方法。
.load(list.get(position))
.transform(new RoundedCorners(40))
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.placeholder(R.drawable.ic_launcher)
.error(R.drawable.ic_launcher)
复制代码
load的话,不写asBitmap或者asGif那默认就是asDrawable
public RequestBuilder<Drawable> load(@Nullable String string) {
return asDrawable().load(string);
}
复制代码
能够这么...链式配置参数,肯定是Builder设计模式了
RequestBuilder上的泛型TranscodeType就是Drawable、Bitmap、File、GifDrawable
这里说下缩略图,缩略图有两种,一种是直接用float参数的,第二种是传入一个新的ReqeustBuilder。错误error也可以传入RequestBuilder,一旦传入新的RequestBuilder,那构建请求时,会创建多个request。
.into(holder.imageView);
复制代码
into后,就真正开始buildRequest了,距离真正发请求获取图片还有很多准备事要干。
-
3.1、请求前的准备工作
-
3.1.1、获取ImageView的scaleType类型
可用于后面对Bitmap的转换。
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) {
......
switch (view.getScaleType()) {
case CENTER_CROP:
requestOptions = requestOptions.clone().optionalCenterCrop();
break;
case CENTER_INSIDE:
requestOptions = requestOptions.clone().optionalCenterInside();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
requestOptions = requestOptions.clone().optionalFitCenter();
break;
case FIT_XY:
requestOptions = requestOptions.clone().optionalCenterInside();
break;
case CENTER:
case MATRIX:
...... //transcodeClass就是上面提到TranscodeType的Class
return into(glideContext.buildImageViewTarget(view, transcodeClass), null,requestOptions, Executors.mainThreadExecutor());
}
}
复制代码
-
3.1.2、构建Request: 包含主Reqeust、缩略图、错误时要显示的图片。
主Reqeust跟缩略图组合,那他们一起开始请求即可。 错误的图片要等主Reqeust失败后才开始。 很巧妙(鸡贼)的是,主请求跟缩略图的组合是ThumbnailRequestCoordinator,其继承Request。同样ErrorRequestCoordinator也是继承Reqeust。
//主Reqeust跟缩略图组合
public void begin() {
synchronized (requestLock) {
isRunningDuringBegin = true;
try {
if (fullState != RequestState.SUCCESS && thumbState != RequestState.RUNNING) {
thumbState = RequestState.RUNNING;
thumb.begin();
}
if (isRunningDuringBegin && fullState != RequestState.RUNNING) {
fullState = RequestState.RUNNING;
full.begin();
}
} finally {
isRunningDuringBegin = false;
}
}
}
//错误的图片
@Override
public void onRequestFailed(Request request) {
synchronized (requestLock) {
if (!request.equals(error)) {
primaryState = RequestState.FAILED;
if (errorState != RequestState.RUNNING) {
errorState = RequestState.RUNNING;
error.begin();
}
return;
}
......
}
复制代码
-
3.1.3、判断构建的请求和ImageView之前的请求是否一致且是否用内存缓存,是的话就用之前的
private <Y extends Target<TranscodeType>> Y into(...) {
Preconditions.checkNotNull(target);
Request request = buildRequest(target, targetListener, options, callbackExecutor);
Request previous = target.getRequest();
if (request.isEquivalentTo(previous)
&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
if (!Preconditions.checkNotNull(previous).isRunning()) {
previous.begin();
}
return target;
}
requestManager.clear(target);
target.setRequest(request);
requestManager.track(target, request);
return target;
}
复制代码
requestManager.clear(target) 这里是清除掉target(就是ImageView)之前的请求,比如我们的ImageView滑出了屏幕,此时Item复用,再次into时,先clear之前的Request,所以无需我们手动去cancel请求。
target.setRequest(request) 就是给View设置Tag,早些年,给列表中的ImageView直接设置Tag显示被占用,就是这里被占用了,不过现在它设置是指定了tagId(R.id.glide_custom_view_target_tag)。
requestManager.track(target, request) 请求开始---> request.begin()。
-
3.1.4、获取View宽高(目标宽高,如果下载的图片大小跟目标宽高不一致,就要做矩阵缩放)且加载占位图
@Override
public void begin() {
synchronized (requestLock) {
......
//overrideWidth, overrideHeight是合法的宽高
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
onSizeReady(overrideWidth, overrideHeight);
} else {
//走回调获取ImageVide的宽高
target.getSize(this);
}
if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)&& canNotifyStatusChanged()) {
// 开始加载占位图
target.onLoadStarted(getPlaceholderDrawable());
}
}
}
复制代码
获取宽高有两种方式:一种是你在设置参数时,通过override(int width, int height)配置的宽高,另外一种是它从ImageView上测量获得宽高。
void getSize(@NonNull final SizeReadyCallback cb) {
.....//代码做过精简处理
view.getViewTreeObserver().addOnPreDrawListener(new OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int horizontalPadding = view.getPaddingLeft() + view.getPaddingRight();
int verticalPadding = view.getPaddingTop() + view.getPaddingBottom();
int currentWidth = view.getWidth() - horizontalPadding;
int currentHeight = view.getHeight() - verticalPadding;
cb.onSizeReady(currentWidth, currentHeight);
return true;
}
});
}
复制代码
-
3.2、Engine开始load,处理请求
-
3.2.1、先尝试从内存中找EngineResource
public <R> LoadStatus load(...) {
//构建key,看好了,与width height有关哈。
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations, resourceClass, transcodeClass, options);
EngineResource<?> memoryResource;
synchronized (this) {
//先从内存缓寸中加载
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
//可能从磁盘 可能从网络中找 后面详细说
......
} //有则直接回调给用户,结束
cb.onResourceReady( memoryResource, DataSource.MEMORY_CACHE, false);
return null;
}
复制代码
这里细节很多,大家注意。
从内存缓存开始,先从弱引用开始查找EngineResource,没有再从ResourceLruCache查找。弱引用和ResourceLruCache二者查找时用的key是一样的,
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;
}
复制代码
-
3.2.2、从内存中找不到,再看要不要从磁盘或者网络加载
//有同样的request在处理,直接复用之前的
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
current.addCallback(cb, callbackExecutor);
return new LoadStatus(cb, current);
}
EngineJob<R> engineJob = engineJobFactory.build(key,isMemoryCacheable, useUnlimitedSourceExecutorPool,useAnimationPool,onlyRetrieveFromCache);
DecodeJob<R> decodeJob = decodeJobFactory.build(glideContext,model,key,signature,width,
height,resourceClass,transcodeClass,priority,diskCacheStrategy,transformations,
isTransformationRequired,isScaleOnlyOrNoTransform,onlyRetrieveFromCache,options,
engineJob);
jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
engineJob.start(decodeJob);
复制代码
public synchronized void start(DecodeJob<R> decodeJob) {
......
//感觉这里总是true,如果状态为Stage.INITIALIZE,下一个肯定是要么磁盘缓存中的一个了,不然呢?
Stage firstStage = getNextStage(Stage.INITIALIZE);
boolean res= firstStage == Stage.RESOURCE_CACHE || firstStage == Stage.DATA_CACHE
GlideExecutor executor = res ? diskCacheExecutor : getActiveSourceExecutor();
executor.execute(decodeJob);
}
复制代码
来看看他们的状态流转,递归方法,本地可能会存在两种类型的DiskLruCache,一种是源数据(他是DATA_CACHE),一种是基于目标宽高的将原图做一次转换后保存的数据(RESOURCE_CACHE)。是的,你没看错,源数据是Data,转换后的数据是Resource。那么在加载磁盘缓存时,优先尝试RESOURCE_CACHE,没得,就尝试DATA_CACHE,都没得再开始老老实实走网络。我们配置DiskCacheStrategy默认是AUTOMATIC,这里diskCacheStrategy. decodeCachedResource()就是true。
优先尝试RESOURCE_CACHE就是走ResourceCacheGenerator的startNext方法。
再次尝试DATA_CACHE就是走DataCacheGenerator的startNext方法。
想从网络加载就走SourceGenerator的startNext方法。
如果用户设置的是DiskCacheStrategy.AUTOMATIC或者DiskCacheStrategy.DATA,那么SourceGenerator的startNext会走两次,第一次是从网络上下载资源返回流数据,第二次再次走startNext方法,是先将流存到磁盘缓存里面,再转入到DataCacheGenerator的startNext方法,从本地缓存File获取ByteBuffer开启后面解析流程。
如果用户设置的是DiskCacheStrategy.RESOURCE,那么SourceGenerator的startNext会只会走一次,从网络中下载资源获取流数据后,就开始后面的解析流程。
//线程池执行decodeJob,那decodeJob就是个是Runnable,优先看其run方法。
public void run() {
switch (runReason) { //runReason默认就是 INITIALIZE
case INITIALIZE:
stage = getNextStage(Stage.INITIALIZE);
currentGenerator = getNextGenerator();
runGenerators();
break;
case SWITCH_TO_SOURCE_SERVICE:
runGenerators();
break;
case DECODE_DATA:
Log.e("test","下载后开始解码数据");
decodeFromRetrievedData();
break;
.....
}
}
private Stage getNextStage(Stage current) {
switch (current) {
case INITIALIZE:
return diskCacheStrategy.decodeCachedResource() ? Stage.RESOURCE_CACHE
: getNextStage(Stage.RESOURCE_CACHE);
case RESOURCE_CACHE:
return diskCacheStrategy.decodeCachedData() ? Stage.DATA_CACHE
: getNextStage(Stage.DATA_CACHE);
......
}
}
private DataFetcherGenerator getNextGenerator() {
switch (stage) {
case RESOURCE_CACHE:
return new ResourceCacheGenerator(decodeHelper, this);
case DATA_CACHE:
return new DataCacheGenerator(decodeHelper, this);
case SOURCE:
return new SourceGenerator(decodeHelper, this);
case FINISHED:
.....
}
}
复制代码
最关键的runGenerators方法中的currentGenerator.startNext(),就是上面的那几个startNext。
private void runGenerators() {
boolean isStarted = false;
while (!isCancelled && currentGenerator != null
&& !(isStarted = currentGenerator.startNext())) {
stage = getNextStage(stage);
currentGenerator = getNextGenerator();
if (stage == Stage.SOURCE) {
reschedule();
return;
}
}
......
}
复制代码
首先肯定是尝试ResourceCacheGenerator的startNext方法,特别关键,这里涉及到Model,没读太仔细,没法讲,只能提个大概,他里面的modelLoader在初始化Glide时在Registry中加了很多类,append了很多很多。
public boolean startNext() {
....
while (modelLoaders == null || !hasNextModelLoader()) {
......
//拼装出来的key
currentKey = new ResourceCacheKey( helper.getArrayPool(), sourceId, helper.getSignature(), helper.getWidth(), helper.getHeight(),
transformation, resourceClass,helper.getOptions());
//首次的话,从磁盘缓存中取肯定找不到这个key对应的文件
cacheFile = helper.getDiskCache().get(currentKey);
if (cacheFile != null) {
sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
......
//如果存在缓存文件就去加载缓存文件
}
复制代码
再次尝试DataCacheGenerator的startNext方法,它的key很简单,就是url,这里的sourceId就是url。 首次肯定没有缓存,等SourceGenerator下载好后,再流转到这里来处理。
@Override
public boolean startNext() {
while (modelLoaders == null || !hasNextModelLoader()) {
Key sourceId = cacheKeys.get(sourceIdIndex);
//这个key跟文件下载后缓存到磁盘是一样的key
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
cacheFile = helper.getDiskCache().get(originalKey);
if (cacheFile != null) {
this.sourceKey = sourceId; //url就是key
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
loadData = modelLoader.buildLoadData(cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
//真正触发流解析的位置是这里哈。
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
复制代码
最后看看SourceGenerator的startNext,它的startNext会走两次,第一次获取数据,第二次是为了缓存源数据,很明显它修改了runReason,然后在新的线程池去跑DecodeJob,再次看decodejob的run方法。
public boolean startNext() {
//dataToCache不为空就可以缓存文件文件到本地
if (dataToCache != null) {
Object data = dataToCache;
dataToCache = null;
cacheData(data);
}//缓存完数据后他就不为空了 其实sourceCacheGenerator=new DataCacheGenerator(xxx)
//存了之后再次会走DataCacheGenerator的startNext方法,缓存不为空了,就开始加载本地文件
if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
return true;
}
sourceCacheGenerator = null;
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
loadData = helper.getLoadData().get(loadDataListIndex++);
if (loadData != null
&& (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
|| helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
started = true;
loadData.fetcher.loadData(xxxx)
}
}
return started;
}
复制代码
MultiModelLoader-->HttpUrlFetcher.loadData从网络上加载数据,加载好后,就回调准备再次调用startNext
loadData.fetcher.loadData(
helper.getPriority(),
new DataCallback<Object>() {
@Override
public void onDataReady(@Nullable Object data) {
if (isCurrentRequest(toStart)) {
onDataReadyInternal(toStart, data);
}
}
@Override
public void onLoadFailed(@NonNull Exception e) {
if (isCurrentRequest(toStart)) {
onLoadFailedInternal(toStart, e);
}
}
});
void onDataReadyInternal(LoadData<?> loadData, Object data) {
DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
dataToCache = data; //此时赋值,SourceGenerator的startNext再次调用就不为空了。
//再次reschedule的原因就是切线程到glide的线程池中线程,因为之前加载网络数据可能是用户自己开的线程
//就是这里会触发SourceGenerator的startNext再次调用
cb.reschedule();
} else {
//如果是非DATA和AUTOMATIC类型,比如Resource类型,那么不用回调,直接
//拿着这个data(ContentLengthInputStream)去操作
//看起来比DATA和AUTOMATIC轻便不少啊。
cb.onDataFetcherReady(loadData.sourceKey,data,loadData.fetcher,
loadData.fetcher.getDataSource(),originalKey);
}
}
复制代码
@Override
public void reschedule() {
runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
getActiveSourceExecutor().execute(job);
}
复制代码
第二次调用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。
-
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;
}
复制代码
解析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--倒带)来干这事。
很明显,我们这个不是Gif图,那就换下一个试试ByteBufferBitmapDecoder。
先将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;
复制代码
-
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;
}
复制代码
圆角的处理,以后有这种需求,也这么干。
-
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();
}
}
复制代码
页面关闭/返回
recyclerView列表滑动
那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加载这块,也做个补充吧。