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