Android中Glide加载库的图片缓存配置究极指南
零、选择Glide
为什么图片加载我首先推荐Glide?
图片加载框架用了不少,从afinal框架的afinalBitmap,Xutils的BitmapUtils,老牌框架universalImageLoader,著名开源组织square的picasso,google推荐的glide到FaceBook推出的fresco。这些我前前后后都体验过,那么面对这么多的框架,该如何选择呢?下面简单分析下我的看法。
afinal和Xuils在github上作者已经停止维护了,开源社区最新的框架要属KJFramework,不过这种快速开发框架看似很好用,功能也应有尽有,小型项目也罢,大型项目我不是很推荐,这样做项目的耦合度太高,一旦出现停止维护,而新的问题不断增加,没人处理就麻烦了。
在glide和fresco还未出来的时候,当时最火的莫过于universalImageLoader和picasso了,当时觉得universalImageLoader配置相对picasso麻烦,虽然提供了各种配置,但是没有实践过,根本不知道如何配置,还不如都采用默认配置,就选择了picasso作为图片加载框架,用了近一年的时间,没有太大的问题,且使用简单,或许是因为之前的项目太过于简单,周期也并不是很长,还有使用eclipse开发,一个很大的问题一直都没有暴露出来,换上了最新的Android Studio可以清晰的看到各种性能相关的监控,如cpu还有内存监控,终于知道了之前做的项目都那么的卡顿的罪魁祸首,picasso加载稍微大一点的图片就特别耗内存,通常一个listView或者顶部滑动广告栏都含有多张图片,这使得做出的页面只要含图片较多就异常卡顿(之前的时候还把它归结为测试机不好),知道这一点后我就有点想把picasso给替换掉,但这一次我不能那么粗心。
测试了picasso,glide,universalImageLoader,fresco这四个框架,测试内容大概有以下几项,内存测试,大图片测试,小图片测试,本地图片,网络图片当然还结合官方文档体验其特色功能,内存测试中,glide,universalImageLoader,fresco表现都非常优秀,picasso这一点上实在是太糟糕了,小图片差别也不是很大,稍微大点图片内存消耗就要比其他高出几倍,这一点上证明了我的猜想,picasso不能再用了,下面一项项分析其他框架,在高于2M左右大图测试中fresco的表现则和picasso一样直接神马都不显示,项目中要实现大图预览功能,这点上是不行的,接着看universalImageLoader和glide在这几项测试中成绩都很好,到底该如何选择呢?
因为我项目之前用的picasso,glide从用法上几乎就是另一个picasso,从picasso转移到glide相对改动较少,还有一点就是这个项目是google在维护,我也能给它更多的信任,相比较universalImageLoader,glide可以支持gif和短视频,后期也需要用到,这里不得不谈一下glide优秀的缓存机制了,glide图片缓存默认使用RGB565相当于ARGB8888可以节省不少的空间,支持与activity,fragment,application生命周期的联动,更智能管理图片请求当然还有其他的扩展更多可以看?glide介绍?当然,glide的方法数量比universalImageLoader多了1000多个,遇到64k问题的会比较关注这个。
刚才只是掠过fresco,其实我对他的期待还是蛮大的,因为刚出来还有居多不稳定的地方,里面存在着大量吸引着我的功能,支持webps格式(和jpg一样都是有损压缩格式,webps相同质量图片更节省空间),支持渐进式jpeg,可以轻松的定制image的各种属性,支持多图请求和图片复用,并支持手势缩放和旋转等等,更多介绍?fresco,当然,实际用的时候并没有那么好,很多功能都有待完善。
还有一点细节的地方要注意的,最好不要直接拿来用,至少经过自己简单的封装,而不是直接在项目中使用,一个简单的例子,后期图片过多,可能需要另外配置一台机器单独存放图片,主机地址做成可配置,可不要因为一个简单的需求又要加班了
更多。
一、Glide3.0以来的新特性
1.动态的GIF图片加载:
Glide.with(context).load(...).asBitmap() //显示gif静态图片 Glide.with(context).load(...).asGif() //显示gif动态图片
2.本地视频快照:
Glide现在还可以把视频解码为一张图片:
Glide.with(context).load(“视频路径“)
(经过我的测试,只能把手机本地的mp4视频解析为一张图片,把mp4文件放在raw文件中,不能解析)
3.对缩略图的支持:
//加载yourView1/10尺寸的缩略图,然后加载全图 Glide.with(yourFragment).load(yourUrl).thumbnail(0.1f).into(yourView)
4.生命周期集成
同时将Activity/Fragment作为with()参数的好处是:图片加载会和Activity/Fragment的生命周期保持一致,
请求会在onStop的时候自动暂停,
在onStart的时候重新启动,gif的动画也会在onStop的时候停止,以免在后台消耗电量。
5.转码
Glide的.toBytes()和.transcode()方法允许在后台获取、解码和转换一个图片,你可以将一张图片转换成更多有用的图片格式,比如,上传一张250*250的图片
Glide.with(context) .load(“/user/profile/photo/path”) .asBitmap() .toBytes() .centerCrop() .into(new SimpleTarget<byte[]>(250, 250) { @Override public void onResourceReady(byte[] data, GlideAnimation anim) { // Post your bytes to a background thread and upload them here. } });
6.动画:3.x加入了cross fades和View的属性动画的支持
比如
(.animate(ViewPropertyAnimation.Animator))
7. 网络模块可以选择OkHttp或者Volley的支持
You can now choose to use either OkHttp, or Volley, or Glide's HttpUrlConnection default as your network stack.
Volley和OkHttp可以在gradle文件当中添加依赖,注册相应的ModelLoaderFactory
二、图片的缓存和缓存的时效机制
1.图片缓存的键值
图片缓存的键值主要用于DiskCacheStrategy.RESULT,Glide当中的键值主要包含三个部分:
通过DataFetcher.getId()方法返回的String数据作为键值。一般的DataFetchers会简单返回数据模型data model的toString()结果,如果是URL/File会返回相应的路径
图片的尺寸,主要是通过override(width,height)或者通过Target's getSize()方法确定的尺寸信息
包含一个可选的签名所有的这些东西会通过一种散列算法生成一个独有、安全的文件名,通过此文件名将图片缓存在disk中
2.缓存失效
因为Glide当中图片缓存key的生成是通过一个散列算法来实现的,所以很难手动去确定哪些文件可以从缓存当中进行删除
2.1 当内容(url,file path)改变的时候,改变相应的标识符就可以了,Glide当中也提供了signature()方法,将一个附加的数据加入到缓存key当中
多媒体存储数据,可用MediaStoreSignature类作为标识符,会将文件的修改时间、mimeType等信息作为cacheKey的一部分
文件,使用StringSignature
Urls ,使用StringSignature
Glide.with(yourFragment) .load(yourFileDataModel) .signature(new StringSignature(yourVersionMetadata)) .into(yourImageView); Glide.with(fragment) .load(mediaStoreUri) .signature(new MediaStoreSignature(mimeType, dateModified, orientation)) .into(view);
自定义标识符:
public class IntegerVersionSignature implements Key { private int currentVersion; public IntegerVersionSignature(int currentVersion) { this.currentVersion = currentVersion; } @Override public boolean equals(Object o) { if (o instanceof IntegerVersionSignature) { IntegerVersionSignature other = (IntegerVersionSignature) o; return currentVersion = other.currentVersion; } return false; } @Override public int hashCode() { return currentVersion; } @Override public void updateDiskCacheKey(MessageDigest md) { messageDigest.update(ByteBuffer.allocate(Integer.SIZE) .putInt(signature).array()); } }
2.2、不缓存可以通过diskCacheStrategy(DiskCacheStrategy.NONE.)实现
三、配置GlideModules
可以通过GlideModule接口来配置Glide的配置文件,并且像ModelLoaders一样注册相关组件。
包含一个GlideMode :
第一步、To use and register a GlideModule, first implement the interface with your configuration and components:
public class MyGlideModule implements GlideModule { @Override public void applyOptions(Context context, GlideBuilder builder) { // Apply options to the builder here. } @Override public void registerComponents(Context context, Glide glide) { // register ModelLoaders here. } }
第二步、然后将上面的实现了加入到proguard.cfg当中:
-keepnames class * com.mypackage.MyGlideModule
第三步、在AndroidManifest.xml文件中添加meta-data,以便Glide能够找到你的Module
<meta-data android:name="com.bumptech.glide.samples.flickr.FlickrGlideModule" android:value="GlideModule" />
四、Library项目
一个Library项目可能会定义一个或者多个GlideModules,如果一个Library项目添加一个Module到Library项目的manifest当中,依赖于此Library的应用就会自动加载依赖库(Library项目)当中的Module。
当然,如果manifest的合并不正确,那么Library里面Module就必须手动地在应用当中添加进去。
五、GlideModules冲突
虽然Glide允许一个应用当中存在多个GlideModules,Glide并不会按照一个特殊的顺序去调用已注册的GlideModules,如果一个应用的多个依赖工程当中有多个相同的Modules,就有可能会产生冲突。
如果一个冲突是不可避免的,应用应该默认去定义一个自己的Module,用来手动地处理这个冲突,在进行Manifest合并的时候,可以用下面的标签排除冲突的module。
<meta-data android:name=”com.mypackage.MyGlideModule” tools:node=”remove”/>
六、通过GlideBuilder配置全局配置文件
Glide允许开发者配置自定义的全局操作应用于所有的请求,这个部分可以通过GlideModule接口中的applyOptions方法的GlideBuilder参数实现 :
1.DiskCache
1.1、硬盘缓存是在一个后台线程当中,通过一个DiskCache.Factory接口进行缓存的。
开发者能够通过GlideBuilder的setDiskCache(DiskCache.Factory df)方法设置存储的位置和大小
通过传入DiskCacheAdapter来完全禁用缓存
自定义一个DiskCache来完全禁用缓存,
Glide默认是用InternalCacheDiskCacheFactory类来创建硬盘缓存的,这个类会在应用的内部缓存目录下面创建一个最大容量250MB的缓存文件夹,使用这个缓存目录而不用sd卡,意味着除了本应用之外,其他应用是不能访问缓存的图片文件的。
1.2.设置disk缓存的大小 : InternalCacheDiskCacheFactory
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, yourSizeInBytes));
1.3.设置缓存的路径
可以通过实现DiskCache.Factory,然后使用DiskLruCacheWrapper创建一个新的缓存目录,比如,可以通过如下方式在外存当中创建缓存目录:
builder .setDiskCache(new DiskCache.Factory() { @Override public DiskCache build() { // Careful: the external cache directory doesn't enforce permissions File cacheLocation = new File(context.getExternalCacheDir(), "cache_dir_name"); cacheLocation.mkdirs(); return DiskLruCacheWrapper.get(cacheLocation, yourSizeInBytes); } });
2.内存当中的缓存和POOLS
GlideBuilder当中,允许开发者去设置内存当中图片缓存区的大小,主要涉及到的类包括MemoryCache和BitmapPool
2.1 大小的设置
默认内存缓存的大小是用过MemorySizeCalculator来实现的,这个类会根据设备屏幕的大小,计算出一个合适的size,开发者可以获取到相关的默认设置信息:
MemorySizeCalculator calculator = new MemorySizeCalculator(context); int defaultMemoryCacheSize = calculator.getMemoryCacheSize(); int defaultBitmapPoolSize = calculator.getBitmapPoolSize();
如果在应用当中想要调整内存缓存的大小,开发者可以通过如下方式:
Glide.get(context).setMemoryCategory(MemoryCategory.HIGH);
2.2 Memory Cache
Glide内存缓存的目的是减少I/O,提高效率
可以通过GlideBuidler的setMemoryCache(MemoryCache memoryCache)去设置缓存的大小,开发者可以通过LruResourceCache类去设置缓存区的大小
builder.setMemoryCache(new LruResourceCache(yourSizeInBytes));
2.3 Bitmap Pool
可以通过GlideBuilder的setBitmapPool()方法设置池子的大小,LruBitmapPool是Glide的默认实现,使用如下:
builder.setBitmapPool(new LruBitmapPool(sizeInBytes));
.图片格式
GlideBuilder允许开发者设置一个全局的默认图片格式,
在默认情况下,Glide使用RGB_565格式加载图片,如果想要使用高质量的图片,可以通过如下方式设置系统的图片格式:
builder.setDecodeFormat(DecodeFormat.ALWAYS_ARGB_8888);
七、自定义显示控件
除了可以将图片、视频快照和GIFS显示在View上面之外,开发者也可以在自定义的Target上面显示这些媒体文件
1.SimpleTarget
重点内容
如果你想简单地加载一个Bitmap,你可以通过以下简单的方式而不是直接地显示给用户,可能是显示一个notification,或者上传一个头像,Glide都能很好地实现
SimpleTarget提供了对Target的简单实现,并且让你专注于对加载结果的处理
为了使用SimpleTarget,开发者需要提供一个宽和高的像素值,用来加载你的资源文件,并且你需要去实现
onResourceReady(R resource,GlideAnimation<? super R> glideAnimation) int myWidth = 512; int myHeight = 384; Glide.with(yourApplicationContext)) .load(youUrl) .asBitmap() .into(new SimpleTarget<Bitmap>(myWidth, myHeight) { @Override public void onResourceReady(Bitmap bitmap, GlideAnimation anim) { // Do something with bitmap here. } };
说明:
通常你去加载资源的时候,是将他们加载到一个view当中,当fragment或者activity失去焦点或者distroyed的时候,Glide会自动停止加载相关资源,确保资源不会被浪费
在大多数SimpleTarget的实现当中,如果需要资源的加载不受组件生命周期的影响,Glide.width(context)当中的context是application context而不是fragment或者activity
另外,由于一些long running operations可能会导致内存泄露,如果你打算使用一个这样的操作,可以考虑使用一个静态的内部类而不是一个动态的内部类。
2.ViewTarget
如果你想加载一张图片到一个view当中,但是又想改变或者监听Glide默认的部分设置,就可以通过重写ViewTarget或者他的子类来实现
如果你想Gidle加载图片的时候可以自定义图片的大小,或者想要设置一个自定义的显示动画,就可以通过ViewTarget来实现,可以通过一个静态的ViewTarget或者动态的内部类来实现相关的功能
Glide.with(yourFragment) .load(yourUrl) .into(new ViewTarget<YourViewClass, GlideDrawable>(yourViewObject) { @Override public void onResourceReady(GlideDrawable resource, GlideAnimation anim) { YourViewClass myView = this.view; // Set your resource on myView and/or start your animation here. } });
说明:
加载一张静态的图片或者一张GIF动态图,可以在load后面加上asBitmap()/asGif()
.Load(url)会通过asXXX()替换ViewTarget当中的GlideDrawable参数,也可以通过实现LifecycleLisener,给target设置一个回调。
3.覆盖默认的相关设置
如果只是想使用Glide的默认配置,可以使用Glide当中ImageViewTargets的两个子类:
GlideDrawableImageViewTarget 默认的实现,可以通过asGif()加载动态图片
BitmapImageViewTarget 可以通过asBitmap()加载静态图片
如果想要使用Glide默认实现,可以在他们的子类方法当中使用super.xx()即可,例如:
Glide.with(yourFragment) .load(yourUrl) .asBitmap() .into(new BitmapImageViewTarget(yourImageView)) { @Override public void onResourceReady(Bitmap bitmap, GlideAnimation anim) { super.onResourceReady(bitmap, anim); Palette.generateAsync(bitmap, new Palette.PaletteAsyncListener() { @Override public void onGenerated(Palette palette) { // Here's your generated palette } }); } });
八、使用Glide下载自定义尺寸的图片
Glide的ModelLoader接口为开发者提供了装载图片的view的尺寸,并且允许开发者使用这些尺寸信息去选择合适的URL去下载图片。选用适当的尺寸可以节省宽带和设备的空间开销,提高app的性能
2014年googleI/o大会发表了一篇文章,阐述了他们如何使用ModelLoader接口去适配图片的尺寸,见下面的连接:https://github.com/google/iosched/blob/master/doc/IMAGES.md
1、通过http/https下载图片,可以通过继承BaseGlideUtlLoader来实现:
public interface MyDataModel { public String buildUrl(int width, int height); } public class MyUrlLoader extends BaseGlideUrlLoader<MyDataModel> { @Override protected String getUrl(MyDataModel model, int width, int height) { // Construct the url for the correct size here. return model.buildUrl(width, height); } }
2、可以使用你自定义的ModelLoader去加载图片了
Glide.with(yourFragment) .using(new MyUrlLoader()) .load(yourModel) .into(yourView);
如果你想避免每次加载图片都要使用.using(new MyUrlLoader()) ,可以实现是一个
ModelLoaderFactory然后使用Glide将它注册到GlideModule当中 public class MyGlideModule implements GlideModule { ... @Override public void registerComponents(Context context, Glide glide) { glide.register(MyDataModel.class, InputStream.class, new MyUrlLoader.Factory()); } }
这样你就可以跳过.using()了
Glide.with(yourFragment) .load(yourModel) .into(yourView);
九、集成库
1.什么是集成库
Glide包含一些小的、可选的集成库,目前Glide集成库当中包含了访问网络操作的Volley和OkHttp
2.为什么要包含集成库
这些集成库,和Glide的ModelLoader系统允许开发者使用一致地框架去进行网络相关的操作
3.如何将一个库集成到Glide当中,
将一个库集成到Glide当中需要两步操作,
包含正确的dependency,
确保创建了该集成库的GlideModule,比如,
将Volley集成到Glide当中
第一步、添加依赖
dependencies { compile 'com.github.bumptech.glide:volley-integration:1.2.2' compile 'com.mcxiaoke.volley:library:1.0.5' }
第二步、创建Volley集成库的GlideModule
<meta-data android:name="com.bumptech.glide.integration.volley.VolleyGlideModule" android:value="GlideModule" />
然后改变混淆文件:
-keep class com.bumptech.glide.integration.volley.VolleyGlideModule #or -keep public class * implements com.bumptech.glide.module.GlideModule
将OkHttp集成到Glide当中:
第一步、添加依赖
dependencies { compile 'com.github.bumptech.glide:okhttp-integration:1.2.2' compile 'com.squareup.okhttp:okhttp:2.0.0' }
第二步、创建OkHttp集成库的GlideModule
<meta-data android:name="com.bumptech.glide.integration.okhttp.OkHttpGlideModule" android:value="GlideModule" />
-keep class com.bumptech.glide.integration.okhttp.OkHttpGlideModule #or -keep public class * implements com.bumptech.glide.module.GlideModule
十、在后台线程当中进行加载和缓存
为了保证Glide在后台线程当中加载资源文件更加容易,Glide除了Glide.with(fragment).load(url).into(view)之外还提供了
downloadOnly(int width, int height) downloadOnly(Y target)// Y extends Target<File> into(int width, int height)
1.downloadOnly
Glide的downloadOnly()允许开发者将Image的二进制文件下载到硬盘缓存当中,以便在后续使用,
在UI线程当中异步下载,在异步线程当中则是使用width和height
在异步线程当中同步调用下载,在同步线程当中,
downloadOnly使用一个target作为参数
(1)在后台线程当中下载图片,可以通过如下的方式:
FutureTarget<File> future = Glide.with(applicationContext) .load(yourUrl) .downloadOnly(500, 500); File cacheFile = future.get();
当future返回的时候,image的二进制文件信息就存入了disk缓存了,值得注意的是downloadOnly API只是保证图片个bytes数据在disk当中是有效的。
(2)下载完毕之后如果想要进行显示,可以通过如下方式进行调用:
Glide.with(yourFragment) .load(yourUrl) .diskCacheStrategy(DiskCacheStrategy.ALL) .into(yourView);
通过DiskCacheStrategy.ALL或者DiskCacheStrategy.SOURCE,可以保证程序会去读取缓存文件
2. 如果想要在后台线程当中获取某个URL对应的Bitmap
不通过downloadOnly,可以使用into(),会返回一个FutureTarget对象,比如,想要得到一个URL对应的500*500的centerCrop裁剪图片,可以通过如下方式实现:
Bitmap myBitmap = Glide.with(applicationContext) .load(yourUrl) .asBitmap() .centerCrop() .into(500, 500) .get()
注意:上面的调用只能在异步线程当中,如果在main Thread当中调用.get(),会阻塞主线
十一、转换器
1.默认的转换器
Glide两个默认的转换器,fitCenter和CenterCrop,其他的转换器详见https://github.com/wasabeef/glide-transformations,可以将图片转为各种形状,例如圆形,圆角型等等
用法:
Glide.with(yourFragment) .load(yourUrl) .fitCenter() .into(yourView); Glide.with(yourFragment) .load(yourUrl) .centerCrop() .into(yourView); // For Bitmaps: Glide.with(yourFragment) .load(yourUrl) .asBitmap() .centerCrop() .into(yourView); // For gifs: Glide.with(yourFragment) .load(yourUrl) .asGif() .fitCenter() .into(yourView);
甚至可以在两幅图片进行类型转换的时候进行transformed
Glide.with(yourFragment) .load(yourUrl) .asBitmap() .toBytes() .centerCrop() .into(new SimpleTarget<byte[]>(...) { ... });
2.自定义转换器
方法:
第一步、编写转换器类 ,继承BitmapTransformation:
private static class MyTransformation extends BitmapTransformation { public MyTransformation(Context context) { super(context); } @Override protected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) { Bitmap myTransformedBitmap = ... // apply some transformation here. return myTransformedBitmap; } @Override public String getId() { // Return some id that uniquely identifies your transformation. return "com.example.myapp.MyTransformation"; } }
第二步、在Glide方法链当中用.transform(…)替换fitCenter()/centerCrop()
Glide.with(yourFragment) .load(yourUrl) .transform(new MyTransformation(context)) .into(yourView); // For Bitmaps: Glide.with(yourFragment) .load(yourUrl) .asBitmap() .transform(new MyTransformation(context)) .into(yourView); // For Gifs: Glide.with(yourFragment) .load(yourUrl) .asGif() .transform(new MyTransformation(context)) .into(yourView);
3.自定义转换器的尺寸
在上面使用过程当中没有设置尺寸值,那么转换器转换的图片尺寸怎么确定呢,
Glide实际上已经足够智能根据view的尺寸来确定转换图片的尺寸了
如果需要自定义尺寸,而不是用view和target当中的尺寸,那么可以使用override(int,int)设置相关的宽和高
4. Bitmap 再利用
为了减少垃圾收集,可以通过BitmapPool接口去释放不需要的Bitmaps,当然也可以对里面的bitmap进行再利用。
例如在一次转换中,
从pool当中得到一个bitmap
将Bitmap回设给Canvas
使用Matrix、Paint在Canvas上面绘制原始的Bitmap,或者通过一个Shader来转换一个image
4.1 不要手动地去释放一个转换的bitmap资源,也不要将transform()之后的Bitmap重新放置到BitmapPool当中去
protected Bitmap transform(BitmapPool bitmapPool, Bitmap original, int width, int height) { Bitmap result = bitmapPool.get(width, height, Bitmap.Config.ARGB_8888); // If no matching Bitmap is in the pool, get will return null, so we should //allocate. if (result == null) { // Use ARGB_8888 since we're going to add alpha to the image. result = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); } // Create a Canvas backed by the result Bitmap. Canvas canvas = new Canvas(result); Paint paint = new Paint(); paint.setAlpha(128); // Draw the original Bitmap onto the result Bitmap with a transformation. canvas.drawBitmap(original, 0, 0, paint); // Since we've replaced our original Bitmap, we return our new Bitmap here. Glide will // will take care of returning our original Bitmap to the BitmapPool for us. return result; }