一步步剖析 Glide 的设计与实现

前言

Glide 源码解析已经有大神完成了,因此这篇文章不会贴一大段一大段的源码解析,而是尝试从需求出发,一步一步地分析 Glide,看看为什么是这么设计的,这么设计有什么好处。

假如现在要由我们来设计一个图片加载框架,名字叫 Glide,那么应该怎么设计呢?

首先分析一下需求,图片加载框架的功能点有:

  1. 获取图像数据
  2. 显示图片到指定的 View 上
  3. 图片变换效果
  4. 数据缓存
  5. 自定义内容

好,现在初步确定了功能需求,下面就正式进入开发阶段了。

图片加载流程

Request

首先,我们定义加载图片并显示到 View 上的过程为一个请求。对于一个请求,应该有开始执行、取消执行、释放资源、状态查询等接口,同时,为了避免同一个请求重复执行,还应该有一个可以和其它请求比较的方法,因此可以创建 Request 接口如下:

public interface Request {
    /**
     * 开始执行
     */
    void begin();

    /**
     * 取消执行
     */
    void clear();

    // 状态查询
    boolean isRunning();
    boolean isComplete();
    boolean isResourceSet();
    boolean isCleared();
    boolean isFailed();

    /**
     * 释放资源
     */
    void recycle();

    /**
     * 比较 Request 是否等价
     */
    boolean isEquivalentTo(Request other);
}
复制代码

为什么 Request 是一个接口,而不是一个类呢?因为考虑到缩略图、出现错误时的替代图片等需求,同一次加载过程中可能包含多张图片、多个请求。

因此,对于单张图片,可以定义实现类 SingleRequest

为了解决同一次请求中的多张图片之间的协调问题,比如全图和缩略图,可以定义类 ThumbnailRequestCoordinator,它拥有成员 full 和 thumb,全图没有加载完成时,同时加载全图和缩略图,缩略图加载完成后显示缩略图,全图加载完成后取消缩略图,显示全图。

出现错误时,原始图片和替代图片之间的协调问题,可以定义类 ErrorRequestCoordinator 解决,它拥有成员 primary 和 error,一开始只会加载原始图片,原始图片加载失败之后,才开始加载替代图片。

假设同一次加载过程的图片数量是不固定的,比如缩略图本身可能又包含它自己的缩略图以及在出现错误时的替代图片,因此,ThumbnailRequestCoordinator 和 ErrorRequestCoordinator 都需要成员 parent,以链表的形式将这些请求组织起来,协调多张图片之间的显示行为。同时,它们都有共同的行为,比如加载是否成功、是否可以显示图片、是否可以显示占位图、加载成功后怎么处理、失败后怎么处理等,因此,定义接口 RequestCoordinator

public interface RequestCoordinator {

    /**
     * 是否可以显示图片
     */
    boolean canSetImage(Request request);

    /**
     * 是否可以显示占位图
     */
    boolean canNotifyStatusChanged(Request request);

    /**
     * 是否可以清除资源
     */
    boolean canNotifyCleared(Request request);

    /**
     * 是否已加载完成,并设置了图片资源
     */
    boolean isAnyResourceSet();

    /**
     * 加载完成后的回调
     */
    void onRequestSuccess(Request request);

    /**
     * 加载失败后的回调
     */
    void onRequestFailed(Request request);
}
复制代码

ThumbnailRequestCoordinator 和 ErrorRequestCoordinator 的定义如下:

public class ThumbnailRequestCoordinator implements RequestCoordinator, Request {
    @Nullable
    private final RequestCoordinator parent;
    private Request full;
    private Request thumb;
	...
}
复制代码
public final class ErrorRequestCoordinator implements RequestCoordinator, Request {
    @Nullable
    private final RequestCoordinator parent;
    private Request primary;
    private Request error;
	...
}
复制代码

Request、RequestCoordinator 的关系示意图如下:

好,完成了对请求的定义,下面就可以尝试构造 Request 了。因为 Request 相关的选项很多,因此构造 Request 的行为我们使用建造者模式,新建一个 RequestBuilder 来提供构造 Request 的功能。而考虑到 placeholder、图片变换、是否缓存等各种需求,我们又另外创建一个类 RequestOptions,统一提供相关的选项给用户设置。

除了提供各种选项、缩略图、替代图的设置等接口之外,RequestBuilder 还应该提供两个核心的接口,即 load 和 into。load 方法用于接收 url、String、File 等资源类型,into 用于开始执行请求,并在请求完成之后显示图片到指定的 View 上面。

RequestManager

开始执行请求之前,我们需要考虑一件事情:假如在加载图片的过程中,当前 Activity 退出了后台,或者该图片需要从网络中获取,但是网络断开了,怎么处理呢?

首先,我们需要监听 Activity 的生命周期,具体实现时,只需要监听 onStart、onStop、onDestroy 三个方法即可。基于这个考虑,定义接口 LifecycleLifecycleListener

public interface LifecycleListener {
    void onStart();
    void onStop();
    void onDestroy();
}
复制代码
public interface Lifecycle {
    void addListener(@NonNull LifecycleListener listener);
    void removeListener(@NonNull LifecycleListener listener);
}

复制代码

对于 Lifycycle,因为用户传入的 Context 类型可能为 Application,而 Application 没有 onStart 等生命周期方法。因此,对于 Application 类型的 Context,我们定义 ApplicationLifecycle,它只会在 addListener 的时候回调 LifecycleListener 的 onStart 方法;对于其它非 Application 类型的 Context,定义 ActivityFragmentLifecycle,维护一组 LifecycleListener,并提供方法 onStart、onStop、onDestroy,在这些方法被调用时回调 LifecycleListener 对应的方法。

那么问题来了,怎么监听 Activity 的生命周期呢?方法其实很简单,在 Activity 上面添加一个 Fragment 即可,这个 Fragment 是 RequestManagerFragment,它不显示 View, 只会在 onStart、onStop、onDestroy 方法回调时会执行 ActivityFragmentLifecycle 对应的方法,通知外部执行相应的操作。

这个"外部"是谁呢?很显然,应该是一个专门用于管理 Request 的类。因此,我们可以创建类 RequestManager,让它实现接口 LifecycleListener,在 onStop 回调时,暂停执行图片加载请求;onStart 回调时,继续执行;onDestroy 回调时,停止执行,并释放资源。

同时,使用广播监听网络状态,在网络可用时,重新开始执行之前因为网络问题被停止的请求。定义接口 ConnectivityMonitor:

public interface ConnectivityMonitor extends LifecycleListener {

    interface ConnectivityListener {
        void onConnectivityChanged(boolean isConnected);
    }
}
复制代码

另外,考虑到同一 Activity 中同时执行的请求可能有很多个,因此创建 RequestTracker 用于维护这些请求:

public class RequestTracker {
    private final Set<Request> requests = Collections.newSetFromMap(new WeakHashMap<Request, Boolean>());
    private final List<Request> pendingRequests = new ArrayList<>();
	
    public void pauseRequests() { ... }
    public void resumeRequests() { ... }
    public void clearRequests() { ... }
    public void restartRequests() { ... }	
}
复制代码

辅助类写好之后,下面就可以给出 RequestManager 的实现了(为了简单起见,下面的代码有经过一些省略及改动):

public class RequestManager implements LifecycleListener, ... {
	
	...
    @Override
    public void onStart() {
        requestTracker.resumeRequests(); // 继续执行
    }
	
    @Override
    public void onStop() {
        requestTracker.pauseRequests(); // 暂停执行
    }
	
    @Override
    public void onDestroy() {
        requestTracker.clearRequests(); // 取消执行
    }

    private static class RequestManagerConnectivityListener implements ConnectivityMonitor.ConnectivityListener {
        private final RequestTracker requestTracker;

        RequestManagerConnectivityListener(@NonNull RequestTracker requestTracker) {
            this.requestTracker = requestTracker;
        }

        @Override
        public void onConnectivityChanged(boolean isConnected) {
            if (isConnected) {
                requestTracker.restartRequests(); // 重新开始执行
            }
        }
    }
	
}
复制代码

如此,就完成了对请求的管理,示意图如下:

Resource

先不考虑具体的加载过程,假设图片数据加载成功了,那么我们需要思考:图片数据用什么来表示呢?因为从本地文件、从网络中获取到的数据,可能是 Bitmap、Drawable 等可以直接显示的类型,也可能是 byte[]、File 等需要转换的类型。因此我们需要有一个接口,以便在成功获取到数据之后,统一对所有数据类型进行处理。这个接口命名为 Resource,它的作用是对图像数据进行包装,并提供统一的对外接口

/**
 * @param Z 指图像的类型
 */
public interface Resource<Z> {

    /**
     * 返回图像的类型信息
     */
    @NonNull
    Class<Z> getResourceClass();

    /**
     * 获取该图像
     */
    @NonNull
    Z get();

    /**
     * 获取该图像占用内存的大小
     */
    int getSize();

    /**
     * 回收资源
     */
    void recycle();
}
复制代码

因此,我们可以有 BitmapResource、DrawableResource、FileResource、BytesResource、GifDrawableResource 等实现类。

但这样又会有一个新的问题,怎么将获取到的图像数据转化为对应的 Resource 对象呢?为了解决这个问题,可以定义一个新的接口 ResourceDecoder,它只需要 handles、decode 两个方法,handles 用于返回当前 ResourceDecoder 是否可以处理该数据类型,如果可以,则执行 decode 方法,将该数据类型转化为对应的 Resource 类型:

/**
 * @param <T> 图像的数据类型
 * @param <Z> 图像数据类型对应的 Resource 类型
 */
public interface ResourceDecoder<T, Z> {

    boolean handles(@NonNull T source, @NonNull Options options) throws IOException;

    @Nullable
    Resource<Z> decode(@NonNull T source, int width, int height, @NonNull Options options)
            throws IOException;
}
复制代码

因此, 我们可以有 FileDecoder、BitmapDrawableDecoder、ByteBufferBitmapDecoder 等实现类。

Engine

准备工作完成之后,现在终于可以加载图像数据了,因为这一步是 Glide 的关键步骤之一,涉及的点比较多,例如内存缓存、本地缓存、网络请求等等,因此我们把这个功能独立出来,Request 直接调用即可,否则 Request 会非常臃肿。这个类起名为 Engine,顾名思义,它是我们的发动机,即动力(图像)的来源。

为了优化性能,在 Engine 加载图像之前,我们应该考虑两件事:

  1. 请求是否重复?
  2. 内存缓存中是否有对应的数据?

对于第一个问题,首先,我们可以用一个 Key 标志相同的请求,因此,创建类 EngineKey。另外,假如前一个相同的请求已记载完成,并且对应的 Resource 引用还在,那么我们直接返回该 Resource 即可。但这又引出了一个新的问题,如果所有地方对该 Resource 的引用都释放了,此时我们应该移除该 Resource 对象,并使用内存缓存 Resource 的图像数据,那么我们怎么知道对该 Resource 的所有引用都被释放了呢?为了解决这个问题,我们创建一个新的 Resource 实现,即 EngineResource,它用于包装另一个 Resource 对象,并使用引用计数,在计数为 0 时回调对应的接口通知 Engine 执行资源释放、内存缓存等操作

class EngineResource<Z> implements Resource<Z> {
    private final Resource<Z> resource;

    interface ResourceListener {
        void onResourceReleased(Key key, EngineResource<?> resource);
    }	
}
复制代码

同时,创建 ActiveResources 类,用于保存所有计数大于 0 的 Resource 对象:

final class ActiveResources {
    final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
}
复制代码

但是,假如前一个相同的请求尚未加载完成,还在执行中,此时我们应该使用一个新的类标志该工作,因此,创建 EngineJob,同时,使用一个集合保存所有正在执行中的工作:

final class Jobs {
    private final Map<Key, EngineJob<?>> jobs = new HashMap<>();
}
复制代码

假如记载图像之前,发现 Jobs 已经有对应的工作了,那么直接返回即可,不需要重复执行。

第一个问题解决了,接着看第二个问题。假如 ActiveResources、Jobs 都没有获取到对应的记录,那么我们应该尝试去内存缓存中获取图像数据,并使用 Resource 包装该图像数据返回给调用方。因此这一步涉及到缓存,因此具体过程先跳过,后面再讲。

通过检查 ActiveResources、 Jobs、内存缓存都没有找到相同的记录,那么就是时候执行 EngineJob 了,因为这个过程是异步的,因此我们需要有一个接口监听结果,因此,创建 EngineJobListener

interface EngineJobListener {

    void onEngineJobComplete(EngineJob<?> engineJob, Key key, EngineResource<?> resource);

    void onEngineJobCancelled(EngineJob<?> engineJob, Key key);
}
复制代码

前期的检查工作完成之后,现在就可以去获取图像数据了。此时,我们有两个选择:

  1. 从本地缓存获取
  2. 从数据源获取

其中,本地缓存又分为两种:

  1. 经过变换的图像数据
  2. 原始的图像数据

因此,我们总共有三个选择,按优先级排列是:

  1. 本地缓存的经过变换的图像数据
  2. 本地缓存的原始的图像数据
  3. 从数据源获取

为了统一上述三种行为,并在成功获取到数据后执行后续的操作,我们定义接口:

interface DataFetcherGenerator {

    interface FetcherReadyCallback {

        /**
         * 重新开始执行 startNext
         */
        void reschedule();

        /**
         * 数据加载完成
         */
        void onDataFetcherReady(Key sourceKey, @Nullable Object data, DataFetcher<?> fetcher,
                                DataSource dataSource, Key attemptedKey);

        /**
         * 数据加载失败
         */
        void onDataFetcherFailed(Key attemptedKey, Exception e, DataFetcher<?> fetcher,
                                 DataSource dataSource);
    }

    // 尝试获取数据,如果当前实现无法获取,则交给下一个
    boolean startNext();

    // 取消
    void cancel();
}
复制代码

因此,我们可以有 ResourceCacheGenerator、DataCacheGenerator、SourceGenerator 三个实现类。

先不管缓存的实现,直接看 SourceGenerator,因为用户传入的资源参数类型有很多个,比如 Uri、File、String 等,对于每一种资源类型,我们应该至少有一种获取数据的方法,因此我们定义接口 DataFetcher 以统一数据获取的行为:

/**
 * @param <T> 准备获取的数据类型
 */
public interface DataFetcher<T> {

    interface DataCallback<T> {
        // 数据获取成功
        void onDataReady(@Nullable T data);
        // 数据获取失败
        void onLoadFailed(@NonNull Exception e);
    }

    // 加载图像数据
    void loadData(@NonNull Priority priority, @NonNull DataCallback<? super T> callback);
    // 释放资源
    void cleanup();
    // 取消加载
    void cancel();
    // 获取数据类型
    Class<T> getDataClass();
	...
}
复制代码

DataFetcher 的实现类应该包括 AssetPathFetcher、ByteBufferFetcher、FileFetcher、HttpUrlFetcher 等等,以对应不同的资源类型。同时,对于网络请求,除了通过 HttpURLConnection获取之外,因为 OkHttp 等第三方库应用非常广泛,我们还应该提供通过第三方库获取数据的接口,因此有实现类 OkHttpStreamFetcher、VolleyStreamFetcher 等。

但是我们怎么知道应该使用哪个 DataFetcher 呢?为了解决这个问题,可以定义工厂接口 ModelLoader,和 ResourceDecoder 一样,提供一个 handles 方法:

/**
 * @param <Model> 传入的资源类型,比如 String、File、Uri 等
 * @param <Data>  成功获取数据后,可提供给 ResourceDecoder 使用的数据类型
 */
public interface ModelLoader<Model, Data> {

	class LoadData<Data> {
        public final Key sourceKey;
        public final List<Key> alternateKeys;
        public final DataFetcher<Data> fetcher;

        public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {
            this(sourceKey, Collections.<Key>emptyList(), fetcher);
        }

        public LoadData(@NonNull Key sourceKey, @NonNull List<Key> alternateKeys,
                        @NonNull DataFetcher<Data> fetcher) {
            this.sourceKey = Preconditions.checkNotNull(sourceKey);
            this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
            this.fetcher = Preconditions.checkNotNull(fetcher);
        }
    }

    // 构建 LoadData
    @Nullable
    LoadData<Data> buildLoadData(@NonNull Model model, int width, int height,
                                 @NonNull Options options);

    // 是否可以处理该数据类型
    boolean handles(@NonNull Model model);
}
复制代码

这样,无论用户传入什么类型的资源,我们都可以遍历 ModelLoader,找到能够处理该类型的 DataFetcher,并执行 loadData 方法即可,数据获取成功之后,就会回调 DataCallback、FetcherReadyCallback、EngineJobListener 等接口,将数据回调给调用方。

Target

通过 DataFetcher 获取到数据,并使用 ResourceDecoder 将图像数据转化为 Resource 对象之后,我们就可以把它显示到 View 上面了。一般情况下,这个 View 会是一个 ImageView,但也可能是其它类型的 View。因此,我们定义一个统一的接口 Target,这个 Target 应该具备什么行为呢?首先,它应该监听结果的回调、数据加载的过程;接着,它要测量自己的宽高,并回调给外部;最后,它应该继承 LifecycleListener,为什么呢?因为对于 GIF 图而言,如果 Activity 的 onStop 执行了,它需要停止动画,以节省资源,并避免出现问题。因此,Target 接口定义如下:

public interface Target<R> extends LifecycleListener {

    /**
     * 开始加载
     */
    void onLoadStarted(@Nullable Drawable placeholder);

    /**
     * 加载失败
     */
    void onLoadFailed(@Nullable Drawable errorDrawable);

    /**
     * 加载完成
     */
    void onResourceReady(@NonNull R resource, @Nullable Transition<? super R> transition);

    /**
     * 取消加载
     */
    void onLoadCleared(@Nullable Drawable placeholder);

    /**
     * 返回尺寸
     */
    void getSize(@NonNull SizeReadyCallback cb);

    /**
     * 移除回调
     */
    void removeCallback(@NonNull SizeReadyCallback cb);

    /**
     * 设置对应的 Request
     */
    void setRequest(@Nullable Request request);

    /**
     * 获取对应的 Request
     */
    @Nullable
    Request getRequest();
}
复制代码

对于 Target,我们有实现类 ImageViewTarget、AppWidgetTarget、FutureTarget 等。

小结

也许你已经发现了,Glide 的设计是很符合针对接口编程这一原则的,阅读源码时,最重要的也是上述几个接口,具体实现反而不用太在意。

图片加载的流程图如下所示:

缓存

下面简单说一下 Glide 的缓存机制。Glide 的缓存机制主要包括数组对象缓存池、Bitmap 缓存池、内存缓存、本地缓存等,分别对应 ArrayPool、BitmapPool、MemoryCache、DiskCache 几个接口,主要的实现类分别是 LruArrayPool、LruBitmapPool、LruResourceCache、DiskLruCacheWrapper,即 Glide 的缓存行为基本都是通过 LRU 算法实现的,实现原理基本都是:每 put 一个元素就检查当前已缓存的资源所占用的内存大小是否大于限定值,如果是,则移除最近最少使用的资源,直到符合要求。

限于篇幅,这里就不展开讲了,说一下缓存相关的值得注意的几个接口或类。

  1. MemorySizeCalculator。用于计算分配给各个缓存池的内存大小,以内存足够的 Android 8.0 及以上的设备为例,ArrayPool 固定 4MB,BitmapPool 占用一个手机屏幕的像素大小(例如 1920 * 1080 分辨率的手机约等于 8MB),MemoryCache 占用 2 个手机屏幕的像素大小(例如 1920 * 1080 分辨率的手机约等于 16MB)。

  2. BitmapPool。Android API 11 之后可以重用 Bitmap,但需要 width、height 等属性都符合要求才可以;而 Android API 19 之后,只要复用的 Bitmap 是可变的,且内存占用大于等于目标 Bitmap 即可。因此 BitmapPool 缓存 Bitmap 时,会根据设备的 API 选择 SizeConfigStrategy 或 AttributeStrategy 两种策略。

  3. DiskCache。本地缓存接口,主要实现类为 DiskLruCacheWrapper,是 DiskLruCache 的封装类,默认最大缓存 250MB 的数据,默认缓存文件夹默认为 /data/data/< package name>/cache/image_manager_disk_cache。

  4. Encoder。用于编写图像数据到本地文件中,是实现 DiskCache 的主要辅助类,不同的图像类型对应不同的 Encoder,例如 BitmapEncoder,是使用 Bitmap 的 compress 方法写入到 FileOutputStream 实现的。

  5. EncodeStrategy。本地缓存行为,共有 SOURCE、TRANSFORMED、NONE 三种,即缓存原始图像数据、缓存经过变换的图像数据、不缓存。

其它

最后再简单说一下其它几个值得注意的接口和类:

  1. Transition。用于在更换图片时实现渐变等动画效果。

  2. Transformation。用于实现图片的变换效果,包括 CenterCrop、FitCenter、RoundedCorners 等。

  3. ResourceTranscoder。用于将一种 Resource 类型转换为另一种 Resource 类型,比如 BitmapBytesTranscoder、BitmapDrawableTranscoder、GifDrawableBytesTranscoder 等。

  4. GlideExecutor。用于获取在加载图片、缓存图片、加载动画帧等情况下使用的线程池。

  5. Registry。用于注册各种 ResourceDecoder、Encoder、ResourceTranscoder、ModelLoader 等等,在需要使用上述几个接口的实现类时,可以方便地通过 Registry 来获取。

  6. AppGlideModule。用于指定 Glide 的自定义行为。比如可以通过 GlideBuilder 设置 自己的 ArrayPool、BitmapPool、MemoryCache、默认 RequestOptions 等,还可以通过 Registry 注册自己的 ResourceDecoder、ModelLoader,以自定义图像数据获取的行为。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值