前言 最近可能会写一系列文章,结合源码分析一些主流第三方库的设计与实现。为了条理清晰,每篇文章都会在前言里列出一些要点,然后按照这些要点来具体分析。
这次要分析的是当前最火热的图片加载框架Glide
。Glide
最初的主旨是为了实现列表中图片的平滑加载,如今它已经发展成了功能全面的图片加载框架,而且star数也后来居上,超越了同为Android端主流图片加载框架的Fresco
和Picasso
,Glide
这么受欢迎自然在设计上有属于自己的亮点,所以本次分析的内容要点包括:
Glide的生命周期绑定
Glide的缓存设计(三级缓存、Lru算法、Bitmap复用)
Glide的完整加载过程
生命周期绑定 Glide的一大特色是可以控制图片的加载状态与当前页面的生命周期同步,使整个加载过程随着页面的状态而启动/恢复、停止、销毁,在LifecycleListener
接口中就定义了这么几种状态用来同步监听页面的生命周期事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface LifecycleListener {
* Callback for when {@link android.app.Fragment#onStart()}} or {@link
* android.app.Activity#onStart()} is called.
*/
void onStart () ;
* Callback for when {@link android.app.Fragment#onStop()}} or {@link
* android.app.Activity#onStop()}} is called.
*/
void onStop () ;
* Callback for when {@link android.app.Fragment#onDestroy()}} or {@link
* android.app.Activity#onDestroy()} is called.
*/
void onDestroy () ;
}
那么这几种状态是如何同当前页面状态绑定的呢?显然和我们平时常用的Glide.with(context)
有关
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
* @see #with(android.app.Activity)
* @see #with(android.app.Fragment)
* @see #with(android.support.v4.app.Fragment)
* @see #with(android.support.v4.app.FragmentActivity)
*/
public static RequestManager with(Context context) {
return getRetriever(context).get(context);
}
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
Preconditions.checkNotNull(
"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();
}
with
有多种重载,支持Activity
,Fragment
,FragmentActivity
等各种Context
对象,这个方法最终会返回一个图片请求管理器RequestManager
,而RequestManager
是由Glide
的成员变量RequestManagerRetriever
执行get
方法获得的,进入RequestManagerRetriever
的源码中
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
public RequestManager get (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) {
return get(((ContextWrapper) context).getBaseContext());
}
}
return getApplicationManager(context);
}
public RequestManager get (Activity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
android.app.FragmentManager fm = activity.getFragmentManager();
return fragmentGet(activity, fm, null );
}
}
private RequestManager fragmentGet (Context context, android.app.FragmentManager fm,
android.app.Fragment parentHint) {
RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null ) {
Glide glide = Glide.get(context);
requestManager =
factory.build(glide, current.getLifecycle(), current.getRequestManagerTreeNode());
current.setRequestManager(requestManager);
}
return requestManager;
}
@TargetApi (Build.VERSION_CODES.JELLY_BEAN_MR1)
RequestManagerFragment getRequestManagerFragment (
final android.app.FragmentManager fm, android.app.Fragment parentHint) {
RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);
if (current == null ) {
current = pendingRequestManagerFragments.get(fm);
if (current == null ) {
current = new RequestManagerFragment();
current.setParentFragmentHint(parentHint);
pendingRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
handler.obtainMessage(ID_REMOVE_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}
可以看到根据Context的不同类型get
方法也有许多重载,比较特殊的情况是return getApplicationManager(context)
,当你在子线程调用with方法或者传入的Context是Application
,那么绑定的生命周期就是Application级别的,至于其他情况这里只截取了参数为Activity
的get
方法,最终RequestManager
由fragmentGet
方法返回,在这个方法中通过getRequestManagerFragment
方法创建并插入了一个RequestManagerFragment
到当前页面中,注意创建时会先判断当前页面是否已经存在该Fragment,不存在时才会去new这个Fragment,该Fragment
中没有添加View,是一个不可见的Fragment,接着进入它的源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class RequestManagerFragment extends Fragment {
private final ActivityFragmentLifecycle lifecycle;
@Override
public void onStart () {
super .onStart();
lifecycle.onStart();
}
@Override
public void onStop () {
super .onStop();
lifecycle.onStop();
}
@Override
public void onDestroy () {
super .onDestroy();
lifecycle.onDestroy();
}
}
在这个类中所有的生命周期事件都绑定到了ActivityFragmentLifecycle
中的对应方法上
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
class ActivityFragmentLifecycle implements Lifecycle {
private final Set<LifecycleListener> lifecycleListeners =
Collections.newSetFromMap(new WeakHashMap<LifecycleListener, Boolean>());
void onStart () {
isStarted = true ;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStart();
}
}
void onStop () {
isStarted = false ;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onStop();
}
}
void onDestroy () {
isDestroyed = true ;
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onDestroy();
}
}
}
而ActivityFragmentLifecycle
则把生命周期时间通知给它所持有的所有LifecycleListener
实现类,这里的实现类正是public class RequestManager implements LifecycleListener
,这里是不是看到了观察者模式的影子?当前页面中不可见的RequestManagerFragment
做为被观察者负责提供生命周期事件,当前页面下所有的图片请求管理器RequestManager
做为观察者响应事件,处理逻辑,至此整个绑定流程就打通了。
此外在Glide的manager包下还有一个网络状态管理接口ConnectivityMonitor
也继承了LifecycleListener
,它的实现类DefaultConnectivityMonitor
会在onStart
的回调中注册监听网络状态的广播,在onStop
中取消注册,并且在网络状态变化时把消息回调给RequestManager
做相应处理,它作为RequestManager
的一个成员对象会和RequestManager
一起被注册到ActivityFragmentLifecycle
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class DefaultConnectivityMonitor implements ConnectivityMonitor {
private final BroadcastReceiver connectivityReceiver = new BroadcastReceiver() {
@Override
public void onReceive (Context context, Intent intent) {
boolean wasConnected = isConnected;
isConnected = isConnected(context);
if (wasConnected != isConnected) {
listener.onConnectivityChanged(isConnected);
}
}
};
@Override
public void onStart () {
register();
}
@Override
public void onStop () {
unregister();
}
}
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 class RequestManager implements LifecycleListener {
if (Util.isOnBackgroundThread()) {
mainHandler.post(addSelfToLifecycle);
} else {
lifecycle.addListener(this );
}
lifecycle.addListener(connectivityMonitor);
@Override
public void onStart () {
Util.assertMainThread();
requestTracker.resumeRequests();
targetTracker.onStart();
}
@Override
public void onStop () {
Util.assertMainThread();
requestTracker.pauseRequests();
targetTracker.onStop();
}
@Override
public void onDestroy () {
targetTracker.onDestroy();
for (Target<?> target : targetTracker.getAll()) {
clear(target);
}
targetTracker.clear();
requestTracker.clearRequests();
lifecycle.removeListener(this );
lifecycle.removeListener(connectivityMonitor);
mainHandler.removeCallbacks(addSelfToLifecycle);
glide.unregisterRequestManager(this );
}
}
总结:每个涉及Glide图片加载的页面都会插入一个不可见的RequestManagerFragment
负责提供生命周期事件,包含一个ActivityFragmentLifecycle(Lifecycle)
负责持有LifecycleListener
并把事件分发给他们,包含一个或多个RequestManager(LifecycleListener)
接收事件,并由RequestTracker
类和TargetTracker
类实际控制请求的停止或恢复。
缓存设计 在Glide中,资源的基本类型是Resource<Z>
,bitmap
, bytes
, drawable
, file
, gif
等都可以视为Resource
, 对于Resource
资源采用了三级缓存策略,而其中针对bitmap
又有专门的缓存池LruBitmapPool
和复用策略LruPoolStrategy
,下面针对这两种类型分别来做讨论:
0.Resource缓存设计 在Glide中负责资源实际加载与管理的类是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
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, Options options, boolean isMemoryCacheable, boolean useUnlimitedSourceExecutorPool, boolean onlyRetrieveFromCache, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
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 ;
}
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 ;
}
EngineJob<?> current = jobs.get(key);
if (current != null ) {
current.addCallback(cb);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Added to existing load" , startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob<R> engineJob = engineJobFactory.build(key, isMemoryCacheable,
useUnlimitedSourceExecutorPool);
DecodeJob<R> decodeJob = decodeJobFactory.build(glideContext, model, key, signature, width, height, resourceClass, transcodeClass, priority, diskCacheStrategy, transformations,
isTransformationRequired, onlyRetrieveFromCache, options, engineJob);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(decodeJob);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load" , startTime, key);
}
return new LoadStatus(cb, engineJob);
}
首先是调用loadFromCache
从内存缓存中加载资源,未命中则loadFromActiveResources
从使用中的资源中加载,仍未命中则创建一个EngineJob
1
2
3
4
5
6
7
8
9
10
public void start (DecodeJob<R> decodeJob) {
this .decodeJob = decodeJob;
GlideExecutor executor = decodeJob.willDecodeFromCache()
? diskCacheExecutor
: getActiveSourceExecutor();
executor.execute(decodeJob);
}
private GlideExecutor getActiveSourceExecutor () {
return useUnlimitedSourceGeneratorPool ? sourceUnlimitedExecutor : sourceExecutor;
}
在EngineJob
中判断是否尝试从磁盘缓存中读取,是的话则由diskCacheExecutor
线程池去负责执行加载任务,否则从数据源中加载,由sourceExecutor
负责,线程池实际执行的任务是实现了Runnable
接口的DecodeJob<R>
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}
DecodeJob
根据需要加载的类型是Cache
还是Source
创建不同的DataFetcherGenerator
。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class SourceGenerator implements DataFetcherGenerator ,
DataFetcher .DataCallback <Object >,
DataFetcherGenerator .FetcherReadyCallback {
public boolean startNext () {
loadData.fetcher.loadData(helper.getPriority(), this );
}
@Override
public void onDataReady (Object data) {
}
@Override
public void onLoadFailed (Exception e) {
}
}
DataFetcherGenerator
使用具有诸多实现类的DataFetcher
完成数据的获取,并在onDataReady
回调中接收结果,比如负责加载网络数据的Fetcher是HttpUrlFetcher
,至此完成加载。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
@Override
public void loadData (Priority priority, DataCallback<? super InputStream> callback) {
long startTime = LogTime.getLogTime();
final InputStream result;
try {
result = loadDataWithRedirects(glideUrl.toURL(), 0 , null ,
glideUrl.getHeaders());
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to load data for url" , e);
}
callback.onLoadFailed(e);
return ;
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime)
+ " ms and loaded " + result);
}
callback.onDataReady(result);
}
根据上面的加载过程可以总结出Resource的缓存设计:
首先尝试从MemoryCache(LruResourceCache)
中取资源,存储结构是LinkedHashMap<T, Y>
然后尝试从ActiveResources
中取资源,存储结构是Map<Key, WeakReference<EngineResource<?>>>
,这层相当于提供了一层更灵活的内存缓存,内存充足时缓存更多资源,紧张时则释放资源
接着尝试从DiskCache(DiskLruCache)
中取资源,存储结构是LinkedHashMap<String, Entry>
最后从Source
中取资源,可以是网络资源、本地原始资源等等
1. Bitmap缓存设计 为了实现对Bitmap
的高效管理,Glide专门设置了LruBitmapPool
用来缓存Bitmap,无论是原始资源解码出的Bitmap还是经过transform
变换处理后的Bitmap都会存储在其中。同时还设置了复用策略,避免Bitmap反复创建。缓存池+复用策略使得Glide在列表控件上性能表现的很出色。
LruBitmapPool
本身依旧是一个实现了Lru算法的容器,就不赘述了,复用策略LruPoolStrategy
在Glide中有三种实现:
AttributeStrategy
针对4.4以下版本的策略,当前图片必须和被复用图片大小一致
SizeStrategy
4.4以上版本的策略,当前图片比被复用图片小即可复用
SizeConfigStrategy
4.4以上版本的策略,当前图片比 被复用图片小且Config一致即可复用
以SizeConfigStrategy
为例,需要根据当前图片size的大小和Config去keyPool
中找出满足复用条件的Bitmap的key,然后去BitmapPool中取到待复用的Bitmap资源
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
private Key findBestKey (int size, Bitmap.Config config) {
Key result = keyPool.get(size, config);
for (Bitmap.Config possibleConfig : getInConfigs(config)) {
NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
if (possibleSize != size
|| (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
keyPool.offer(result);
result = keyPool.get(possibleSize, possibleConfig);
}
break ;
}
}
return result;
}
public Bitmap get (int width, int height, Bitmap.Config config) {
int size = Util.getBitmapByteSize(width, height, config);
Key bestKey = findBestKey(size, config);
Bitmap result = groupedMap.get(bestKey);
if (result != null ) {
decrementBitmapOfSize(bestKey.size, result);
result.reconfigure(width, height,
result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
}
return result;
}
把待复用Bitmap设置给当前Bitmap的options.inBitmap
,实现复用
1
options.inBitmap = bitmapPool.getDirty(width, height, options.inPreferredConfig);
完整加载过程 上面已经聊过生命周期绑定和缓存策略了,现在该走通整个加载流程了,从load(String).into(ImageView)
这里继续。
1
2
3
4
5
private RequestBuilder<TranscodeType> loadGeneric (@Nullable Object model) {
this .model = model;
isModelSet = true ;
return this ;
}
load
方法的内容很少,返回一个Build模式的Builder类,只是用来给Request
对象提供参数
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
public <Y extends Target<TranscodeType>> Y into (@NonNull Y target) {
Util.assertMainThread();
Preconditions.checkNotNull(target);
if (!isModelSet) {
throw new IllegalArgumentException("You must call #load() before calling #into()" );
}
Request previous = target.getRequest();
if (previous != null ) {
requestManager.clear(target);
}
requestOptions.lock();
Request request = buildRequest(target);
target.setRequest(request);
requestManager.track(target, request);
return target;
}
public Target<TranscodeType> into (ImageView view) {
Util.assertMainThread();
Preconditions.checkNotNull(view);
if (!requestOptions.isTransformationSet()
&& requestOptions.isTransformationAllowed()
&& view.getScaleType() != null ) {
if (requestOptions.isLocked()) {
requestOptions = requestOptions.clone();
}
switch (view.getScaleType()) {
case CENTER_CROP:
requestOptions.optionalCenterCrop();
break ;
case CENTER_INSIDE:
requestOptions.optionalCenterInside();
break ;
case FIT_CENTER:
case FIT_START:
case FIT_END:
requestOptions.optionalFitCenter();
break ;
case FIT_XY:
requestOptions.optionalCenterInside();
break ;
case CENTER:
case MATRIX:
default :
}
}
return into(context.buildImageViewTarget(view, transcodeClass));
}
public void setRequest (@Nullable Request request) {
setTag(request);
}
@Override
@Nullable
public Request getRequest () {
Object tag = getTag();
Request request = null ;
if (tag != null ) {
if (tag instanceof Request) {
request = (Request) tag;
} else {
throw new IllegalArgumentException(
"You must not call setTag() on a view Glide is targeting" );
}
}
return request;
}
into
有两个重载方法,其中一个会把ImageView
封装成为Target
并且根据scaleType
属性对图片做处理,之后清除对象上之前可能存在的旧的请求(View复用时会留有之前的Request),创建新的请求,并和Target互相绑定。Target的getRequest
和setRequest
都会操作View的Tag,把Request记录到Tag里,这也是Glide不允许我们直接使用setTag
的原因,最后由RequestManager
发起请求,执行Request的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
50
51
52
53
54
55
56
57
@Override
public void begin () {
stateVerifier.throwIfRecycled();
startTime = LogTime.getLogTime();
if (model == null ) {
if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
width = overrideWidth;
height = overrideHeight;
}
int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
onLoadFailed(new GlideException("Received null model" ), logLevel);
return ;
}
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 (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
public void onSizeReady (int width, int height) {
@Override
public void onSizeReady (int width, int height) {
stateVerifier.throwIfRecycled();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
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 (Log.isLoggable(TAG, Log.VERBOSE)) {
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.getOptions(), requestOptions.isMemoryCacheable(), requestOptions.getUseUnlimitedSourceGeneratorsPool(), requestOptions.getOnlyRetrieveFromCache(), this );
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
}
}
begin
方法中确定Size的大小并产生onSizeReady
回调,同时设置PlaceholderDrawable
占位图,在onSizeReady
中设置状态为Status.RUNNING
并调用engine.load
,至此就和前面的内容对接上了,在Resource
缓存策略中已经介绍了Engine
类的加载流程。Engine
是图片加载的管理中心,暴露了一系列方法供Request
操作。根据之前已经分析过的内容,在DecodeJob
里最终会拿到我们需要的Resource<Z>
。
最后 纵观下来文章已经挺长了,但是目前也只是把Glide中最主体的加载过程过了一遍,还有很多细节处理没提到。Glide中运用了大量的设计模式,使得整个框架扩展性很好,各模块解耦充分,职责粒度很细,同时还包含了很多对于图片类的处理方法,是很不错的学习资料呢╮( ̄▽ ̄)╭
参考文章 Glide核心设计一 Glide核心设计二 拆 Glide 系列之 - Bitmap 复用 Glide源码详解
声明:本站所有文章均为原创或翻译,遵循署名-非商业性使用-禁止演绎 4.0 国际许可协议 ,如需转载请确保您对该协议有足够了解,并附上作者名(Est)及原贴地址