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竟然找不到, 既然找不到 V i e w 所在的 F r a g m e n t 容器,那就只能用跟 A c t i v i t y 保持一致了 \color{#ff0000}{既然找不到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存在,是不是一定可以复用?有没有啥限制?

  //缓冲池策略
  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机子

image.png

三、从首次加载网络图片来看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,如果在子线程调用这个,那就是 单例的 R e q u e s t M a n a g e r \color{#ff0000}{单例的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设计模式了 image.png image.png

RequestBuilder上的泛型TranscodeType就是Drawable、Bitmap、File、GifDrawable image.png

这里说下缩略图,缩略图有两种,一种是直接用float参数的,第二种是传入一个新的ReqeustBuilder。错误error也可以传入RequestBuilder,一旦传入新的RequestBuilder,那构建请求时,会创建多个request。 image.png

.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是一样的, 看样子 E n g i n e R e s o u r c e 不可能同时存在于在弱引用和 R e s o u r c e L r u C a c h e 中了。 \color{#ff0000}{看样子EngineResource不可能同时存在于在弱引用和ResourceLruCache中了。}

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);
}
复制代码

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加载这块,也做个补充吧。

猜你喜欢

转载自juejin.im/post/7014041490439536654