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

来源:互联网 发布:河北经济网络电视台 编辑:程序博客网 时间:2024/06/05 01:59

好久没写文章了,一个是工作的原因,还一个就是这几个月看了很多文章,一直在补充自己的知识。之前看到一句很喜欢的话——感到快乐就忙东忙西,感到累了就放空自己,这几个月一“快乐”了,就停不下来地看、一直在写代码。期间由于项目的需求,用这里那里学来的东西,写了一套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("http://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上,慢慢填充。在自我提高和总结的同时,也希望能更多的给也在做着同样事情的朋友们一些参考。

4 0
原创粉丝点击