[Glide4源码解析系列]–2.Glide数据模型转换与数据抓取

原创 2018年04月17日 16:37:07

Glide


Glide4源码解析系列

[Glide4源码解析系列]–1.Glide初始化
[Glide4源码解析系列]–2.Glide数据模型转换与数据抓取


一、简介

上一篇文章,我们梳理了一遍Glide的初始化流程,看到了Gilde在Glide#with一句简单的代码背后做了巨大的准备工作,而这所有的准备工作,都是为了接下来顺利地完成数据解析和显示做了铺垫。

在上一遍文章中提到,Glide的数据解码流程可以分为以下几个步骤(如果没看上一篇文章,建议可以先看看):

model(数据源)–>data(转换数据)–>decode(解码)–>transformed(缩放)–>transcoded(转码)–>encoded(编码保存到本地)

那么本篇文章就重点来看看Glide的数据转换与数据抓取流程。

我们依然从一句代码开始,那就是RequestManager#load

二、Glide资源加载前的准备

1. RequestManager.load装载原始数据

通过上一篇文章,我们知道,Glide.with()最后给我们返回了一个请求管理工具,那就是RequestManger。通常如果是简单的加载一张图片的话,我们调用链如下:


Glide.with(this).load(url).into(iv_img);

在RequestManger的中,对load方法进行了多个类型的重载,基本上可以满足日常图片加载类型,如String, URL,Bitmap,InputStream,File等等。

为了方便分析和流程的梳理,我们需要指定一个数据类型来进行跟踪,其它的流程基本是一致的,只是过程中使用的转换模型和解码方式不一样而已。

那么,这里就使用日常最常使用的,加载一个网络图片作为分析源头。我们直接进入:

  public RequestBuilder<Drawable> load(@Nullable String string) {
    return asDrawable().load(string);
  }

  public RequestBuilder<Drawable> asDrawable() {
    return as(Drawable.class);
  }

  public <ResourceType> RequestBuilder<ResourceType> as(Class<ResourceType> resourceClass) {
    return new RequestBuilder<>(glide, this, resourceClass, context);
  }

首先看到,在最后这个as方法中,创建了一个RequestBuilder,即一个请求的构建者,用来构建个Request请求工具。

这里需要注意一个参数,即as(Drawable.class),这个Drawable.class类型,将是最后转码得到的,最终用于显示的数据类型。

其次,asDrawable()得到一个RequestBuilder,然后通过其load方法,将原始数据设置给RequstBuilder。

那么,我们就来看看RequestBuild做了什么。

  public RequestBuilder<TranscodeType> load(@Nullable String string) {
    return loadGeneric(string);
  }

  private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) {
    this.model = model;
    isModelSet = true;
    return this;
  }

可以看到,这里并没有马上进入数据请求加载过程,而是简单的将数据模式进行了保存,并将isModelSet设置为true,然后返回。

那么什么时候才开始进入数据加载流程。那就要来看看RequestBuilder#into()方法了。

2. RequestBuilder.into()启动资源加载
1)生成请求参数,及显示目标
public ViewTarget<ImageView, TranscodeType> into(ImageView view) {
    Util.assertMainThread();
    Preconditions.checkNotNull(view);

    RequestOptions requestOptions = this.requestOptions;
    if (!requestOptions.isTransformationSet()
        && requestOptions.isTransformationAllowed()
        && view.getScaleType() != null) {
      // Clone in this method so that if we use this RequestBuilder to load into a View and then
      // into a different target, we don't retain the transformation applied based on the previous
      // View's scale type.
      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:
        default:
          // Do nothing.
      }
    }

    return into(
        glideContext.buildImageViewTarget(view, transcodeClass),
        /*targetListener=*/ null,
        requestOptions);
  }

根据ImageView设置的缩放类型,配置一个请求参数,这里设置的缩放工具,就是加载流程中transformed(缩放)使用到的工具。

重点看最后一个调用,第一个参数

glideContext.buildImageViewTarget(view, transcodeClass),

这里会根据transcodeClass类型生成一个ViewTarget,这里transcodeClass为Drawable.class,所有将会生成一个DrawableImageViewTarget。

2)构建请求,并启动数据加载

进入into方法中,过程比较简单,直接看代码中的注释:

private <Y extends Target<TranscodeType>> Y into(
  @NonNull Y target,
  @Nullable RequestListener<TranscodeType> targetListener,
  RequestOptions options) {
    Util.assertMainThread();
    Preconditions.checkNotNull(target);
    //第一:通过isModelSet检查是否通过load设置了数据源,否则抛出异常;
    if (!isModelSet) {
      throw new IllegalArgumentException("You must call #load() before calling #into()");
    }

    options = options.autoClone();

    //第二:创建请求;
    Request request = buildRequest(target, targetListener, options);

    //第三:判断当前请求是否已经存在,
    //是的话,直接启动请求;
    Request previous = target.getRequest();
    if (request.isEquivalentTo(previous)) {
      request.recycle();
      // If the request is completed, beginning again will ensure the result is re-delivered,
      // triggering RequestListeners and Targets. If the request is failed, beginning again will
      // restart the request, giving it another chance to complete. If the request is already
      // running, we can let it continue running without interruption.
      if (!Preconditions.checkNotNull(previous).isRunning()) {
        // Use the previous request rather than the new one to allow for optimizations like skipping
        // setting placeholders, tracking and untracking Targets, and obtaining View dimensions that
        // are done in the individual Request.
        previous.begin();
      }
      return target;
    }

    //第四:保存当前请求到ViewTarget的Tag中,
    //并将Request添加RequestManager中进行跟踪维护。
    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request);

    return target;
}

一看便知,重点在第二和第四两步上。

首先来看下第二步,构建一个Request:

private Request buildRequest(
  Target<TranscodeType> target,
  @Nullable RequestListener<TranscodeType> targetListener,
  RequestOptions requestOptions) {
    return buildRequestRecursive(
        target,
        targetListener,
        /*parentCoordinator=*/ null,
        transitionOptions,
        requestOptions.getPriority(),
        requestOptions.getOverrideWidth(),
        requestOptions.getOverrideHeight(),
        requestOptions);
}

private Request buildRequestRecursive(
  Target<TranscodeType> target,
  @Nullable RequestListener<TranscodeType> targetListener,
  @Nullable RequestCoordinator parentCoordinator,
  TransitionOptions<?, ? super TranscodeType> transitionOptions,
  Priority priority,
  int overrideWidth,
  int overrideHeight,
  RequestOptions requestOptions) {

    // Build the ErrorRequestCoordinator first if necessary so we can update parentCoordinator.
    ErrorRequestCoordinator errorRequestCoordinator = null;
    if (errorBuilder != null) {
      errorRequestCoordinator = new ErrorRequestCoordinator(parentCoordinator);
      parentCoordinator = errorRequestCoordinator;
    }

    //构建目标请求
    Request mainRequest =
        buildThumbnailRequestRecursive(
            target,
            targetListener,
            parentCoordinator,
            transitionOptions,
            priority,
            overrideWidth,
            overrideHeight,
            requestOptions);

    if (errorRequestCoordinator == null) {
      return mainRequest;
    }

    int errorOverrideWidth = errorBuilder.requestOptions.getOverrideWidth();
    int errorOverrideHeight = errorBuilder.requestOptions.getOverrideHeight();
    if (Util.isValidDimensions(overrideWidth, overrideHeight)
        && !errorBuilder.requestOptions.isValidOverride()) {
      errorOverrideWidth = requestOptions.getOverrideWidth();
      errorOverrideHeight = requestOptions.getOverrideHeight();
    }

    Request errorRequest = errorBuilder.buildRequestRecursive(
        target,
        targetListener,
        errorRequestCoordinator,
        errorBuilder.transitionOptions,
        errorBuilder.requestOptions.getPriority(),
        errorOverrideWidth,
        errorOverrideHeight,
        errorBuilder.requestOptions);
    errorRequestCoordinator.setRequests(mainRequest, errorRequest);
    return errorRequestCoordinator;
}

直接看第二个方法buildRequestRecursive,递归构建请求。从方法命名来看,请求不一定只有一个,而是会视情况递归地去构建多个请求,这些请求类型包括:

  • 错误图片请求(正常的请求出错时,如果有配置该请求,则启动该请求)
  • 缩略图请求(小图请求,可以较快显示。如有配置,在请求开始时,就会启动)
  • 目标图片请求(目标图片请求,在请求开始时,就是启动)

如果除了目标图片外,用户还配置了错误图片显示,或缩略图显示,那么,这时候会创建一个请求协调器,来协调各类型图片间的请求顺序。

在看协调器之前,我们先来看下Request这类,它是一个接口类,规定了请求相关的接口,如开始/停止/清除/回收请求……

public interface Request {
  void begin();
  void pause();
  void clear();
  boolean isPaused();
  boolean isRunning();
  boolean isComplete();
  boolean isResourceSet();
  boolean isCancelled();
  boolean isFailed();
  void recycle();
  boolean isEquivalentTo(Request other);
}

而协调器,其实也是一个Request的实现类,比如上面第二个方法中的ErrorRequestCoordinator

public final class ErrorRequestCoordinator implements RequestCoordinator,
    Request {

  @Nullable
  private final RequestCoordinator parent;
  private Request primary;
  private Request error;

  public ErrorRequestCoordinator(@Nullable RequestCoordinator parent) {
    this.parent = parent;
  }

  public void setRequests(Request primary, Request error) {
    this.primary = primary;
    this.error = error;
  }

  @Override
  public void begin() {
    if (!primary.isRunning()) {
      primary.begin();
    }
  }

  @Override
  public void pause() {
    if (!primary.isFailed()) {
      primary.pause();
    }
    if (error.isRunning()) {
      error.pause();
    }
  }

  @Override
  public void clear() {
    primary.clear();
    if (primary.isFailed()) {
      error.clear();
    }
  }

  @Override
  public void onRequestFailed(Request request) {
    if (!request.equals(error)) {
      if (!error.isRunning()) {
        error.begin();
      }
      return;
    }

    if (parent != null) {
      parent.onRequestFailed(this);
    }
  }

  //省略其余方法......
}
  1. 在构建协调器后,会将目标图片请求和错误图片请求设置给协调器。
  2. 一旦请求begin,就会启动目标图片请求。
  3. 当目标图片请求失败时,就会启动错误图片请求。

其它的协调器也是类似的,只不过各类型请求启动的时机不一样罢了!

接着,我们回到刚刚buildRequestRecursive方法中,为了方便分析,我们简化一下流程,只看请求只有目标图片的情况。

构建目标图片用是在buildThumbnailRequestRecursive方法中:

private Request buildThumbnailRequestRecursive(
  Target<TranscodeType> target,
  RequestListener<TranscodeType> targetListener,
  @Nullable RequestCoordinator parentCoordinator,
  TransitionOptions<?, ? super TranscodeType> transitionOptions,
  Priority priority,
  int overrideWidth,
  int overrideHeight,
  RequestOptions requestOptions) {
    if (thumbnailBuilder != null) {
        //构建请求......
    } else if (thumbSizeMultiplier != null) {
        //构建请求......
    } else {
      // Base case: no thumbnail.
      return obtainRequest(
          target,
          targetListener,
          requestOptions,
          parentCoordinator,
          transitionOptions,
          priority,
          overrideWidth,
          overrideHeight);
    }
}

看最后一个else,只有目标图片的情况,这里会构建并返回一个Request:

private Request obtainRequest(
  Target<TranscodeType> target,
  RequestListener<TranscodeType> targetListener,
  RequestOptions requestOptions,
  RequestCoordinator requestCoordinator,
  TransitionOptions<?, ? super TranscodeType> transitionOptions,
  Priority priority,
  int overrideWidth,
  int overrideHeight) {
    return SingleRequest.obtain(
        context,
        glideContext,
        model,
        transcodeClass,
        requestOptions,
        overrideWidth,
        overrideHeight,
        priority,
        target,
        targetListener,
        requestListener,
        requestCoordinator,
        glideContext.getEngine(),
        transitionOptions.getTransitionFactory());
}

通过SingleRequest.obtain获取到了一个SingleRequst的单例(这一堆的参数让人忍不住想吐槽一下)。

到这构建好了Request,那么接下来就是启动请求了。

3)启动请求

回到into方法中:


private <Y extends Target<TranscodeType>> Y into(
  @NonNull Y target,
  @Nullable RequestListener<TranscodeType> targetListener,
  RequestOptions options) {

    //省略部分代码......

    requestManager.clear(target);
    target.setRequest(request);
    requestManager.track(target, request);

    return target;
}

在平时使用Glide过程中,我们可能会调用ImageView的setTag来缓存一些数据,但是在使用Glide加载图片的时候,就会抛出异常,告诉我们使用Glide来加载图片的ImageView不能调用setTag方法,这是为什么呢?原因就在这句代码:

target.setRequest(request);

这句代码会将Request缓存到ImageView的tag中,如果你确实需要缓存数据,那么你只能调用setTag(int key, Object tag)给tag设置一个key。

最后,将这个请求放到RequestManager的请求队列中,同时发起加载请求。

//RequestManager.java
void track(Target<?> target, Request request) {
    targetTracker.track(target);
    requestTracker.runRequest(request);
}

//RequestTracker.java
public void runRequest(Request request) {
    requests.add(request);
    if (!isPaused) {
      request.begin();
    } else {
      pendingRequests.add(request);
    }
}

那么,Glide就通过RequestManager、RequestOption、Request,构建了一个请求序列,并通过监听生命周期来动态管理Request的开启、暂停、恢复、销毁等。

三、开启资源加载任务

来到SingleRequest#begin方法中(非完整代码,省略一些不太紧要的代码)

public void begin() {

    //省略部分代码...

    if (status == Status.RUNNING) {
      throw new IllegalArgumentException("Cannot restart a running request");
    }
    if (status == Status.COMPLETE) {
      onResourceReady(resource, DataSource.MEMORY_CACHE);
      return;
    }

    //重点在以下if中
    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
      onSizeReady(overrideWidth, overrideHeight);
    } else {
      target.getSize(this);
    }

    if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
        && canNotifyStatusChanged()) {
      target.onLoadStarted(getPlaceholderDrawable());
    }
}

public void onSizeReady(int width, int height) {

    //省略部分代码...

    if (status != Status.WAITING_FOR_SIZE) {
      return;
    }
    status = Status.RUNNING;

    float sizeMultiplier = requestOptions.getSizeMultiplier();
    this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
    this.height = maybeApplySizeMultiplier(height, sizeMultiplier);

    loadStatus = engine.load(
        glideContext,
        model,
        requestOptions.getSignature(),
        this.width,
        this.height,
        requestOptions.getResourceClass(),
        transcodeClass,
        priority,
        requestOptions.getDiskCacheStrategy(),
        requestOptions.getTransformations(),
        requestOptions.isTransformationRequired(),
        requestOptions.isScaleOnlyOrNoTransform(),
        requestOptions.getOptions(),
        requestOptions.isMemoryCacheable(),
        requestOptions.getUseUnlimitedSourceGeneratorsPool(),
        requestOptions.getUseAnimationPool(),
        requestOptions.getOnlyRetrieveFromCache(),
        this);

    if (status != Status.RUNNING) {
      loadStatus = null;
    }
}

在begin方法中,如果图片显示尺寸有效,会直接调用onSizeReady。否则, 会调用target.getSize,去计算图片尺寸,计算完毕后,同样会回调onSizeReady方法。

因此,最终都会进入到onSizeReady方法中,进而调用Engine(请求加载引擎)的load方法。

代码中简单标示了加载流程。

//Engine.java
public <R> 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) {
    Util.assertMainThread();
    long startTime = LogTime.getLogTime();

    //1:创建资源索引key
    EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
        resourceClass, transcodeClass, options);

    //2:从内存中当前正在显示的资源缓存加载图片
    EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
    if (active != null) {
      cb.onResourceReady(active, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from active resources", startTime, key);
      }
      return null;
    }

    //3:从内存缓存资源中加载图片
    EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
    if (cached != null) {
      cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Loaded resource from cache", startTime, key);
      }
      return null;
    }

    //4:获取已经存在的加载任务
    EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
    if (current != null) {
      current.addCallback(cb);
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Added to existing load", startTime, key);
      }
      return new LoadStatus(cb, current);
    }

    //5:新建加载任务,用于启动解码任务
    EngineJob<R> engineJob =
        engineJobFactory.build(
            key,
            isMemoryCacheable,
            useUnlimitedSourceExecutorPool,
            useAnimationPool,
            onlyRetrieveFromCache);

    //6:新建解码任务,真正执行数据加载和解码的类
    DecodeJob<R> decodeJob =
        decodeJobFactory.build(
            glideContext,
            model,
            key,
            signature,
            width,
            height,
            resourceClass,
            transcodeClass,
            priority,
            diskCacheStrategy,
            transformations,
            isTransformationRequired,
            isScaleOnlyOrNoTransform,
            onlyRetrieveFromCache,
            options,
            engineJob);

    //7:缓存加载任务
    jobs.put(key, engineJob);

    engineJob.addCallback(cb);

    //8:开启解码任务
    engineJob.start(decodeJob);

    return new LoadStatus(cb, engineJob);
}

以上共有8个步骤来获取或开始一个资源的加载和解码。分析一下:

1:创建资源索引key。
我们可以看到生成一个索引key需要资源本身、图片宽高、转换类型、加载参数等等,只要这些都一致的情况下,才判定为一个相同的图片资源加载。所以,即便是要显示的ImageView宽高不一样,Glide都会重新执行一次加载过程,而不是内存中加载已有的图片资源。

2和3:
如果要加载的图片已经正在显示,直接使用已有的资源。如果图片没有在显示,但是已经正好还在内存缓存中,没有被销毁,那么直接使用缓存中的资源

4到8:
如果内存中并没有可以直接使用的图片资源,那么就要开始从网络或者本地硬盘中去加载一张图片。

还记得上一篇文中说到的,初始化过程中会创建几个不同类的线程池,用于加载图片资源吗?Glide将每一个请求都封装为一个解码任务DecodeJob,并扔到线程池中,以此来开启任务的异步加载。

public void start(DecodeJob<R> decodeJob) {
    this.decodeJob = decodeJob;
    GlideExecutor executor = decodeJob.willDecodeFromCache()
        ? diskCacheExecutor
        : getActiveSourceExecutor();
    executor.execute(decodeJob);
}

如此就很明了了,DecodeJob肯定是一个继承Runnable的类,任务启动的入口就在run方法中。

//DecodeJob.java

public void run() {

    //省略部分代码...

    DataFetcher<?> localFetcher = currentFetcher;
    try {
      if (isCancelled) {
        notifyFailed();
        return;
      }
      //重点调用
      runWrapped();
    } catch (Throwable t) {

      //省略日志打印和注释...

      if (stage != Stage.ENCODE) {
        throwables.add(t);
        notifyFailed();
      }
      if (!isCancelled) {
        throw t;
      }
    } finally {
      // Keeping track of the fetcher here and calling cleanup is excessively paranoid, we call
      // close in all cases anyway.
      if (localFetcher != null) {
        localFetcher.cleanup();
      }
      TraceCompat.endSection();
    }
}
 ```

如果一切正常,那么会进入runWrapped方法中

```java
//DecodeJob.java

private void runWrapped() {
     switch (runReason) {
      case INITIALIZE:
        stage = getNextStage(Stage.INITIALIZE);
        currentGenerator = getNextGenerator();
        runGenerators();
        break;
      case SWITCH_TO_SOURCE_SERVICE:
        runGenerators();
        break;
      case DECODE_DATA:
        decodeFromRetrievedData();
        break;
      default:
        throw new IllegalStateException("Unrecognized run reason: " + runReason);
    }
}

private Stage getNextStage(Stage current) {
    switch (current) {
      //初始状态:下一状态为从处理过的资源缓存加载图片
      case INITIALIZE:
        return diskCacheStrategy.decodeCachedResource()
            ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);

      //状态2:下一状态从未处理过的原始资源加载图片
      case RESOURCE_CACHE:
        return diskCacheStrategy.decodeCachedData()
            ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);

      //状态3:下一状态从远程加载图片
      case DATA_CACHE:
        // Skip loading from source if the user opted to only retrieve the resource from cache.
        return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;

      //状态4:结束解码
      case SOURCE:
      case FINISHED:
        return Stage.FINISHED;
      default:
        throw new IllegalArgumentException("Unrecognized stage: " + current);
    }
}

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:
        return null;
      default:
        throw new IllegalStateException("Unrecognized stage: " + stage);
    }
}

private void runGenerators() {
    currentThread = Thread.currentThread();
    startFetchTime = LogTime.getLogTime();
    boolean isStarted = false;
    while (!isCancelled && currentGenerator != null
        && !(isStarted = currentGenerator.startNext())) {
      stage = getNextStage(stage);
      currentGenerator = getNextGenerator();

      if (stage == Stage.SOURCE) {
        reschedule();
        return;
      }
    }
    // We've run out of stages and generators, give up.
    if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
      notifyFailed();
    }
}




<div class="se-preview-section-delimiter"></div>

==这几个方法构成了Glide的解码流程:==

  1. 尝试从处理过的本地资源加载图片
  2. 尝试从未处理过的原始本地资源加载图片
  3. 尝试从远程加载图片

因此,这是一个嵌套的循环,通过状态的切换来寻找下一个加载器,直到加载一张图片,返回成功;或者找不到要加载的图片,返回失败。

==以上三个步骤分别对应以下三个加载器:==

  1. ResourceCacheGenerator
  2. DataCacheGenerator
  3. SourceGenerator

接下来终于要进入本文的重点部分了(铺垫了一堆?,想讲明白Glide真不容易啊~)

四、Glide数据模型转换

1. 加载核心简介

以上三个加载器是顺序遍历的,本文以加载一张网络图片来讲解这个解码过程,为了便于理解与理清加载逻辑,我们不按照这个流程来,而是从最后一个SourceGenerator加载器入手,因为,当你第一次加载一张新的网络图片时,本地并没有这张网络图片的缓存。

从runGenerators方法中看到,Generator的入口是startNext()方法

//SourceGenerator.java

public boolean startNext() {

    //1:判断是否有缓存,如有,直接加载缓存
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      cacheData(data);
    }

    if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
      return true;
    }
    sourceCacheGenerator = null;

    //2:没有缓存,从远程加载
    loadData = null;
    boolean started = false;
    while (!started && hasNextModelLoader()) {
      //3:获取数据加载器
      loadData = helper.getLoadData().get(loadDataListIndex++);
      if (loadData != null
          && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
          || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
        started = true;
        loadData.fetcher.loadData(helper.getPriority(), this);
      }
    }
    return started;
}




<div class="se-preview-section-delimiter"></div>

分为两种情况,第一中情况我们先不管,后面再详细讲到。直接来看第二种情况,也是首次加载的情况。

又是一个循环遍历,遍历所有的ModelLoader模块加载器。为了更好的理解,我们先来了解下Glide用来数据加载和解码的几个模块。

类名 作用
SourceGenerator继承DataFetcherGenerator DataFecher生成器
ModelLoader 数据转换和创建LoadData
LoadData 数据加载(包含DataFecher)
DataFecher 数据抓取
LoadPath 加载器包含多个DecodePath
DecodePath 解码器

从命名上也可以看出他们之间的些许联系:

  • Generator是一个DataFecher生成器(还有ResourceCacheGenerator和DataCacheGenerator)
  • DataFether是一个数据抓取器,存放在LoadData中。
    而这些LoadData是从哪里生产的呢?就是ModelLoader。

    以上1-4构成了Glide数据转换与获取(如:String –> url –> InputStream)的核心;
    5-6则构成Glide数据解码的核心(5-6我们在下一篇文章再详细分析)。

  • 当抓取到数据以后,需要对数据进行解码,这时候就会用到DecodePath来进行解码,而DecodePath正是存放在LoadPath当中的。

这几个工具,基本构成了Glide数据加载过程的核心。

2. 模型转换匹配
1)数据转换,获取ModelLoader

Glide是如何后获取到匹配的模型加载器的?看startNext的第3步,进入helper.getLoadData(),其中helper为DecoderHelper,解码任务的帮助类。

//DecoderHelper.java

List<LoadData<?>> getLoadData() {
    if (!isLoadDataSet) {
      isLoadDataSet = true;
      loadData.clear();
      List<ModelLoader<Object, ?>> modelLoaders =
      glideContext.getRegistry().getModelLoaders(model);

      int size = modelLoaders.size();
      for (int i = 0; i < size; i++) {
        ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
        LoadData<?> current =
            modelLoader.buildLoadData(model, width, height, options);
        if (current != null) {
          loadData.add(current);
        }
      }
    }
    return loadData;
}




<div class="se-preview-section-delimiter"></div>

看到熟悉的代码了吗?

List<ModelLoader<Object, ?>> modelLoaders =
glideContext.getRegistry().getModelLoaders(model);




<div class="se-preview-section-delimiter"></div>

这里获取的不就是Glide在初始化的时候那个注册器吗?而getModelLoaders获取的就是model类型对应的数据转换器ModelLoader(如果不了解,建议先看上篇文章)。

下面只看关键的方法调用,

首先是ModelLoaderRegistry.java

  //ModelLoaderRegistry.java

  public synchronized <A> List<ModelLoader<A, ?>> getModelLoaders(A model) {
    //获取model类型对应的数据模型加载器
    List<ModelLoader<A, ?>> modelLoaders = getModelLoadersForClass(getClass(model));
    int size = modelLoaders.size();
    List<ModelLoader<A, ?>> filteredLoaders = new ArrayList<>(size);
    for (int i = 0; i < size; i++) {
      ModelLoader<A, ?> loader = modelLoaders.get(i);
      if (loader.handles(model)) {
        filteredLoaders.add(loader);
      }
    }
    return filteredLoaders;
  }

  private <A> List<ModelLoader<A, ?>> getModelLoadersForClass(Class<A> modelClass) {
    List<ModelLoader<A, ?>> loaders = cache.get(modelClass);
    if (loaders == null) {
      //调用工厂方法,构建ModelLoader
      loaders = Collections.unmodifiableList(multiModelLoaderFactory.build(modelClass));
      cache.put(modelClass, loaders);
    }
    return loaders;
  }




<div class="se-preview-section-delimiter"></div>

相信大家都看得懂,关键代码就是multiModelLoaderFactory.build(modelClass)

  //MultiModelLoaderFactory.java

  synchronized <Model> List<ModelLoader<Model, ?>> build(Class<Model> modelClass) {
    try {
      List<ModelLoader<Model, ?>> loaders = new ArrayList<>();
      //1:遍历所有注册的模型转换器
      for (Entry<?, ?> entry : entries) {
        //2:过滤重复
        if (alreadyUsedEntries.contains(entry)) {
          continue;
        }
        //3:如果是要转换的数据类型
        if (entry.handles(modelClass)) {
          alreadyUsedEntries.add(entry);
          //4:添加构建好的ModelLoader
          loaders.add(this.<Model, Object>build(entry));
          alreadyUsedEntries.remove(entry);
        }
      }
      return loaders;
    } catch (Throwable t) {
      alreadyUsedEntries.clear();
      throw t;
    }
  }

  private <Model, Data> ModelLoader<Model, Data> build(Entry<?, ?> entry) {
    //调用工厂方法构建ModelLoader
    return (ModelLoader<Model, Data>) Preconditions.checkNotNull(entry.factory.build(this));
  }

  //判断数据类型是否为要查找的数据类型,或者父类
  //即this.modelClass == modelClass 或者 modelClass为this.modelClass的子类
  public boolean handles(Class<?> modelClass) {
      return this.modelClass.isAssignableFrom(modelClass);
  }




<div class="se-preview-section-delimiter"></div>

那么,先把String对应的类型转换以及相应的工厂列出来

Model 转换类型 ModelLoader工厂
String.class InputStream.class DataUrlLoader.StreamFactory
String.class InputStream.class StringLoader.StreamFactory
String.class InputStream.class StringLoader.FileDescriptorFactory()

是不是觉得很简单,就是三种转换而已,事实并非如此,我们继续往下看代码。

 //DataUrlLoader.java

 private static final String DATA_SCHEME_IMAGE = "data:image";

 public boolean handles(String url) {
    return url.startsWith(DATA_SCHEME_IMAGE);
 }

 //DataUrlLoader#StreamFactory.java

 public final ModelLoader<String, InputStream> build(MultiModelLoaderFactory multiFactory) {
    return new DataUrlLoader<>(opener);
 }




<div class="se-preview-section-delimiter"></div>

从handles方法可以看出,DataUrlLoader是用来加载base64 Url图片的。所以这会被过滤掉,不会用来处理普通的String类型图片路径。

剩下的两个:

  //StringLoader.java

  public boolean handles(String model) {
    return true;
  }

  //StringLoader#StreamFactory.java

  public ModelLoader<String, InputStream> build(MultiModelLoaderFactory multiFactory) {
    return new StringLoader<>(multiFactory.build(Uri.class, InputStream.class));
  }

  //StringLoader#FileDescriptorFactory.java

  public ModelLoader<String, ParcelFileDescriptor> build(MultiModelLoaderFactory multiFactory) {
      return new StringLoader<>(multiFactory.build(Uri.class, ParcelFileDescriptor.class));
    }




<div class="se-preview-section-delimiter"></div>

你会发现,后面这两个工厂什么鬼?是不是走错片场了?通过multiFactory.build的方法又回到MultiModelLoaderFactory中了?

但是,如果你够仔细的话,你又会发现,此build非比build,这儿的build多了一个参数!!!

这就是Gilde数据模型转换非常高明的地方了。这里不仅将String.class类型的数据转换成了Uri.class的数据,并且还精确缩小了搜索范围,即要输入为Uri,又要输出只为InputStream.class和ParcelFileDescriptor.class的ModelLoader。

同时,也利用了Uri数据类型的ModelLoader的解析数据能力,来解析String类型的网络图片,不得不赞叹Glide强大的架构设计思维。

通过这种数据类型转换的能力,Glide几乎可以无缝的加载任意类型的图片数据。

好了,我们继续往下:

 public synchronized <Model, Data> ModelLoader<Model, Data> build(Class<Model> modelClass,
      Class<Data> dataClass) {
    try {
      List<ModelLoader<Model, Data>> loaders = new ArrayList<>();
      boolean ignoredAnyEntries = false;
      //1:再次重新遍历,只不过,这次model变成了Uri.class
      for (Entry<?, ?> entry : entries) {
        if (alreadyUsedEntries.contains(entry)) {
          ignoredAnyEntries = true;
          continue;
        }
        //2:同时满足modelClass和dataClass才是要查找的ModelLoader
        if (entry.handles(modelClass, dataClass)) {
          alreadyUsedEntries.add(entry);
          loaders.add(this.<Model, Data>build(entry));
          alreadyUsedEntries.remove(entry);
        }
      }
      //3:如果多于1个loader,创建个兼容的多Model加载器MultiModelLoader
      if (loaders.size() > 1) {
        return factory.build(loaders, throwableListPool);
      } else if (loaders.size() == 1) {
        //4:只有1个,直接返回
        return loaders.get(0);
      } else {
        if (ignoredAnyEntries) {
          return emptyModelLoader();
        } else {
          throw new NoModelLoaderAvailableException(modelClass, dataClass);
        }
      }
    } catch (Throwable t) {
      alreadyUsedEntries.clear();
      throw t;
    }
  }





<div class="se-preview-section-delimiter"></div>

遍历逻辑基本于上一个build相同,只不过,最后返回的时候,如果遍历到多个ModelLoader会创建一个MultiModelLoader,用来保存多个ModelLoader,其实也是一个协调器,类似新建Request是的多个类型Request时,用一个协调器来包裹和协调。

我们依旧把model类型为Uri,data转换类型为InputStream和ParcelFileDescriptor的ModelLoader用表格列出来:

model data Factory
Uri.class InputStream.class HttpUriLoader.Factory
Uri.class InputStream.class UriLoader.StreamFactory
Uri.class InputStream.class AssetUriLoader.StreamFactory
Uri.class InputStream.class MediaStoreImageThumbLoader.Factory
Uri.class InputStream.class MediaStoreVideoThumbLoader.Factory
Uri.class ParcelFileDescriptor.class AssetUriLoader.FileDescriptorFactory

这里列出了大体的ModelLoader,根据参数不一样其工厂也会有些差别。

经过这么一转换,分化出来的ModelLoader就有七八个。但是每个loader都有其对应可以加载数据类型,又或者在构建LoadData的时候会有所判断,来真正确认是否可以加载目标类型数据。

当然,你可能会想,这么多的loader是否是又会分化出更多的ModelLoader出来?那么我要告诉你的是,bingo~,答对了!

但是不用担心,万变不离其宗,所有的分化,到最后都会有个实际干活的Loader。

这里我们加载的是一张网络图片,可想而知,最筛选出来的只有可以加载网络数据的ModelLoader,判别方法也很简单,通过handlers方法就可以判别出来,具体就不再展开了。这里能加载网络图片的有HttpUriLoader和UriLoader

以HttpUriLoader为例:

  //HttpUriLoader.java
  private static final Set<String> SCHEMES =
      Collections.unmodifiableSet(new HashSet<>(Arrays.asList("http", "https")));

  @Override
  public boolean handles(Uri model) {
    return SCHEMES.contains(model.getScheme());
  }

  //HttpUriLoader#Factory.java
  public ModelLoader<Uri, InputStream> build(MultiModelLoaderFactory multiFactory) {
    return new HttpUriLoader(multiFactory.build(GlideUrl.class, InputStream.class));
  }




<div class="se-preview-section-delimiter"></div>

看到没,又继续转换了,这回变成了GlideUrl和InputStream,但是这次不一样了,因为这两个构成的Modeloader只有一个,并且还是实际干活的!

model data Factory
GlideUrl.class InputStream.class HttpGlideUrlLoader.Factory
 //HttpGlideUrlLoader#Factory.java

 public ModelLoader<GlideUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
    return new HttpGlideUrlLoader(modelCache);
 }




<div class="se-preview-section-delimiter"></div>

经过这一些列遍历和转换构建,最终,Glide将得到一个ModelLoader列表,这个列表可能包含ModelLoader或者MultiModelLoader,取决于要加载的Model数据在注册表中注册的ModelLoad,以及ModelLoader相互间可发生的转换。

2)构建LoadData

让我们回到DecodeHelper:

List<LoadData<?>> getLoadData() {
    if (!isLoadDataSet) {
      isLoadDataSet = true;
      loadData.clear();
      List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
      int size = modelLoaders.size();
      for (int i = 0; i < size; i++) {
        ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
        //构建LoadData
        LoadData<?> current =
            modelLoader.buildLoadData(model, width, height, options);
        if (current != null) {
          loadData.add(current);
        }
      }
    }
    return loadData;
  }




<div class="se-preview-section-delimiter"></div>

得到ModelLoader后,将利用这些ModelLoader逐个构建LoadData

首先,看MultiModelLoader是如何构建LoadData的

//MultiModelLoader.java

public LoadData<Data> buildLoadData(Model model, int width, int height,
      Options options) {
    Key sourceKey = null;
    int size = modelLoaders.size();
    List<DataFetcher<Data>> fetchers = new ArrayList<>(size);
    for (int i = 0; i < size; i++) {
      ModelLoader<Model, Data> modelLoader = modelLoaders.get(i);
      if (modelLoader.handles(model)) {
        LoadData<Data> loadData = modelLoader.buildLoadData(model, width, height, options);
        if (loadData != null) {
          sourceKey = loadData.sourceKey;
          fetchers.add(loadData.fetcher);
        }
      }
    }
    return !fetchers.isEmpty()
        ? new LoadData<>(sourceKey, new MultiFetcher<>(fetchers, exceptionListPool)) : null;
}




<div class="se-preview-section-delimiter"></div>

MultiModelLoader会遍历保存的ModelLoader列表,逐个构建LoadData,并将各个LoadData中的DataFetcher取出,存放在MultiFetcher中,从而MultiFetcher又成为一个协调器。

再来看单个ModelLoader构建LoadData,同样的,以Model为String为例

  //StringLoader.java

  public LoadData<Data> buildLoadData(String model, int width, int height,
      Options options) {
    //将String转换为Uri
    Uri uri = parseUri(model);
    return uri == null ? null : uriLoader.buildLoadData(uri, width, height, options);
  }




<div class="se-preview-section-delimiter"></div>

还记得上面StringLoader构建的时候,进行Uri转换吗?此处的uriLoader正是转换后的ModelLoader/MultiLoader。其实最后构建的就是实际干活的对象:HttpGlideUrlLoader

  //HttpGlideUrlLoader.java

  public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height,
      @NonNull Options options) {
    // GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time
    // spent parsing urls.
    GlideUrl url = model;
    if (modelCache != null) {
      url = modelCache.get(model, 0, 0);
      if (url == null) {
        modelCache.put(model, 0, 0, model);
        url = model;
      }
    }
    int timeout = options.get(TIMEOUT);
    return new LoadData<>(url, new HttpUrlFetcher(url, timeout));
  }




<div class="se-preview-section-delimiter"></div>

最后的代码,将新建的HttpUrlFetcher注入给了LoadData,至此,得到一个有效的LoadData和DataFetcher。

3)数据抓取

得到了LoadData后,回到SourceGenerotor中,

  public boolean startNext() {

    //省略部分代码...

    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(helper.getPriority(), this);
      }
    }
    return started;
  }




<div class="se-preview-section-delimiter"></div>

通过循环遍历所有的LoadData,并通过其DataFetcher,就可以进行数据的抓取了。

  //HttpUrlFetcher.java

  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);
    } catch (IOException e) {
      if (Log.isLoggable(TAG, Log.DEBUG)) {
        Log.d(TAG, "Failed to load data for url", e);
      }
      callback.onLoadFailed(e);
    } finally {
      if (Log.isLoggable(TAG, Log.VERBOSE)) {
        Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime));
      }
    }
  }




<div class="se-preview-section-delimiter"></div>

通过loadDataWithRedirects方法,利用HttpURLConnection就可以抓取到网络图片的InputStream,具体不再展开,有兴趣可以查看源码。

最后通过callback.onDataReady(result)将结果回调给SourceGenerator进行下一步处理。

  //SourceGenerator.java

  public void onDataReady(Object data) {
    DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
    if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
      dataToCache = data;
      // We might be being called back on someone else's thread. Before doing anything, we should
      // reschedule to get back onto Glide's thread.
      cb.reschedule();
    } else {
      cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
          loadData.fetcher.getDataSource(), originalKey);
    }
  }




<div class="se-preview-section-delimiter"></div>

获取到数据后,分两种情况,一是需要将图片缓存到本地硬盘,一种是不需要缓存。Glide配置了多种缓存策略,默认是自动智能切换缓存存储策略,Glide认为远程网络图片获取是昂贵的,所以默认网络图片是会缓存原图的。而本地图片,包括drawable/assets等是不会缓存原图的。(当然你也可以重新配置)

那么这里获得的是网络图片,所以会进入if中,而else则是直接将结果返回给DecodeJob进行解码了。

进入if后,会将数据保存起来(留意dataToCache),然后重启任务。

reschedule后,将会接连回调DecodeJob和EngineJob的reschedule方法,从而重新开启DecodeJob任务。

  //DecodeJob.java
  @Override
  public void reschedule() {
    runReason = RunReason.SWITCH_TO_SOURCE_SERVICE;
    callback.reschedule(this);
  }

  //EngineJob.java
  @Override
  public void reschedule(DecodeJob<?> job) {
    getActiveSourceExecutor().execute(job);
  }




<div class="se-preview-section-delimiter"></div>

由于开启的是同一个DecodeJob任务,所以整个任务的内容是会继续得到执行的。结果仍然是回到SourceGenerator的startNext方法中

而这个时候,就会进入上面提到的另一个情况。

  //SourceGenerator.java

  public boolean startNext() {
    if (dataToCache != null) {
      Object data = dataToCache;
      dataToCache = null;
      //1:先缓存数据
      cacheData(data);
    }

    //2:再重新执行数据转换和抓取
    if (sourceCacheGenerator != null &&
        sourceCacheGenerator.startNext()) {
      return true;
    }

    //省略循环遍历代码......
  }

  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);
  }

这时dataToCache将不再是null的,所以会将数据缓存到本地硬盘,并启动另一个加载器DataCacheGenerator,而这个Generator正是用来加载缓存在本地的图片的。

而DataCacheGenerator和ResourceCacheGenerator的原理与SourceGenerator基本是一致的,只不过一个用来加载原始的本地缓存图,另一个用来加载处理过的本地缓存。

最后,来总结一下Glide整个的数据转换与抓取流程:

  1. Glide利用线程池的方式,将每一个解码过程都封装为一次解码任务。
  2. 整个数据抓取过程中,Glide会尝试从内存到处理过的图片缓存,再到原图缓存,最后到远程图片等四个地方进行数据加载。(这里的远程图片包括drawable/assets等资源)
  3. 数据模型转换时,根据Glide初始化时注册的模型转换注册表,将原始model模型数据转换为可能的数据模型,并尝试使用这些模型来抓取数据,直至抓取到数据,或抓取失败返回。
  4. Glide数据转模型使得Glide有非常好的拓展性和重用性。
  5. 整个数据转换和抓取流程非常复杂,但是只要抓住其中一个源头,并跟踪下去,其实还是非常清晰的,也可以看到Glide设计的优雅高明之处。

以上,就是Glide数据模型转换和抓取的流程分析,下一篇我们将进入Glide的解码和转码源码分析。


Glide4源码解析系列

[Glide4源码解析系列]–1.Glide初始化
[Glide4源码解析系列]–2.Glide数据模型转换与数据抓取


EventBus源码的简单解析

剔除繁杂的理论,注重实践,系统地讲解EventBus中的的源码
  • 2018年03月27日 09:22

浅谈Android Architecture Components

浅谈Android Architecture Components浅谈Android Architecture Components 简介 Android Architecture Component...
  • xia215266092
  • xia215266092
  • 2017-06-24 16:04:29
  • 9889

初探Architecture Components之LiveData

在初探Architecture Components之Lifecycle中,我们已经了解到Lifecycle是如何与组件的生命周期相关联的。在本文中,我们将会了解Architecture Compon...
  • IO_Field
  • IO_Field
  • 2017-07-18 14:01:11
  • 2864

谷歌官方Android应用架构库——LiveData

谷歌官方Android应用架构库——LiveData
  • hubinqiang
  • hubinqiang
  • 2017-06-11 05:06:00
  • 9712

Android 应用架构组件(Architecture Components)实践

Architecture Components 是在 2017 年 Google I/O 大会上,Google 官方推出的一个构建 Android 应用架构的库。它可以帮你避免在 Android 应用...
  • suyimin2010
  • suyimin2010
  • 2018-02-27 08:35:54
  • 149

android LiveData

LiveData是数据holder类,并支持数据可被监听(观察)。和传统的观察者模式中的被观察者不一样,LiveData是一个生命周期感知组件,因此观察者可以指定某一个LifeCycle给LiveDa...
  • lmjssjj
  • lmjssjj
  • 2017-05-24 11:10:12
  • 1965

architecture-components

开发者经常面临的问题 Android应用由四大组件构成,各组件可以被独立且无序的调起,用户会在各个App之间来回切换。组件启动后,生命周期会受用户的操作和系统影响,不完全受开发者控制。而由于设...
  • Jaden_hool
  • Jaden_hool
  • 2018-01-04 19:51:15
  • 73

Android架构组件之LiveData

基本概念LiveData是一个可以被观察的数据持有类,它可以感知并遵循Activity、Fragment或Service等组件的生命周期。正是由于LiveData对组件生命周期可感知特点,因此可以做到...
  • u014738140
  • u014738140
  • 2017-11-29 18:14:26
  • 924

Android架构组件—LiveData

概述 简单地说,LiveData是一个数据持有类。它具有以下特点: 数据可以被观察者订阅; 能够感知组件(Fragment、Activity、Service)的生命周期; 只有在组件出于激活状态...
  • qq_24442769
  • qq_24442769
  • 2018-03-02 16:01:37
  • 193

Android Architecture Components应用架构组件源码详解(基于1.0以上)(第二篇ViewModel和LiveData)

熟悉mvp模式的小伙伴应该都清楚,m-&amp;gt;Model,v-&amp;gt;View,p-&amp;gt;presenter, p层调用model层的业务处理,将结果通过接口返回给View层...
  • xiatiandefeiyu
  • xiatiandefeiyu
  • 2017-11-29 14:56:13
  • 1074
收藏助手
不良信息举报
您举报文章:[Glide4源码解析系列]–2.Glide数据模型转换与数据抓取
举报原因:
原因补充:

(最多只允许输入30个字)