我是靠谱客的博主 潇洒魔镜,这篇文章主要介绍Android最火的框架系列(六)Glide,现在分享给大家,希望可以做个参考。

        Android开源的图片加载框架有很多,常见的四种分别是:ImageLoader、Picasso、Glide和Fresco。其实,这些框架加载普通的图片,使用方法都差不多。这篇文章,我也不会花很多的篇幅去比较几种框架的优缺点。

        这几种框架我都使用过,都是比较简单的几行代码就可以实现图片的加载。在实际项目开发中,我基本是使用Glide。放一下GitHub上的star数。ImageLoader(16.5K),Picasso(16.9K),Glide(26.5K),Fresco(15.7K)。嗯,还是Glide的星星多一点。接下来,简单的总结一下Glide的基本使用。

一.Glide的导入

    首先,Glide的GitHub地址:https://github.com/bumptech/glide,可以看到,当前最新版本是4.9.0。我们在gradle文件中添加依赖:

复制代码
1
2
implementation 'com.github.bumptech.glide:glide:4.9.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'

二.Glide的使用

1.Glide加载普通图片

        Glide加载普通图片,需要三个参数:Context,URL,ImageView。无论是网络图片,还是drawable下的图片,还是手机内存里的图片,都只需要指定正确的URL即可。

复制代码
1
2
3
4
5
Glide.with(LoginActivity.this)         .load(R.drawable.login_logo)         .into(mLoginLogo);

2.Glide加载圆角图片

    Glide加载圆角图片,除了Context,url,ImageView外,还需要另一个参数指定圆角的半径:

复制代码
1
2
3
4
Glide.with(LoginActivity.this) .load(R.drawable.login_logo) .apply(RequestOptions.bitmapTransform(new RoundedCorners(20))) .into(mLoginLogo);

3.Glide加载圆形图片

复制代码
1
2
3
4
Glide.with(LoginActivity.this) .load(R.drawable.login_logo) .apply(RequestOptions.bitmapTransform(new CircleCrop())) .into(mLoginLogo);

4.Glide加载gif图片

    Glide支持加载gif图片,使用方法与加载普通图片一样,也可以为其指定圆角或者圆形。

复制代码
1
2
3
Glide.with(LoginActivity.this) .load(R.drawable.test) .into(mLoginLogo);

三.Glide源码分析

1.with()

    通过上面对Glide的基本使用,我们对Glide的源码做一下分析。首先,我们看下.with()方法的源码。如我们前面所说的,with方法需要我们传入context,可以看到,context可以是任何context:activity,fragmentActivity,fragment等。然后,会通过传入的context通过getRetriever().get()方法返回一个RequestManager对象。也就是说,通过with方法传入需要使用Glide加载图片的Activity或者Fragment,然后为这个Activity或者fragment提供一个用于加载图片的RequestManager。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static RequestManager with(@NonNull Context context) { return getRetriever(context).get(context); } public static RequestManager with(@NonNull Activity activity) { return getRetriever(activity).get(activity); } public static RequestManager with(@NonNull FragmentActivity activity) { return getRetriever(activity).get(activity); } public static RequestManager with(@NonNull Fragment fragment) { return getRetriever(fragment.getActivity()).get(fragment); } public static RequestManager with(@NonNull android.app.Fragment fragment) { return getRetriever(fragment.getActivity()).get(fragment); } public static RequestManager with(@NonNull View view) { return getRetriever(view.getContext()).get(view); }

        我们看一下RequestManager这个类的介绍:

复制代码
1
2
3
4
5
/** * A class for managing and starting requests for Glide. Can use activity, fragment and connectivity * lifecycle events to intelligently stop, start, and restart requests. Retrieve either by * instantiating a new object, or to take advantage built in Activity and Fragment lifecycle * handling, use the static Glide.load methods with your Fragment or Activity.

        一个为Glide管理和开始请求的类。可以通过activity,fragment等的生命周期来停止、开始和重新开始请求。所谓的请求,就是加载图片的请求。而RequestManager可以通过Activity等的生命周期来自动的管理图片的加载。例如,在Activity执行onDestroy的时候,Glide应该停止图片的加载。

2.load()

        接下来,看一下load方法。通过前面的介绍,我们知道,load方法接收的是图片的路径,这个路径可以是任意的路径。通过源码我们可以知道,load接收的参数,可以是下面的任意一种:bitmap,drawable,string,uri,file,resourceId,url,byte数组,Object。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public RequestBuilder<Drawable> load(@Nullable Bitmap bitmap) { return asDrawable().load(bitmap); } public RequestBuilder<Drawable> load(@Nullable Drawable drawable) { return asDrawable().load(drawable); } public RequestBuilder<Drawable> load(@Nullable String string) { return asDrawable().load(string); } public RequestBuilder<Drawable> load(@Nullable Uri uri) { return asDrawable().load(uri); } public RequestBuilder<Drawable> load(@Nullable File file) { return asDrawable().load(file); } public RequestBuilder<Drawable> load(@RawRes @DrawableRes @Nullable Integer resourceId) { return asDrawable().load(resourceId); } public RequestBuilder<Drawable> load(@Nullable URL url) { return asDrawable().load(url); } public RequestBuilder<Drawable> load(@Nullable byte[] model) { return asDrawable().load(model); } public RequestBuilder<Drawable> load(@Nullable Object model) { return asDrawable().load(model); }

        继续跟踪asDrawable().load()方法,先看一下第一个asDrawable方法:

复制代码
1
2
3
public RequestBuilder<Drawable> asDrawable() { return as(Drawable.class); }

继续跟踪as方法,可以看到,在这里获取了一个RequestBuilder对象。

复制代码
1
2
3
4
public <ResourceType> RequestBuilder<ResourceType> as( @NonNull Class<ResourceType> resourceClass) { return new RequestBuilder<>(glide, this, resourceClass, context); }

我们跟踪第二个方法load方法,也是跳转到RequestBuilder类:

复制代码
1
2
3
public RequestBuilder<TranscodeType> load(@Nullable String string) { return loadGeneric(string); }

        继续跟踪loadGeneric方法,通过这一系列的方法,最后,我们的url传到了这里,同时,isModelSet置为true,也就是标志着我们的数据源设置成功,最后返回RequestBuilder自己。

复制代码
1
2
3
4
5
private RequestBuilder<TranscodeType> loadGeneric(@Nullable Object model) { this.model = model; isModelSet = true; return this; }

3.into()

        最后,我们看下into方法的源码。这个方法里面,会获取imageview的ScaleType属性,根据不同的ScaleType得到不同的requestOptions参数。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public ViewTarget<ImageView, TranscodeType> into(@NonNull ImageView view) { Util.assertMainThread(); Preconditions.checkNotNull(view); BaseRequestOptions<?> requestOptions = this; 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, Executors.mainThreadExecutor()); }

        上面,最后return into(),我们继续跟踪。可以看到,前面通过load设置的isModelSet在这起作用了。如果我们没有调用load方法,也就是没有设置数据源,会抛出一个异常。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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)) { 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 un-tracking Targets, and obtaining View dimensions // that are done in the individual Request. previous.begin(); } return target; } requestManager.clear(target); target.setRequest(request); requestManager.track(target, request); return target; }

        上面的方法,我们看到一个target,首先,我们先跟踪一下target是个什么东西:

复制代码
1
2
3
4
public <X> ViewTarget<ImageView, X> buildImageViewTarget( @NonNull ImageView imageView, @NonNull Class<X> transcodeClass) { return imageViewTargetFactory.buildTarget(imageView, transcodeClass); }

        继续跟踪buildTarget方法,通过对图片类型的判断,创建并返回与图片来源对应的imageViewTarget:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ImageViewTargetFactory { @NonNull @SuppressWarnings("unchecked") public <Z> ViewTarget<ImageView, Z> buildTarget(@NonNull ImageView view, @NonNull Class<Z> clazz) { if (Bitmap.class.equals(clazz)) { return (ViewTarget<ImageView, Z>) new BitmapImageViewTarget(view); } else if (Drawable.class.isAssignableFrom(clazz)) { return (ViewTarget<ImageView, Z>) new DrawableImageViewTarget(view); } else { throw new IllegalArgumentException( "Unhandled class: " + clazz + ", try .as*(Class).transcode(ResourceTranscoder)"); } } }

        into方法的target我们分析到此为止,继续看一下into方法的其他代码,我们会发现其中这么两行代码:

复制代码
1
2
3
4
Request request = buildRequest(target, targetListener, options, callbackExecutor); target.setRequest(request); requestManager.track(target, request);

        继续看一下buildRequest方法,我们发现,这个加载图片的request里面有很多参数,例如target和宽高信息。后面,target.setRequest()。最后,调用RequestManager的track方法。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Request buildRequest( Target<TranscodeType> target, @Nullable RequestListener<TranscodeType> targetListener, BaseRequestOptions<?> requestOptions, Executor callbackExecutor) { return buildRequestRecursive( target, targetListener, /*parentCoordinator=*/ null, transitionOptions, requestOptions.getPriority(), requestOptions.getOverrideWidth(), requestOptions.getOverrideHeight(), requestOptions, callbackExecutor); }

        前面我们就知道了,RequestManager是管理图片加载开始和停止的类。我们继续看一下track方法。track方法里,我们看到了runRequest方法。不管英语好不好,看到runRequest基本都能猜到意思:执行一个请求。

复制代码
1
2
3
4
synchronized void track(@NonNull Target<?> target, @NonNull Request request) { targetTracker.track(target); requestTracker.runRequest(request); }

        接下来,看一下runRequest方法。如果请求的标志不是暂停的,开始这个请求,如果请求的标志是暂停的,那么清除这个请求,也就是停止加载图片的请求。

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
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); } }

        接下来,毫无疑问的,我们需要看一下begin这个方法,点进去,是Request接口,实现begin方法的是实现Request的SingleRequest类,我们看一下begin 方法:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public synchronized void begin() { assertNotCallingCallbacks(); stateVerifier.throwIfRecycled(); startTime = LogTime.getLogTime(); if (model == null) { if (Util.isValidDimensions(overrideWidth, overrideHeight)) { width = overrideWidth; height = overrideHeight; } // Only log at more verbose log levels if the user has set a fallback drawable, because // fallback Drawables indicate the user expects null models occasionally. int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG; onLoadFailed(new GlideException("Received null model"), logLevel); return; } if (status == Status.RUNNING) { throw new IllegalArgumentException("Cannot restart a running request"); } // If we're restarted after we're complete (usually via something like a notifyDataSetChanged // that starts an identical request into the same Target or View), we can simply use the // resource and size we retrieved the last time around and skip obtaining a new size, starting a // new load etc. This does mean that users who want to restart a load because they expect that // the view size has changed will need to explicitly clear the View or Target before starting // the new load. if (status == Status.COMPLETE) { onResourceReady(resource, DataSource.MEMORY_CACHE); return; } // Restarts for requests that are neither complete nor running can be treated as new requests // and can run again from the beginning. 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)); } }

        大体的过程如下:

        (1)获取宽高,如果有重新设置宽高并且均大于零,那么宽高使用重新设置的宽高。

        (2)宽高信息确定后,不论是处于RUNNING状态还是WAITING_FOR_SIZE状态,首先会加载占位图。

        (3)在WAITING_FOR_SIZE下,如果宽高合法,那么会走onSizeReady方法。至此,我们还没看到解码图片的代码,我们跟踪onSizeReady方法:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public synchronized void onSizeReady(int width, int height) { stateVerifier.throwIfRecycled(); 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 load 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); // This is a hack that's only useful for testing right now where loads complete synchronously // even though under any executor running on any thread but the main thread, the load would // have completed asynchronously. if (status != Status.RUNNING) { loadStatus = null; } if (IS_VERBOSE_LOGGABLE) { logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime)); } }

        看了上面的代码,毫无疑问,重点是engine.load方法,继续跟踪:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public synchronized <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, width, height, transformations, resourceClass, transcodeClass, options); EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable); if (active != null) { cb.onResourceReady(active, DataSource.MEMORY_CACHE); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Loaded resource from active resources", startTime, key); } return null; } EngineResource<?> cached = loadFromCache(key, isMemoryCacheable); if (cached != null) { cb.onResourceReady(cached, DataSource.MEMORY_CACHE); if (VERBOSE_IS_LOGGABLE) { logWithTimeAndKey("Loaded resource from cache", startTime, key); } return null; } 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); }

        上面的代码,我们发现两个疑似加载图片的地方:loadFromActiveResources,loadFromCache。这其实是两种加载图片的方式,从弱引用加载和从缓存加载。这两个方法位于Engine类中,Engine类负责开始加载图片和管理弱引用和缓存的资源。

        engineJob添加了一个回调接口:engineJob.addCallback(cb, callbackExecutor)。跟进去,最后实际上就是上面两种解码方式的onResourceReady回调:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public synchronized void onResourceReady(Resource<?> resource, DataSource dataSource) { stateVerifier.throwIfRecycled(); loadStatus = null; if (resource == null) { GlideException exception = new GlideException("Expected to receive a Resource<R> with an " + "object of " + transcodeClass + " inside, but instead got null."); onLoadFailed(exception); return; } Object received = resource.get(); if (received == null || !transcodeClass.isAssignableFrom(received.getClass())) { releaseResource(resource); GlideException exception = new GlideException("Expected to receive an object of " + transcodeClass + " but instead" + " got " + (received != null ? received.getClass() : "") + "{" + received + "} inside" + " " + "Resource{" + resource + "}." + (received != null ? "" : " " + "To indicate failure return a null Resource " + "object, rather than a Resource object containing null data.")); onLoadFailed(exception); return; } if (!canSetResource()) { releaseResource(resource); // We can't put the status to complete before asking canSetResource(). status = Status.COMPLETE; return; } onResourceReady((Resource<R>) resource, (R) received, dataSource); }

        核心还是最后一行代码,继续跟踪进去:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private synchronized void onResourceReady(Resource<R> resource, R result, DataSource dataSource) { // We must call isFirstReadyResource before setting status. boolean isFirstResource = isFirstReadyResource(); status = Status.COMPLETE; this.resource = resource; if (glideContext.getLogLevel() <= Log.DEBUG) { Log.d(GLIDE_TAG, "Finished loading " + result.getClass().getSimpleName() + " from " + dataSource + " for " + model + " with size [" + width + "x" + height + "] in " + LogTime.getElapsedMillis(startTime) + " ms"); } isCallingCallbacks = true; try { boolean anyListenerHandledUpdatingTarget = false; if (requestListeners != null) { for (RequestListener<R> listener : requestListeners) { anyListenerHandledUpdatingTarget |= listener.onResourceReady(result, model, target, dataSource, isFirstResource); } } anyListenerHandledUpdatingTarget |= targetListener != null && targetListener.onResourceReady(result, model, target, dataSource, isFirstResource); if (!anyListenerHandledUpdatingTarget) { Transition<? super R> animation = animationFactory.build(dataSource, isFirstResource); target.onResourceReady(result, animation); } } finally { isCallingCallbacks = false; } notifyLoadSuccess(); }

        核心代码是target.onResourceReady(result, animation)。实际执行这个方法的是抽象类ImageViewTarget:

复制代码
1
2
3
4
5
6
7
8
@Override public void onResourceReady(@NonNull Z resource, @Nullable Transition<? super Z> transition) { if (transition == null || !transition.transition(resource, this)) { setResourceInternal(resource); } else { maybeUpdateAnimatable(resource); } }

        继续跟踪方法setResourceInternal(resource):

复制代码
1
2
3
4
5
6
private void setResourceInternal(@Nullable Z resource) { // Order matters here. Set the resource first to make sure that the Drawable has a valid and // non-null Callback before starting it. setResource(resource); maybeUpdateAnimatable(resource); }

        继续跟踪setResource方法,这是抽象类ImageViewTarget一个抽象方法,跟踪到实现类DrawableImageViewTarget。最后,我们发现,通过imageview的setDrawable方法,将drawable显示到ImageView中。  

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DrawableImageViewTarget extends ImageViewTarget<Drawable> { public DrawableImageViewTarget(ImageView view) { super(view); } /** * @deprecated Use {@link #waitForLayout()} instead. */ // Public API. @SuppressWarnings({"unused", "deprecation"}) @Deprecated public DrawableImageViewTarget(ImageView view, boolean waitForLayout) { super(view, waitForLayout); } @Override protected void setResource(@Nullable Drawable resource) { view.setImageDrawable(resource); } }

        后面,有个EnginJob和DecodeJob,并且通过engineJob.start(decodeJob)来开始解码,跟踪进去,最终是通过线程池来执行解码的任务:

复制代码
1
2
3
4
5
6
7
public synchronized void start(DecodeJob<R> decodeJob) { this.decodeJob = decodeJob; GlideExecutor executor = decodeJob.willDecodeFromCache() ? diskCacheExecutor : getActiveSourceExecutor(); executor.execute(decodeJob); }

    最后,总结一下,使用Glide加载图片确实很方便,只需要几行代码即可。其实我们没有必要太去关注Glide的源码,只要会基本的使用就可以了,跟踪源码其实是一个很漫长很复杂的过程,跟踪到最后,好几个小时,都有点坚持不下去的感觉了。最后,坚持跟踪完into方法。好了,周末结束,洗漱睡觉,下次再见!

最后

以上就是潇洒魔镜最近收集整理的关于Android最火的框架系列(六)Glide的全部内容,更多相关Android最火内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(104)

评论列表共有 0 条评论

立即
投稿
返回
顶部