开发者

Android图片加载框架Gilde源码层深入分析

开发者 https://www.devze.com 2022-11-28 12:26 出处:网络 作者: lpf_wei
目录1.使用Gilde显示一张图片2.Glidewith操作源码解析3.Glide的load操作4.Glide的into流程解析5.Glide1.使用Gilde显示一张图片Glide.with(t...
目录
  • 1.使用Gilde显示一张图片
  • 2.Glide with操作源码解析
  • 3.Glide 的load操作
  • 4.Glide的into流程解析
  • 5.Glide

1.使用Gilde显示一张图片

Glide.with(this).
        load("https://cn.bing.com/sa/simg/hpb/xxx.jpg")
        .into(imageView);

上边是Glide最简单的来显示一张图片,虽然只有三步操作:with、load、into,但是gilde却通过大量的代码在维护着。

with:返回一个RequestManager

load:返回一个RequestBuilder

下面通过源码分析这三块,重点是Glide生明周期的管理和缓存的使用,以及整个加载的流程的梳理。

2.Glide with操作源码解析

Glide的with函数由于参数类型原因有多个重载,下面选择传入Activity当做参数的

public static RequestManager with(@NonNull Activity activity) {
  return getRetriever(activity).get(activity);
}

这个方法就是得到一个RequestManager

分析getRetriever

private static RequestManagerRetriever getRetriever(@Nullable Context context) {
  Preconditions.checkNotNull(
      context,
      "You cannot start a load on a not yet attached View or a Fragment where getActivity() "
          + "returns null (which usually occurs when getActivity() is called before the Fragment "
          + "is attached or after the Fragment is destroyed).");
  return Glide.get(context).getRequestManagerRetriever();
}

得到一个RequestManagerRetriever,这个管理RequestManager

RequestManagerRetriever的get方法

public RequestManager get(@NonNull Context context) {
  if (context == null) {
    throw new IllegalArgumentException("You cannot start a load on a null Context");
  } else if (Util.isOnMainThread() && !(context instanceof Application)) {
    if (context instanceof FragmentActivity) {
      return get((FragmentActivity) context);
    } else if (context instanceof Activity) {
      return get((Activity) context);
    } else if (context instanceof ContextWrapper
        // Only unwrap a ContextWrapper if the baseContext has a non-null application context.
        // Context#createPackageContext may return a Context without an Application instance,
        // in which case a ContextWrapper may be used to attach one.
        && ((ContextWrapper) context).getBaseContext().getApplicationContext() != null) {
      return get(((ContextWrapper) context).getBaseContext());
    }
  }
  return getApplicationManager(context);
}

根据不同的参数类型,执行不同的代码逻辑。

参数是:FragmentActivity的源码

public RequestManager get(@NonNull FragmentActivity activity) {
  if (Util.isOnBackgroundThread()) {
    return get(activity.getApplicationContext());
  } else {
    assertNotDestroyed(activity);
    frameWaiter.registerSelf(activity);
    FragmentManager fm = activity.getSupportFragmentManager();
    return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
  }
}

注意这里判断了线程,如果是子线程会直接走ApplicationContext类型的函数重载。

supportFragmentGet方法:

private RequestManager supportFragmentGet(
    @NonNull Context context,
    @NonNull FragmentManager fm,
    @Nullable Fragment parentHint,
    boolean isParentVisible) {
  SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm, parentHint);
  RequestManager requestManager = current.getRequestManager();
  if (requestManager == null) {
    // TODO(b/27524013): Factor out this Glide.get() call.
    Glide glide = Glide.get(context);
    requestManager =
        factory.build(
            glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
    // This is a开发者_JAVA开发 bit of hack, we're going to start the RequestManager, but not the
    // corresponding Lifecycle. It's safe to start the RequestManager, but starting the
    // Lifecycle might trigger memory leaks. See b/154405040
    if (isParentVisible) {
      requestManager.onStart();
    }
    current.setRequestManager(requestManager);
  }
  return requestManager;
}

这个方法会创建一个空白的SupportRequestManagerFragment,这个Fragment的作用很重要,也是Glide的核心,声明周期的管理就是靠这个来监控的。

监控了:onStart、onStop、onDestroy,所以Glide在使用的时候,我们不用关心资源的释放,因为Glide会自动释放资源,释放资源的依据就是这个声明周期的管理。

get函数参数是:ApplicationContext 的源码

private RequestManager getApplicationManager(@NonNull Context context) {
  // Either an application context or we're on a background thread.
  if (applicationManager == null) {
    synchronized (this) {
      if (applicationManager == null) {
        Glide glide = Glide.get(context.getApplicationContext());
        applicationManager =
            factory.build(
                glide,
                new ApplicationLifecycle(),
                new EmptyRequestManagerTreeNode(),
                context.getApplicationContext());
      }
    }
  }
  return applicationManager;
}

可见当参数类型是:ApplicationContext或者子线程调用with方法不会创建空白的Fragmnet来进行生命周期的监控,也不会进行资源释放,所以在调用with方法的时候不能传ApplicationContext。

下面分析Glide分别在onStart、onStop、onDestroy执行了那些逻辑

RequestManager的onStart方法

public synchronized void onStart() {
  resumeRequests();
  targetTracker.onStart();
}

执行了刷新请求和Tracker的onStart,resumeRequests这个跟请求的缓存的使用

RequestTracker 的resumeRequests方法

public void resumeRequests() {
  isPaused = false;
  for (Request request : Util.getSnapshot(requests)) {
    if (!request.isComplete() && !request.isRunning()) {
      request.begin();
    }
  }
  pendingRequests.clear();
}

将运行队列requests所有没有完成又处于停止状态的任务全部开始执行,requests是一个正在运行的请求缓存集合,这是一个set集合,处于等待状态的请求队列pendingRequests进行清理操作。这个就是Glide感应到声明周期onStart进行的操作。

RequestManager的onStop方法

public synchronized void onStop() {
  pauseRequests();
  targetTracker.onStop();
}

RequestTracker的pauseRequests方法

public void pauseRequests() {
  isPaused = true;
  for (Request request : Util.getSnapshot(requests)) {
    if (request.isRunning()) {
      request.pause();
      pendingRequests.add(request);
    }
  }
}

request.pause():将所有的运行缓存集合中的请求进行暂停操作,并将这些请求加入到等待缓存请求集合pendingRequests

RequestManager的onDestroy

public synchronized void onDestroy() {
  targetTracker.onDestroy();
  for (Target<?> target : targetTracker.getAll()) {
    clear(target);
  }
  targetTracker.clear();
  requestTracker.clearRequests();
  lifecycle.removeListener(this);
  lifecycle.removeListener(connectivityMonitor);
  Util.removeCallbacksOnUiThread(addSelfToLifecycle);
  glide.unregisterRequestManager(this);
}

可以看到Glide在感应到onDestroy会进行各编程客栈种资源释放,监听的移除操作。

3.Glide 的load操作

RequestManager的load方法

public RequestBuilder<Drawable> load(@Nullable Bitmap bitmap) {
  return asDrawable().load(bitmap);
}
public RequestBuilder<Drawable> asDrawable() {
   return as(Drawable.class);
}
public <ResourceType> RequestBuilder<ResourceType> as(
    @NonNull Class<ResourceType> resourceClass) {
   return new RequestBuilder<>(glide, this, resourceClass, context);
}

load方法返回值是RequestBuilder,通过new的方式进行创建

RequestBuilder的构造方法

protected RequestBuilder(
    @NonNull Glide glide,
    RequestManager requestManager,
    Class<TranscodeType> transcodeClass,
    Context context) {
  this.glide = glide;
  this.requestManager = requestManager;
  this.transcodeClass = transcodeClass;
  this.context = context;
  this.transitionOptions = requestManager.getDefaultTransitionOptions(transcodeClass);
  this.glideContext = glide.getGlideContext();
  initRequestListeners(requestManager.getDefaultRequestListeners());
  apply(requestManager.getDefaultRequestOptions());
}

initRequestListeners:初始化监听

apply:校验参数

load的过程比较简单,重点的into将会非常复杂

4.Glide的into流程解析

public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
  return into(target, /*targetListener=*/ null, Executors.mainThreadExecutor());
}
<Y extends Target<TranscodeTypphpe>> Y into(
    @NonNull Y target,
    @Nullable RequestListener<TranscodeType> targetListener,
    Executor callbackExecutor) {
    return into(target, targetListener, /*options=*/ this, callbackExecutor);
 }

into另外一个重载方法:

private <Y extends Target<TranscodeType>> Y into(
    @NonNull Y target,
    @Nullable RequestListener<TranscodeType> targetListener,
    BaseRequestOptions<?> options,
    Executor callbackExecutor) {
  Preconditions.checkNotNull(target);
  if (!isModelSet) {
    throw new IllegalArgumentException("You must call #load() before calling #into()");
  }
  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;
}

buildRequest 会通过SingleRequest.obtain,创建一个SingleRequest,这个必须要前置了解,后边的流程需要用到。

requestManager.track(target, request):这个是主流程,下面来看RequestManager的track方法

RequestManager的track方法

synchronized void track(@NonNull Target<?> target, @NonNull Request request) {
  targetTracker.track(target);
  requestTracker.runRequest(request);
}

RequestTracker的runRequest方法

public void runRequest(@NonNull Request request) {
  requests.add(request);
  if (!isPaused) {
    request.begin();
  } else {
    request.clear();
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
      Log.v(TAG, "Paused, delaying request");
    }
    pendingRequests.add(request);
  }
}

这里的Request是一个接口,调用了begin方法,这个Request就是前面创建的SingleRequest

SingleRequest的begin

public void begin() {
  synchronized (requestLock) {
    ...
    if (status == Status.COMPLETE) {
      onResourceReady(
          resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
      return;
    }
    cookie = GlideTrace.beginSectionAsync(TAG);
    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());
    }
    if (IS_VERBOSE_LOGGABLE) {
      logV("finished run method in " + LogTime.getElapsedMillis(startTime));
    }
  }
}

如果没有设置宽高会调用 target.getSize(this)来获取大小,如果设置了会执行onSizeReady方法

SingleRequest的onSizeReady方法

public void onSizeReady(int width, int height) {
  stateVerifier.throwIfRecycled();
  synchronized (requestLock) {
    if (IS_VERBOSE_LOGGABLE) {
      logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
    }
    if (status != Status.WAITING_FOR_SIZE) {
      return;
    }
    status = Status.RUNNING;
    float sizeMultiplier = requestOptions.getSizeMultiplier();
    this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
    this.height = maybeApplySizeMultiplier(height, sizeMultiplier);

    if (IS_VERBOSE_LOGGABLE) {
      logV("finished setup for calling loa编程d in " + LogTime.getElapsedMillis(startTime));
    }
    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,
            callbackExecutor);
    if (status != Status.RUNNING) {
      loadStatus = null;
    }
    if (IS_VERBOSE_LOGGABLE) {
      logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
    }
  }
}

接下来调用Engin的load方法

Engin的load的方法

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,
    Executor callbackExecutor) {
  long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
  EngineKey key =
      keyFactory.buildKey(
          model,
          signature,
        http://www.devze.com  width,
          height,
          transformations,
          resourceClass,
          transcodeClass,
          options);
  EngineResource<?> memoryResource;
  synchronized (this) {
    memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
    if (memoryResource == null) {
      return waitForExistingOrStartNewJob(
          glideContext,
          model,
          signature,
          width,
          height,
          resourceClass,
          transcodeClass,
          priority,
          diskCacheStrategy,
          transformations,
          isTransformationRequired,
          isScaleOnlyOrNoTransform,
          options,
          isMemoryCacheable,
          useUnlimitedSourceExecutorPool,
          useAnimationPool,
          onlyRetrieveFromCache,
          cb,
          callbackExecutor,
          key,
          startTime);
    }
  }
  // Avoid calling back while holding the engine lock, doing so makes it easier for callers to
  // deadlock.
  cb.onResourceReady(
      memoryResource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
  return null;
}

loadFromMemory这个方法是先从缓存中去查找,如果有直接就可以直接使用了

loadFromMemory方法

@Nullable
private EngineResource<?> loadFromMemory(
    EngineKey key, boolean isMemoryCacheable, long startTime) {
  if (!isMemoryCacheable) {
    return null;
  }
  EngineResource<?> active = loadFroMACtiveResources(key);
  if (active != null) {
    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Loaded resource from active resources", startTime, key);
    }
    return active;
  }
  EngineResource<?> cached = loadFromCache(key);
  if (cached != null) {
    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Loaded resource from cache", startTime, key);
    }
    return cached;
  }
  return null;
}

loadFromActiveResources:从活动缓存查找,如果有直接返回使用,这个缓存是一个Map集合

loadFromCache:从内存缓存查找,如果有直接使用,这个是一个LRU集合

上边两个是都是内存缓存,只要显示在页面上的,都会缓存在活动缓存。

Engin的waitForExistingOrStartNewJob方法

private <R> LoadStatus waitForExistingOrStartNewJob(
    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,
    Executor callbackExecutor,
    EngineKey key,
    long startTime) {
  EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
  if (current != null) {
    current.addCallback(cb, callbackExecutor);
    if (VERBOSE_IS_LOGGABLE) {
      logWithTimeAndKey("Added to existing load", startTime, key);
   编程 }
    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);
  if (VERBOSE_IS_LOGGABLE) {
    logWithTimeAndKey("Started new load", startTime, key);
  }
  return new LoadStatus(cb, engineJob);
}

engineJob 管理一个线程池,decodeJob这个是任务的抽象是一个Runable,所以下面就会执行任务的run方法

DecodeJob的run方法

public void run() {
  ...
  try {
    if (isCancelled) {
      notifyFailed();
      return;
    }
    runWrapped();
  } catch (CallbackException e) {
    // If a callback not controlled by Glide throws an exception, we should avoid the Glide
    // specific debug logic below.
    throw e;
  } catch (Throwable 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();
    }
    GlideTrace.endSection();
  }
}

DecodeJob的runWrapped方法

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

默认会执行case INITIALIZE的runGenerators(),getNextGenerator得到的是SourceGenerator,这个必须要明确,否则走不下去。

SourceGenerator的startNext方法

public boolean 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;
      startNextLoad(loadData);
    }
  }
  return started;
}

DecodeHelper的getLoadData方法

List<LoadData<?>> getLoadData() {
  if (!isLoadDataSet) {
    isLoadDataSet = true;
    loadData.clear();
    List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
    //noinspection ForLoopReplaceableByForEach to improve perf
    for (int i = 0, size = modelLoaders.size(); 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;
}

HttpGlideUrlLoader的buildLoadData方法

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

可以看到最终返回的是包含HttpUrlFetcher的loadData

SourceGenerator的startNextLoad方法

private void startNextLoad(final LoadData<?> toStart) {
  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);
          }
        }
      });
}

loadData.fetcher的fetcher就是HttpUrlFetcher

HttpUrlFetcher的loadData方法

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

loadDataWithRedirects方法

private InputStream loadDataWithRedirects(
    URL url, int redirects, URL lastUrl, Map<String, String> headers) throws HttpException {
  if (redirects >= MAXIMUM_REDIRECTS) {
    throw new HttpException(
        "Too many (> " + MAXIMUM_REDIRECTS + ") redirects!", INVALID_STATUS_CODE);
  } else {
    // Comparing the URLs using .equals performs additional network I/O and is generally broken.
    // See http://michaelscharf.blogspot.com/2006/11/Javaneturlequals-and-hashcode-make.html.
    try {
      if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
        throw new HttpException("In re-direct loop", INVALID_STATUS_CODE);
      }
    } catch (URISyntaxException e) {
      // Do nothing, this is best effort.
    }
  }
  urlConnection = buildAndConfigureConnection(url, headers);
  try {
    // Connect explicitly to avoid errors in decoders if connection fails.
    urlConnection.connect();
    // Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352.
    stream = urlConnection.getInputStream();
  } catch (IOException e) {
    throw new HttpException(
        "Failed to connect or obtain data", getHttpStatusCodeOrInvalid(urlConnection), e);
  }
  ...
}

到这里就看到urlConnection拿到图片的InputStream,终于从一个网络链接请求到流了,然后就是将流转成bitmap显示的过程了,感兴趣的可以继续查看。

5.Glide

声明周期和缓存总结 Glide 调用with函数如果传入的ApplicationContext或者在子线程不会为我们创建空白的Fragment,不会进行生命周期的监控,也就不会进行资源自动释放,所以不用传ApplicationContext。其他情况glide会自动创建空白的Fragment来进行生命周期监控,并自动进行资源释放。

Glide的缓存分为4级缓存:活动缓存、内存缓存、磁盘缓存、网络缓存

  • 活动缓存,是一个Map集合,加载图片会首先从这个缓存里边查找。
  • 内存缓存,是一个LRU缓存集合,如果获取缓存没有就从内存缓存查找。
  • 磁盘缓存,是disklrucache缓存集合,如果从内存缓存没有找到就从磁盘缓存查找。
  • 网络缓存,如果磁盘缓存没有就需要从网络上就行请求加载了。

到此这篇关于android图片加载框架Gilde源码层深入分析的文章就介绍到这了,更多相关Android Gilde内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号