最近的工作是做一个IM的Android端的SDK和插件。
在社交软件中浏览图片是一项基本功能,我们的IM也不例外,支持图片收发,预览等基本操作。但是随着斗图时代的到来,对IM的图片处理提出了更高的要求,IM的PC端也已经开始支持发送gif图片消息,所以Android上也准备支持gif图的收发和展示。
先说说IM对图片库的一些需求:
1. 因为IM中聊天的图片需要从Http的Header中校验请求是否合法,所以需要有定制网络请求的能力;
2. 支持Gif图片格式,而且需要能够自动识别图片格式,不需要人工判断
3. 內建支持图片的裁剪变形等功能
4. 性能必须足够好,否则对IM的使用体验会有严重的损害
其中第2条是本次新增的需求,1、3、4是原本就有的需求。
安卓上常用的图片图有ImageLoader、Picasso、Glide和Fresco这几个库。
IM原来是使用Picasso加载图片的,将图片的下载、加载、缓存等功能都交给Picasso来完成,上面的1、3、4都能比较好的满足。Picasso的优点是体积小速度快,而且是Square出品的,跟自家的库(OkHttp等)的可以很好的结合使用;缺点就是不支持gif。
其实IM中本来也使用了Gif图的库android-gif-drawable,但是只能显示已经下载好的Gif图。图片的下载、缓存等功能都需要自己重新实现,比较麻烦。
因此我们综合考虑以后,选择了Glide作为IM图片库的新选择。关于这些图片库的对比可以参考——Android 四大大图片缓存(Imageloader,Picasso,Glide,Fresco)原理、特性对比
以上是背景
因为IM中对Picasso进行了包装,所以替换图片库的改动范围并不大,也没有什么高深的原理或技巧,而且关于Glide的教程已经有很多了,所以本文的重点并不是介绍Glide的接入方式,而是记录我在接入过程中遇到的一些坑,方便同样遇到问题的朋友排查错误。
Glide的api基本上跟Picasso一致,只是将图片变形、占位图设置、错误图设置放到了apply()方法中,稍微学习一下就能掌握了。如下:
Glide.with(context)
.load(path)
.apply(RequestOptions.centerInsideTransform()
.error(context.getResources().getDrawable(errorResId))
.placeholder(context.getResources().getDrawable(placeholderResId)))
.into(target);
而且其load()方法不区分url、File、drawableID,也不需要指定图片格式为jpg或Gif,可以自动识别图片类型,完成加载。
我们用Picasso或者ImageLoader的时候,有几个使用习惯:
1. 给ImageView设置Tag,防止显示的图片错位;
2. 在Recyclerview或ListView中加载图片,设置滑动监听,滚动的时候暂停加载,停止滚动的时候恢复加载,以使滚动流畅,缺点是滚动的过程中,图片都只能显示占位图,无法动态显示;
3. 在Activity或者Fragment退出的时候,取消正在加载的图片。
在Glide中如果继续使用这些方法则会引起问题:
1、 如果ImageView设置了Tag,Glide会抛异常,因为Glide内部已经做了Tag的设置了,所以一定要查找代码中的Tag设置;
2、 Glide内部判断了View在Recyclerview或ListView的显示状态,会自动判断图片是否应该暂停加载,滑动时的显示效果非常好,可以立即展示出图片来,如果仍然使用暂停加载的操作,可能反而会导致滚动时卡顿,如果发现卡顿的情况,一定要先排查这个问题。
3、 Glide在调用时必须以Glide.with()开头,参数为Activity、Fragment、View或Context对象,Glide会自动绑定这些对象的生命周期,在其退出时自动取消图片加载,因此无需手动调用。
使用Glide还有一个需要注意的地方,如果ImageView的宽高设置成wrap_content,显示Gif图时可能不正确,会变得很小,跟布局有关,调整一下布局的属性设置。
也就是如何实现上文中的“需求1”,有两种方法:
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
}
}
是不是觉得怪怪的,感觉云里雾里,没关系,看完本文你就懂了。
GlideUrl glideUrl = new GlideUrl("url", new LazyHeaders.Builder()
.addHeader("key1", "value")
.addHeader("key2", new LazyHeaderFactory() {
@Override
public String buildHeader() {
String expensiveAuthHeader = computeExpensiveAuthHeader();
return expensiveAuthHeader;
}
})
.build());
Glide....load(glideUrl)....;
我们都知道,使用图片库,很重要的一步是对其进行定制,并不是要重写代码,而是根据图片库提供的API设置缓存大小、缓存策略、网络请求方式等功能,以符合本应用的使用场景。IM中在定制Glide库的时候就遇到了不少问题。
网上流传了不少的Glide的定制教程,很多是针对3.*版本的,最新的4.0版本与3.*版本相差不大,定制方式也差不多—— android 图片加载库 Glide 的使用介绍。
Glide从3.0开始通过注解方式对Glide进行自定义,也就是自定义 GlideModule。
自定义 GlideModule 可以:
1、全局的改变 glide 的加载策略
2、自定义磁盘缓存目录
3、设置图片加载的质量
4、…
如何操作:
1. 首先定义一个类实现 GlideModule
@GlideModule
public class FlickrGlideModule extends AppGlideModule {
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
registry.append(Photo.class, InputStream.class, new FlickrModelLoader.Factory());
}
@Override
public void applyOptions(Context context, GlideBuilder builder) {
MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context)
.setMemoryCacheScreens(2)
.build();
builder.setMemoryCache(new LruResourceCache(calculator.getMemoryCacheSize()));
}
}
"package.path.of.FlickrGlideModule "
android:value="GlideModule" />
在一般的情况下,这样就完成了Glide的定制。这样的定制方式有什么好处,我暂时还没有领悟到。下面的源码是Glide.java文件中解析AndroidManifest文件并生成Glide对象的逻辑:
private static void initializeGlide(Context context) {
Context applicationContext = context.getApplicationContext();
GeneratedAppGlideModule annotationGeneratedModule = getAnnotationGeneratedGlideModules();
List manifestModules = Collections.emptyList();
if (annotationGeneratedModule == null || annotationGeneratedModule.isManifestParsingEnabled()) {
manifestModules = new ManifestParser(applicationContext).parse();
}
...
RequestManagerRetriever.RequestManagerFactory factory =
annotationGeneratedModule != null
? annotationGeneratedModule.getRequestManagerFactory() : null;
GlideBuilder builder = new GlideBuilder()
.setRequestManagerFactory(factory);
for (GlideModule module : manifestModules) {
module.applyOptions(applicationContext, builder);
}
if (annotationGeneratedModule != null) {
annotationGeneratedModule.applyOptions(applicationContext, builder);
}
Glide glide = builder.build(applicationContext);
for (GlideModule module : manifestModules) {
module.registerComponents(applicationContext, glide, glide.registry);
}
if (annotationGeneratedModule != null) {
annotationGeneratedModule.registerComponents(applicationContext, glide, glide.registry);
}
context.getApplicationContext().registerComponentCallbacks(glide);
Glide.glide = glide;
}
查看一下Glide中这个方法的调用就会发现:Glide.glide的实例只能从这个方法中生成。其中GlideModule 的applyOptions()方法是在glide生成之前调用的,registerComponents()方法是在glide生成之后调用的,分别完成不同阶段的定制。
但是这也造成了一个问题:必须通过GlideModule的方式才能定制Glide。而IM目前主要是通过插件的形式使用的,因此无法读取到自身AndroidManifest文件中的内容,但是IM又必须定制Glide,在没有其他方法的情况下,IM最后是使用GlideBuilder创建出Glide对象以后,再通过反射替换掉Glide.glide的对象,以此达到定制的目的,代码很简单,就不贴了。
请看流水账:
默认情况下,Glide会根据图片的前两个字节判断图片格式,从而自动转换成对应的对象加载显示。而Glide也开放了接口,让我们可以自定义图片的下载、加载、解码、编码(保存)过程。这种能力是通过Registry类实现的。
边看源码,边听解说,下面的源码是Glide对象的构造函数,里面通过Registry注册了所有默认支持的格式处理过程,看代码会发现参数都是很有规律的,粗略过一遍代码即可。
Glide(...) {
...
registry = new Registry();
registry.register(new DefaultImageHeaderParser());
...
registry.register(ByteBuffer.class, new ByteBufferEncoder())
.register(InputStream.class, new StreamEncoder(arrayPool))
/* Bitmaps */
.append(ByteBuffer.class, Bitmap.class,
new ByteBufferBitmapDecoder(downsampler))
.append(InputStream.class, Bitmap.class,
new StreamBitmapDecoder(downsampler, arrayPool))
.append(ParcelFileDescriptor.class, Bitmap.class, new VideoBitmapDecoder(bitmapPool))
.register(Bitmap.class, new BitmapEncoder())
/* GlideBitmapDrawables */
.append(ByteBuffer.class, BitmapDrawable.class,
new BitmapDrawableDecoder<>(resources, bitmapPool,
new ByteBufferBitmapDecoder(downsampler)))
.append(InputStream.class, BitmapDrawable.class,
new BitmapDrawableDecoder<>(resources, bitmapPool,
new StreamBitmapDecoder(downsampler, arrayPool)))
.append(ParcelFileDescriptor.class, BitmapDrawable.class,
new BitmapDrawableDecoder<>(resources, bitmapPool, new VideoBitmapDecoder(bitmapPool)))
.register(BitmapDrawable.class, new BitmapDrawableEncoder(bitmapPool, new BitmapEncoder()))
/* GIFs */
.prepend(InputStream.class, GifDrawable.class,
new StreamGifDecoder(registry.getImageHeaderParsers(), byteBufferGifDecoder, arrayPool))
.prepend(ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
.register(GifDrawable.class, new GifDrawableEncoder())
/* GIF Frames */
.append(GifDecoder.class, GifDecoder.class, new UnitModelLoader.Factory())
.append(GifDecoder.class, Bitmap.class, new GifFrameResourceDecoder(bitmapPool))
/* Files */
.register(new ByteBufferRewinder.Factory())
.append(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory())
.append(File.class, InputStream.class, new FileLoader.StreamFactory())
.append(File.class, File.class, new FileDecoder())
.append(File.class, ParcelFileDescriptor.class, new FileLoader.FileDescriptorFactory())
.append(File.class, File.class, new UnitModelLoader.Factory())
/* Models */
.register(new InputStreamRewinder.Factory(arrayPool))
.append(int.class, InputStream.class, new ResourceLoader.StreamFactory(resources))
.append(
int.class,
ParcelFileDescriptor.class,
new ResourceLoader.FileDescriptorFactory(resources))
.append(Integer.class, InputStream.class, new ResourceLoader.StreamFactory(resources))
.append(
Integer.class,
ParcelFileDescriptor.class,
new ResourceLoader.FileDescriptorFactory(resources))
.append(String.class, InputStream.class, new DataUrlLoader.StreamFactory())
.append(String.class, InputStream.class, new StringLoader.StreamFactory())
.append(String.class, ParcelFileDescriptor.class, new StringLoader.FileDescriptorFactory())
.append(Uri.class, InputStream.class, new HttpUriLoader.Factory())
.append(Uri.class, InputStream.class, new AssetUriLoader.StreamFactory(context.getAssets()))
.append(
Uri.class,
ParcelFileDescriptor.class,
new AssetUriLoader.FileDescriptorFactory(context.getAssets()))
.append(Uri.class, InputStream.class, new MediaStoreImageThumbLoader.Factory(context))
.append(Uri.class, InputStream.class, new MediaStoreVideoThumbLoader.Factory(context))
.append(
Uri.class,
InputStream.class,
new UriLoader.StreamFactory(context.getContentResolver()))
.append(Uri.class, ParcelFileDescriptor.class,
new UriLoader.FileDescriptorFactory(context.getContentResolver()))
.append(Uri.class, InputStream.class, new UrlUriLoader.StreamFactory())
.append(URL.class, InputStream.class, new UrlLoader.StreamFactory())
.append(Uri.class, File.class, new MediaStoreFileLoader.Factory(context))
.append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
.append(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory())
.append(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory())
/* Transcoders */
.register(Bitmap.class, BitmapDrawable.class,
new BitmapDrawableTranscoder(resources, bitmapPool))
.register(Bitmap.class, byte[].class, new BitmapBytesTranscoder())
.register(GifDrawable.class, byte[].class, new GifDrawableBytesTranscoder());
ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
glideContext =
new GlideContext(
context, registry, imageViewTargetFactory, defaultRequestOptions,
defaultTransitionOptions, engine, logLevel);
}
看完代码会发现好像有规律:
带着疑问再来看看Registy的源码:
...
public Registry register(Class dataClass, Encoder encoder) {
encoderRegistry.add(dataClass, encoder);
return this;
}
public Registry register(Class resourceClass,
ResourceEncoder encoder) {
resourceEncoderRegistry.add(resourceClass, encoder);
return this;
}
public Registry register(DataRewinder.Factory factory) {
dataRewinderRegistry.register(factory);
return this;
}
public Registry register(Class resourceClass,
Class transcodeClass, ResourceTranscoder transcoder) {
transcoderRegistry.register(resourceClass, transcodeClass, transcoder);
return this;
}
public Registry append(Class modelClass, Class dataClass,
ModelLoaderFactory factory) {
modelLoaderRegistry.append(modelClass, dataClass, factory);
return this;
}
...
会发现源码中用了一个单词来作为泛型的名称:Model、Data、TResource,想必此中大有深意。
我也不卖关子了,直接看:
而Factory、ModelLoaderFactory、Encoder、Decoder、Transcoder则是串联上面三种类型的桥梁:
用图片展示可能容易理解各部分的作用:
明白了上图中数据的流转过程,再结合上面Glide构造函数的代码,就不难理解Glide如何自动识别图片源,如何自动解码图片,以及如何用Glide自定义图片的下载、加载、解码、编码(保存)过程了。回过头再去看第三节中使用ModelLoader实现自定义网络请求的方式也更容易理解了。
了解了上面的原理以后,就知道如何结合Glide和android-gif-drawable了:
用GifImageView替换的ImageView来展示图片;
android-gif-drawable有自己的GifDrawable(A),与Glide的GifDrawable(B)不一样,所以我们需要注册一个从Data到GifDrawable(A)转换的Decoder;
因为GifDrawable(A)默认的getConstantState()方法是返回空,但是Glide中需要用到这个方法,所以需要自定义ChatGifDrawable继承GifDrawable(A),并实现这个方法;
如果需要Gif图的文件缓存,实现一个Encoder;
注册这些类,让Glide自动完成其他的转换。
Glide.getRegistry()
.prepend(InputStream.class, ChatGifDrawable.class,
new StreamGifDrawableResourceDecoder(sGlide.getRegistry().getImageHeaderParsers(),
sGlide.getArrayPool()));
Glide.getRegistry()
.prepend(ByteBuffer.class, ChatGifDrawable.class,
new ByteBufferGifDrawableResourceDecoder(
sGlide.getRegistry().getImageHeaderParsers(), Glide.getArrayPool()));
Glide.getRegistry().register(GifDrawable.class, new ChatGifDrawableEncoder())
至于具体的ChatGifDrawable、StreamGifDrawableResourceDecoder、ByteBufferGifDrawableResourceDecoder和ChatGifDrawableEncoder的代码实现就不贴了,反正IM中也已经删掉了……