封装并实现统一的图片加载架构

11,629 阅读10分钟

GitHub: 统一的图片加载架构

前言

对于图片加载框架,大家用到的可能是Glide,Picasso或者Fresco,这基本上是主流的图片加载框架,我们使用它的时候,大都感觉如臂使指,简直愉快的不要不要的。但是我们还是发现至少有两个问题,以Glide为例,第一,当需求变动,你需要对图片加载失败时的情景添加一个单独的占位符,这个时候你就不得不在每一个使用到Glide的地方去添加这样的设置;第二,当你需要对项目进行重构时,或者目前的图片加载框架无法实现某些需求,而需要替换的时候,你可能还是需要对原有项目大动干戈。

大家回顾自己手头上的代码,不知道是否都面临这样的隐患?反正当我看到我们团队的项目代码的时候,我的头总是比平时大两倍...你问我为啥?一堆历史遗留问题,比如最早就直接在项目中使用Glide,后来我建议说,至少稍微做点封装,毕竟吃相不能太难看,于是才做了一层封装,却依然经不起新需求的考验,更别提替换框架的程度了了(这可能就是为什么我们团队转向了RN,因为谁都不想看过去的代码了)。

如果你以为这是因为我是一个完美主义者,那么可能没有尝试过一行一行粘贴复制,删除重构的日子。

废话讲完,我们正是开始吧

封装的新使命

我们先聊聊封装,封装的好处大家都很熟悉,对外提供简单接口屏蔽内部复杂,保护数据,保证安全....等,大家可能基本上都倒背如流了, 如今我们在开发Android项目的时候封装的主要目的却不再是这些了,为什么,因为我们所有诸如okhttp,retrofit,Glide,等等框架本身就实现了完美的封装,并达成了对外提供简单接口屏蔽内部复杂,保护数据,保证安全等目的,如果仅仅是为了这些目的,我们大可不必在做封装。

那么我们封装的新的使命是什么呢,是为了达成对模块的控制,什么意思呢?还是以图片加载框架为例,假如你直接在业务代码中使用了Glide,Picasso或者Fresco的话,也就意味着,你把图片加载的控制权完全交给了他们,后面你想对图片加载流程做任何改动,你都需要一个一个去修改,那么你就丧失了对图片加载模块的控制权。所以,我所说的对于模块的控制,是你随时能够以很小的代价修改甚至替换整个模块。

这也是为什么现在各种发开框架已经把自己封装的如此之好的情况下,我们依然需要对它做封装的原因。

好了,接下来,我们就分析具体问题。


从封装Glide开始

以Glide为例,Glid通过链式调用,可以随意的调用各种图片加载相关的设定,如缓存策略,动画,占位符等等,各类api数不胜数,而我们现在先要把这些调用抽象成一个接口,进而就能轻松实现对它的封装。

一个简单的Glide的调用可能是这样的:

        Glide.with(getContext())
                .load(url)
                .skipMemoryCache(true)
                .placeholder(drawable)
                .centerCrop()
                .animate(animator)
                .into(img);

尽管没有使用Glide所有的图片加载相关的设置,但是大家应该能感受到,它的图片加载设置选项十分丰富,也很随意,那么我们究竟应该如何把它封装到一个接口里面去呢?可能你首先想到是这种:

public interface ImageLoader{
    static void showImage(ImageView v, Context context,String url, boolean skipMemoryCache,int placeholder,ViewPropertyAnimation.Animator animator)
}

这显然是很有问题的,对于一个有很多可选项的接口做封装,既要保留丰富的可选项,还要保证统一而简介的调用。这么一长串参数显然有伤大雅。

那么应该如何设计呢?我们可以从这个角度来分析,对于图片加载而言,什么是最基本最重要的必选项,什么是可有可无的可选项:

  • 必选项:url(图片来源),ImageView(图片容器),上下文环境(Context)
  • 可选项:除此必选项之外的所有

那么我们的接口初具雏形了

public interface ImageLoader{
    void showImage(ImageView imageview, String url, Context context,ImageLoaderOptions options);
    void showImage(ImageView imageview,int drawable,Context context,ImageLoaderOptions options);
}

这样是不是就好了呢?也不是,我们还可以在继续探索,
我们发现ImageView内部其实包含了Context这个参数,完全可以省略,所以我们的基本参数应该是:url,ImageView,options,

public interface ImageLoader{
    void showImage(ImageView imageview,  String url, ImageLoaderOptions options);
    void showImage(ImageView imageview, int drawable,ImageLoaderOptions options);
}

然后我们再来看看方法中定义的ImageLoaderOptions,这个其实比较简单,基本上Glide有多少可选项,你就可以往里面加多少属性。由于这些属性都是可选择的,因此我们需要使用Builder模式来构建它,具体就不赘述了。

那么,到这里,我们对于Glide的封装的设计就基本完成了。


统一的图片加载架构

我们说了想要打造一个统一的图片加载框架,也就是说,不管Glide,还是Fresco,或者Picasso都能在这套架构下愉快的玩耍。其实我们只要在封装Glide的基础上进一步的做出改进即可,因为当我们封装Glide的时候,就已经是对图片加载的抽象了。

我们首先来看,之前抽象的接口总体上在其他的图片加载框架中都是可用的,不过由于Fresco的特殊设计,自己实现了图片容器,导致了一点问题,但是这也很简单,我们在接口里面用View作为图片容器即可。

public interface ImageLoader{
    void showImage(View v,  String url, ImageLoaderOptions options);
    void showImage(View v, int drawable,ImageLoaderOptions options);
}

好了,上面这个接口基本上可以完美兼容Glide,Picasso,Fresco这三种加载库,现在的问题是如何实现他们的可替换。这个时候我们就需要一种设计模式(策略模式迫不及待的跳出来说,选我选我!)

没错,就是策略模式,它的设计图如下:


(图片画的不好,大家多多包含)

至此,我们在设计上已经完成了一个统一的图片加载架构的设计,但是有一个问题我特意留到了最后,就是ImageLoaderOptions的内部的构造。

当我们只需要封装一个Glide的时候,ImageLoaderOptions可以和Glide中的那些设置项完全匹配,只要你愿意,你可以把Glide里面的所有图片加载的相关的设置项都放进去。但是,如果我们要兼容三个加载框架甚至更多的时候,还能这样做么?

理论上是可以的,不过当你这么干了,那么ImageLoaderOptions内部可能是可能是这样的:

public class ImageLoaderOptions {
    //Glide的设置项
    private int placeHolder=-1; //当没有成功加载的时候显示的图片
    private ImageReSize size=null; //重新设定容器宽高
    private int errorDrawable=-1;  //加载错误的时候显示的drawable
    private boolean isCrossFade=false; //是否渐变平滑的显示图片
    private  boolean isSkipMemoryCache = false; //是否跳过内存缓存
    private   ViewPropertyAnimation.Animator animator = null; // 图片加载动画
    ...
    ...
     //Fresco的设置项
    private int placeHolder=-1; //当没有成功加载的时候显示的图片
    private Drawable  pressedStateOverlay =null;  //按下时显示的图层
    private boolean isCrossFade=false; //是否渐变平滑的显示图片

    ...
    ...
}

大家很容易发现,其实各个图片加载框架之间的设置项很多功能都是重叠的,比如占位符,渐进加载,缓存等等,也有一些设置项是类似的,因此实际上我们应该把他们合并在一起,也就是说,当我们思考对于ImageLoaderOptions的设计的时候,我们应该首先把几个框架共同和相似的设置项合并,因为这代表着图片加载领域最普遍最重要的需求。其次我们再按需加入自己需要的各个框架之间有差异的设置项。

下面是我对于这个统一图片加载架构的具体实现,大家可以仅作参考。

接口定义

public interface ImageLoaderStrategy{
    void showImage(View v,  String url, ImageLoaderOptions options);
    void showImage(View v, int drawable,ImageLoaderOptions options);
}

设置项定义

public class ImageLoaderOptions {
    //你可以把三个图片加载框架所有的共同或相似设置项搬过来,现在仅仅用以下几种作为范例演示。
    private int placeHolder=-1; //当没有成功加载的时候显示的图片
    private ImageReSize size=null; //重新设定容器宽高
    private int errorDrawable=-1;  //加载错误的时候显示的drawable
    private boolean isCrossFade=false; //是否渐变平滑的显示图片
    private  boolean isSkipMemoryCache = false; //是否跳过内存缓存
    private   ViewPropertyAnimation.Animator animator = null; // 图片加载动画


    private ImageLoaderOptions(ImageReSize resize, int placeHolder, int errorDrawable, boolean isCrossFade, boolean isSkipMemoryCache, ViewPropertyAnimation.Animator animator){
        this.placeHolder=placeHolder;
        this.size=resize;
        this.errorDrawable=errorDrawable;
        this.isCrossFade=isCrossFade;
        this.isSkipMemoryCache=isSkipMemoryCache;
        this.animator=animator;
    }
    public class ImageReSize{
        int reWidth=0;
        int reHeight=0;
        public ImageReSize(int reWidth,int reHeight){
            if (reHeight<=0){
                reHeight=0;
            }
            if (reWidth<=0) {
                reWidth=0;
            }
            this.reHeight=reHeight;
            this.reWidth=reWidth;

        }

    }
 public static final  class Builder {
        private int placeHolder=-1; 
        private ImageReSize size=null;
        private int errorDrawable=-1;
        private boolean isCrossFade =false;
        private  boolean isSkipMemoryCache = false;
        private   ViewPropertyAnimation.Animator animator = null;
        public Builder (){

        }
        public Builder placeHolder(int drawable){
            this.placeHolder=drawable;
            return  this;
        }

        public Builder reSize(ImageReSize size){
            this.size=size;
            return  this;
        }

        public Builder anmiator(ViewPropertyAnimation.Animator animator){
            this.animator=animator;
            return  this;
        }
        public Builder errorDrawable(int errorDrawable){
            this.errorDrawable=errorDrawable;
            return  this;
        }
        public Builder isCrossFade(boolean isCrossFade){
            this.isCrossFade=isCrossFade;
            return  this;
        }
        public Builder isSkipMemoryCache(boolean isSkipMemoryCache){
            this.isSkipMemoryCache=isSkipMemoryCache;
            return  this;
        }

        public ImageLoaderOptions build(){

            return new ImageLoaderOptions(this.size,this.placeHolder,this.errorDrawable,this.isCrossFade,this.isSkipMemoryCache,this.animator);
        }
    }

下面以Glide实现该接口的方式:

public class GlideImageLoaderStrategy implements ImageLoaderStrategy {

    @Override
    public void showImage(View v, String url, ImageLoaderOptions options) {
        if (v instanceof ImageView) {
            //将类型转换为ImageView
            ImageView imageView= (ImageView) v;
            //装配基本的参数
            DrawableTypeRequest dtr = Glide.with(imageView.getContext()).load(url);
            //装配附加参数
            loadOptions(dtr, options).into(imageView);
        }
    }

    @Override
    public void showImage(View v, int drawable, ImageLoaderOptions options) {
        if (v instanceof ImageView) {
            ImageView imageView= (ImageView) v;
            DrawableTypeRequest dtr = Glide.with(imageView.getContext()).load(drawable);
            loadOptions(dtr, options).into(imageView);
        }
    }
    //这个方法用来装载由外部设置的参数
    private DrawableTypeRequest loadOptions(DrawableTypeRequest dtr,ImageLoaderOptions options){
        if (options==null) {
            return dtr;
        }
        if (options.getPlaceHolder()!=-1) {
            dtr.placeholder(options.getPlaceHolder());
        }
        if (options.getErrorDrawable()!=-1){
            dtr.error(options.getErrorDrawable());
        }
        if (options.isCrossFade()) {
            dtr.crossFade();
        }
        if (options.isSkipMemoryCache()){
            dtr.skipMemoryCache(options.isSkipMemoryCache());
        }
        if (options.getAnimator()!=null) {
            dtr.animate(options.getAnimator());
        }
        if (options.getSize()!=null) {
            dtr.override(options.getSize().reWidth,options.getSize().reHeight);
        }
        return dtr;
    }

}

Picsso,Fresco的接口实现类依照Glide。

下面就是最后一步,实现整个图片加载架构的管理类,用于对外提供图片加载服务和图片加载框架的替换

public class ImageLoaderStrategyManager implements ImageLoaderStrategy {
    private static final ImageLoaderStrategyManager INSTANCE = new ImageLoaderStrategyManager();
    private ImageLoaderStrategy imageLoader;
    private ImageLoaderStrategyManager(){
        //默认使用Glide
        imageLoader=new GlideImageLoaderStrategy();
    }
    public static ImageLoaderStrategyManager getInstance(){
        return INSTANCE;
    }
    //可实时替换图片加载框架
  public void setImageLoader(ImageLoaderStrategy loader) {
      if (loader != null) {
          imageLoader=loader;
      }
   }

    @Override
    public void showImage(@NonNull View mView, @NonNull String mUrl, @Nullable ImageLoaderOptions options) {

        imageLoader.showImage(mView,mUrl,options);
    }


    @Override
    public void showImage(@NonNull  View mView, @NonNull int mDraeable, @Nullable ImageLoaderOptions options) {
        imageLoader.showImage(mView,mDraeable,options);
    }

}

至此,整个图片加载架构都已经设计完毕了,我们也可以基本实现了对图片加载模块的控制。

这个小的图片加载架构是不是已经很完美了呢?其实也不是,由于Fresco的特殊,当我们切换到Fresco,或者从Fresco切换到其他加载框架的时候,我们可能仍然需要到处去修改xml文件的图片容器节点(ImageView/DraweeView),因为Fresco使用的时自家的组件。不过我也考虑过一种解决方案,那就是把图片容器(ImageView/DraweeView)节点放在一个单独的xml文件中,使用merge的方式添加到布局文件中,并在代码层面使统一用View 来获取图片容器(ImageView/DraweeView)的实例做相应操作。


项目已经上传了github,点此获取,求star !

后记

假如你实在理解不了控制力这个概念,也可以理解为打造高内聚低耦合的模块