打造自己的图片加载缓存库(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上,慢慢填充。在自我提高和总结的同时,也希望能更多的给也在做着同样事情的朋友们一些参考。
- [转]JavaScript/Node.JS 中的 Promises
- java中的序列化
- 汉语转英文字母
- 学习笔记---回溯算法与贪心算法
- Spring入门hello world常见问题及解决办法
- 打造自己的图片加载缓存库(Picasso OR Glide)
- 121. Best Time to Buy and Sell Stock
- 欢迎使用CSDN-markdown编辑器
- 使用pano2vr创建全景图
- js json转url参数
- W88 Online Casino Weekdays 100% Combo Rebate Bonus(Cash Back, Combo Rebate Bonus, w88, W88 Online C)
- Android Studio开发——VT-x is disabled in BIOS问题解决
- 每日小记2017.3.7
- java方法传递引用、传递基本类型