打造自己的图片加载缓存库(Picasso OR Glide)

好久没写文章了,一个是工作的原因,还一个就是这几个月看了很多文章,一直在补充自己的知识。之前看到一句很喜欢的话——感到快乐就忙东忙西,感到累了就放空自己,这几个月一“快乐”了,就停不下来地看、一直在写代码。期间由于项目的需求,用这里那里学来的东西,写了一套RxJava+Retrofit+OkHttp的网络请求框架;重新整理了项目中下拉刷新的列表,封装了一套UltraPTR+RecyclerView的下拉刷新控件……当然,也包括今天这里要写到图片加载缓存库,包括对Picasso和Glide的封装。

Picasso和Glide

Picasso和Glide两个库应该算是目前Android界最流行的图片加载库了,Picasso是由JakeWharthon大神牵头开发和维护的,Glide则是Google内部员工开源的,所以无论选择那个,都可以不必太过担心售后的问题,毕竟大品牌就摆在那里。Picasso正如它的名字取于大画家“毕加索”,所以它以加载图片的质量高著称,当然还有一个是它所需的缓存空间少;而Glide也好比它的中文意思”滑翔”,则以它加载图片的速度快为特色,相对的它需要的缓存空间更大。其中详细的对比,在这里就不做展开,毕竟今天的主题是“打造自己的图片加载库”。由于Picaaso和Glide的Api和用法都是类似的,为了描述方便,接下来主要用Picasso来讲解。

详细的对比,这里推荐一篇文章。
Picasso和Glide图片加载库对比

第一版实现

封装第三方的图片加载库,对于我们开发者来说,最关心的就是两点:使用简单、替换方便
参考了之前项目,和网上的一些例子,普遍的做法就是写一个工具类,然后维护一个静态加载库实例,最后根据需求写一系列的静态方法。这个实现应该是最简单、直接、暴力的了,使用也简单,在需要加载图片的地方,直接调用一些静态方法即可用上Picasso。

ImageLoaderUtil.getInstance().load(url, imageView1);

ImageLoaderUtil.getInstance().load(resourceId, imageView2);

但是如果这样都用静态方法来实现,使用虽然简单,替换却不怎么简单。当要替换另一个图片加载库时,我们需要改写一系列的load重载方法,而且随着需求点越多,这些方法会越来越多,变得越来越难管理。再者从设计的角度来看,这样的“简单封装”也是不可取的,违背了软件设计的OCP开闭原则

使用设计模式

Picasso本身的功能点并不多,参考其本身使用方法的设计,就是典型的建造者模式的实现。因此我们也决定使用建造者模式进行调用的封装。而针对替换方便,我们可以考虑使用策略模式进行替换封装。这里简单普及一下建造者模式和策略模式。

建造者模式

建造者模式(Builder),是将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

建造者模式

通俗的讲,使用者只需要告诉我你想要哪些功能点,逐个告诉我,然后我来拼凑给你,期间的建造过程和细节,你都不需要知道。这样正符合我们对Picasso的调用,指定加载的参数,内部使用Picasso进行图片加载,外部不需要只能里面使用的是Picasso还是Glide,我只需要得到我指定参数的图片。

策略模式

策略模式(Strategy),定义多组不同的算法或策略,并将每个算法或策略封装到具有共同接口的独立类中,从而可以灵活相互替换,不影响使用者。

策略模式

策略模式的核心在于定义一组算法的公共接口,然后就可以实现这些接口来制定不同的策略。拿到我们的应用场景,我们可以定义一个策略接口,包含一个loadImage(Builder builder)方法,并在参数内传入我们上面提供的建造者对象,则可以在具体的策略实现类里面,构造我们的第三方图片加载库调用。由于我们可以指定不同的策略,因此我们可以非常轻松的切换Picasso和Glide。

改进版实现

BaseImageConfig.java

根据调用的配置,我们先定下通用配置的基类BaseImageConfig.java。这里支持配置远程url、本地资源resourceId和Uri加载,默认加载到ImageView,还包含占位图placeholder和加载失败图errorPic。

public class BaseImageConfig {

    protected String url;

    protected int resourceId;

    protected Uri uri;

    protected ImageView imageView;

    protected int placeholder;

    protected  int errorPic;

    public String getUrl() {
        return url;
    }

    public int getResourceId() {
        return resourceId;
    }

    public Uri getUri() {
        return uri;
    }

    public ImageView getImageView() {
        return imageView;
    }

    public int getPlaceholder() {
        return placeholder;
    }

    public int getErrorPic() {
        return errorPic;
    }
}
PicassoImageConfig.java

PicassoImageConfig类继承BaseImageConfig,并针对Picasso的功能点增加自定义参数,如缓存策略,transformation转换等等,并使用建造者模式实现。

public class PicassoImageConfig extends BaseImageConfig {

    /**
     * 默认,内存缓存、本地缓存
     */
    public static final int STRATEGY_ALL = 0;
    /**
     * 只需本地缓存
     */
    public static final int STRATEGY_NO_MEMORY = 1;
    /**
     * 直接从网络获取
     */
    public static final int STRATEGY_NO_MEMORY_DISK = 2;
    /**
     * 从内存缓存、本地缓存获取
     */
    public static final int STRATEGY_OFFLINE = 3;

    private boolean isFit = false;

    private Transformation transformation;
    private List<Transformation> transformations;

    private int cacheStrategy = STRATEGY_ALL;

    private PicassoImageConfig(Builder builder) {
        this.url = builder.url;
        this.resourceId = builder.resourceId;
        this.uri = builder.uri;
        this.placeholder = builder.placeholder;
        this.errorPic = builder.errorPic;
        this.imageView = builder.imageView;

        this.isFit = builder.isFit;
        this.transformation = builder.transformation;
        this.transformations = builder.transformations;
        this.cacheStrategy = builder.cacheStrategy;
    }

    public boolean isFit() {
        return isFit;
    }

    public Transformation getTransformation() {
        return transformation;
    }
    public List<Transformation> getTransformations() {
        return transformations;
    }

    public int getCacheStrategy() {
        return cacheStrategy;
    }


    public static Builder builder() {
        return new Builder();
    }


    public static final class Builder {
        private String url;
        private int resourceId;
        private Uri uri;
        private int placeholder;
        private int errorPic;
        private ImageView imageView;

        private boolean isFit = false;

        private Transformation transformation;
        private List<Transformation> transformations;

        private int cacheStrategy;

        private Builder() {
        }

        public Builder load(String url) {
            this.url = url;
            return this;
        }
        public Builder load(int resId) {
            this.resourceId = resId;
            return this;
        }
        public Builder load(Uri uri) {
            this.uri = uri;
            return this;
        }


        /*public Builder resourceId(int resourceId) {
            this.resourceId = resourceId;
            return this;
        }*/

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

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

        public Builder into(ImageView imageView) {
            this.imageView = imageView;
            return this;
        }

        public Builder fit() {
            this.isFit = true;
            return this;
        }

        public Builder transformation(Transformation transformation) {
            this.transformation = transformation;
            return this;
        }

        public Builder transformations(List<Transformation> transformations) {
            this.transformations = transformations;
            return this;
        }

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


        public PicassoImageConfig build() {
            return new PicassoImageConfig(this);
        }
    }
}
BaseImageLoaderStrategy.java

定义所有策略的公共接口。loadImage用于加载图片,clear用于清除缓存。

public interface BaseImageLoaderStrategy<T extends BaseImageConfig> {
    void loadImage(Context ctx, T config);
    void clear(Context ctx, T config);
}
PicassoImageLoaderStrategy.java

封装Picasso核心代码,通过调用Picasso加载图片的策略具体类。

public class PicassoImageLoaderStrategy implements BaseImageLoaderStrategy<PicassoImageConfig> {

    public PicassoImageLoaderStrategy(Context ctx) {
        OkHttpClient okHttpClient = new OkHttpClient.Builder()
                .cache(OkHttp3Downloader.createDefaultCache(MyApplication.getContext()))
                .build();
        Picasso picasso = new Picasso.Builder(ctx.getApplicationContext())
                .downloader(new OkHttp3Downloader(okHttpClient))
                .build();

        if (AppConfig.IS_DEBUG_ABLE) {
            picasso.setIndicatorsEnabled(true);
            picasso.setLoggingEnabled(true);
        }

        Picasso.setSingletonInstance(picasso);
    }



    @Override
    public void loadImage(Context ctx, PicassoImageConfig config) {
        if (ctx == null) throw new IllegalStateException("Context is required");
        if (config == null) throw new IllegalStateException("GlideImageConfig is required");
        if (config.getImageView() == null) throw new IllegalStateException("imageview is required");
        if (TextUtils.isEmpty(config.getUrl()) && config.getResourceId() == 0 && config.getUri() == null) {
            throw new IllegalStateException("url or resourceId or Uri is required");
        }

        Picasso picasso = Picasso.with(ctx);

        RequestCreator requestCreator;
        if (!TextUtils.isEmpty(config.getUrl())) {
            requestCreator = picasso.load(config.getUrl());
        }else if (config.getResourceId() != 0) {
            requestCreator = picasso.load(config.getResourceId());
            //使用资源id为picasso的stableKey,用来清除缓存
            requestCreator.stableKey(String.valueOf(config.getResourceId()));
        }else {
            requestCreator = picasso.load(config.getUri());
        }


        switch (config.getCacheStrategy()) {
            case PicassoImageConfig.STRATEGY_ALL:
                break;
            case PicassoImageConfig.STRATEGY_NO_MEMORY:
                requestCreator.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE);
                break;
            case PicassoImageConfig.STRATEGY_NO_MEMORY_DISK:
                requestCreator.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)
                        .networkPolicy(NetworkPolicy.NO_CACHE, NetworkPolicy.NO_STORE);
                break;
            case PicassoImageConfig.STRATEGY_OFFLINE:
                requestCreator.networkPolicy(NetworkPolicy.OFFLINE);
                break;
        }

        if (config.isFit()) {
            requestCreator.fit();
        }

        if (config.getTransformation() != null) {
            requestCreator.transform(config.getTransformation());
        }
        if (config.getTransformations() != null) {
            requestCreator.transform(config.getTransformations());
        }

        if (config.getPlaceholder() != 0) {
            requestCreator.placeholder(config.getPlaceholder());
        }
        if (config.getErrorPic() != 0) {
            requestCreator.error(config.getErrorPic());
        }


        requestCreator.into(config.getImageView());

    }

    @Override
    public void clear(Context ctx, PicassoImageConfig config) {
        if (ctx == null) throw new IllegalStateException("Context is required");
        if (config == null) throw new IllegalStateException("GlideImageConfig is required");
        if (TextUtils.isEmpty(config.getUrl()) && config.getResourceId() == 0 && config.getUri() == null) {
            throw new IllegalStateException("url or resourceId or Uri is required");
        }

        Picasso picasso = Picasso.with(ctx);

        if (!TextUtils.isEmpty(config.getUrl())) {
            picasso.invalidate(config.getUrl());
        }else if (config.getResourceId() != 0){
            picasso.invalidate(String.valueOf(config.getResourceId()));
        }else {
            picasso.invalidate(config.getUri());
        }
    }
}
ImageLoaderUtil.java

最后就是我们第一版里面的ImageLoaderUtil工具类,不过这里只是实现对策略指定而已。

public class ImageLoaderUtil {

    private static volatile ImageLoaderUtil INSTANCE = null;
    private BaseImageLoaderStrategy mStrategy;

    public ImageLoaderUtil() {
    }

    public static ImageLoaderUtil getInstance() {
        if (INSTANCE == null) {
            synchronized (ImageLoaderUtil.class) {
                if (INSTANCE == null) {
                    INSTANCE = new ImageLoaderUtil();
                }
            }
        }
        return INSTANCE;
    }

    /**
     * 指定ImageLoader策略
     */
    public void init(BaseImageLoaderStrategy strategy) {
        setLoadImgStrategy(strategy);
    }




    public <E extends Activity, T extends BaseImageConfig> void loadImage(E context, T config) {
        if (this.mStrategy == null) {
            throw new IllegalStateException("You must call init() to set the ImageLoader strategy first!");
        }
        this.mStrategy.loadImage(context, config);
    }

    public <T extends BaseImageConfig> void clear(Context context, T config) {
        if (this.mStrategy == null) {
            throw new IllegalStateException("You must call init() to set the ImageLoader strategy first!");
        }
        this.mStrategy.clear(context, config);
    }

    public void setLoadImgStrategy(BaseImageLoaderStrategy strategy) {
        this.mStrategy = strategy;
    }

}

最后的最后,我们需要在程序最开始启动的地方(也就是Application里面)指定ImageLoaerUtil的当前策略。

ImageLoaderUtil.getInstance().init(new PicassoImageLoaderStrategy(this));
具体调用
         //一般使用
        ImageLoaderUtil.getInstance().loadImage(this,
                PicassoImageConfig.builder()
                        .load(R.drawable.ic_test)
                        .into(imageView1)
                        .build());

        //定制使用
       ImageLoaderUtil.getInstance().loadImage(this,
                PicassoImageConfig.builder()
                        .load("https://img-my.csdn.net/uploads/201205/11/1336732187_4598.jpg")
                        .transformation(new CropCircleTransformation())             //转成圆形
                        .cacheStrategy(PicassoImageConfig.STRATEGY_NO_MEMORY)       //不使用内存缓存
                        .errorPic(R.drawable.ic_error)                         //加载失败默认图
                        .into(imageView2)
                        .build());

上面提到了一个CropCircleTransformation,是自定义的Picasso transformation,用来将图片转成圆形图形。transformation的功能非常强大,在里面可以直接拿到你加载的图片的bitmap,有了bitmap你就尽可以为所欲为了…这里推荐一个github上面写得比较完整的transformation,其中还包括了Glide的transformation(也是一样的名字,可见两者相似度之高)

Picasso-Transformations

Picasso的坑

1、Picasso + OkHttp3

Picasso和OkHttp都是square旗下的开源项目,因此两者能给非常友好的兼容起来,而且Picasso本身的本地缓存也是依赖于OkHttp(或者HttpURLConnection)实现的。
但由于2.5.2版本里面,Picasso指定的OkHttp路径名是“com.squareup.okhttp.OkHttpClient”,而OkHttp3已经将包名改成了“okhttp3.OkHttpClient”,因此默认情况下它是不兼容OkHttp3的。所以如果你项目引入的OkHttp3以后的版本,又需要让Picasso用上OkHttp3做下载和缓存,则需要自定义Picasso的Downloader。如上面PicassoImageLoaderStrategy的代码。

Picasso picasso = new Picasso.Builder(ctx.getApplicationContext())
                .downloader(new OkHttp3Downloader(okHttpClient))
                .build();

这里直接用了JW大神写好的OkHttp3Downloader,下面是git地址。

JW大神的OkHttp3Downloader

2、加载图片墙,2张大图下来就卡飞了

Picasso加载图片的高清晰,在于它加载的都是原图,无论你的ImageView有多小,它加载到内存以及显示到ImageView中的都是你指定的原图。因此,在图片墙应用中,如果直接简单的loadImage,2张5M的图片下来,就足以将屏幕卡爆。
网上通用的解决方案,是加载大图不使用内存缓存,也就是使用我们上面的PicassoImageConfig.STRATEGY_NO_MEMORY策略。
但实际下来发现还是会非常卡,经过多次测试猜测应该是大图加载到多个ImageView中导致的,因此在后面又加入了fit()方法,自适应ImageView的大小进行加载。从而解决上面的图片墙加载大图问题。

3、使用fit()、resize()导致的加载失败

使用fit()解决了加载大图的问题,但同时又引入了另一个加载失败的问题。经过层层调试,发现是在2.5.2版本中BitmapHunter的问题,报出了Exception: java.io.IOException: Cannot reset ,俨然是Picasso的一个大Bug啊…
到github上面看了下Issues,发现JakeWharton大神也已经知道了,已经在snapshot版本里面改好了,只是一直没时间合到realse版本里面…ORZ…. 被迫无奈,只能在Gradle里面将picasso切到snapshot版本,从而问题暂时解决。
这里注明下:2.6.0版本已经把OkHttp3Downloader加了进去,所以已经兼容好了OkHttp3。

repositories {
maven { url ‘https://oss.sonatype.org/content/repositories/snapshots/’ }
}
……
compile ‘com.squareup.picasso:picasso:2.6.0-SNAPSHOT’

总结

以上就是对Picasso封装的过程,鉴于Glide的使用方法和Picasso的使用是完全类似的,在这里就不做展开,在后续的完整代码提交中,会带有Gilde的封装。如果想要自己实现,只需要参照PicassoImageConfig和PicassoImageLoaderStrategy两个类写就行了,其他就没什么改动。
简单回顾一下,我们思考一个封装的过程,关键点是从目的出发。对于图片加载库,我们的目的就是——”使用简单、替换方便”,结合Picasso和Glide的实际,使用简单,我们想到了建造者模式;替换方便,我们想到了策略模式,紧接从设计模式出发,逐步实现我们整个打造的过程。
说到这里,很多人会觉得,像图片加载库这些在项目中十分通用的第三方库,一些参数、方法都十分多,如果重新封装一层,显示累赘,没什么必要。其实这就取决于你对整个项目架构的部署了,直接使用当然也是可以的,只是从软件设计的角度,这显然是会对整个项目带来非常多的外部侵入,第三方库的代码会散落在项目的个个地方,耦合度过高,违背了依赖倒置原则。
当项目迁移或者越来越庞大的时候,这些代码会变得十分难管理。相反如果做了封装处理,我们代码依然是我们的代码,第三方的代码也还是第三方的代码,层次相当的清晰。
我们举一反三,延伸下去。如果我们对网络请求、日志管理、下拉刷新等等这些引用带第三方库的地方进行封装,打造成自己的一整套架构,形成模块化的管理,从整个项目架构来看,就更加的清晰了。
这也是我逐步在思考和部署的方向,包括上面提到的RxJava+Retrofit+OkHttp的网络请求框架,UltraPTR+RecyclerView的下拉刷新等等,都将往模块化、层级化去构建。这几块都会在后面逐步发布上来,同时我也会将完整代码和Demo发布带GitHub上,慢慢填充。在自我提高和总结的同时,也希望能更多的给也在做着同样事情的朋友们一些参考。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 5
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值