使用Glide替换Picasso经验小结

1,800 阅读11分钟

0、 背景

最近的工作是做一个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的使用方式

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,可以自动识别图片类型,完成加载。

二、Glide替换Picasso的注意事项

我们用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图时可能不正确,会变得很小,跟布局有关,调整一下布局的属性设置。

三、 Glide如何实现加载网络图片时校验请求

也就是如何实现上文中的“需求1”,有两种方法:

  1. 使用ModelLoader,自定义AppGlideModule,在其中对Glide进行定制,除了定制网络请求,还可以定制缓存策略等,这是官方提供的方法使用ModelLoader
@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());
  }
}

是不是觉得怪怪的,感觉云里雾里,没关系,看完本文你就懂了。

  1. 使用GlideUrl包装一下网络请求的url:
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)....;

四、 Glide4.0版的定制方式

我们都知道,使用图片库,很重要的一步是对其进行定制,并不是要重写代码,而是根据图片库提供的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()));
  }
}
  1. 然后在 AndroidManifest.xml 去申明你写的 GlideModule:
<meta-data
    android:name="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<GlideModule> 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的对象,以此达到定制的目的,代码很简单,就不贴了。

五、IM接入Glide过程中踩的坑

请看流水账:

  1. 首先遇到的问题是普通图片(非Gif图)加载不流畅,想到的解决方法是定制Glide,设置更大的内存缓存空间,果然成功解决;
  2. 解决了普通图片加载不流畅,结果发现Gif图加载也不流畅,百度、谷歌、StackOverflow后,结果直指Glide的Gif图加载效率低下,我误以为真,于是尝试改用Glide结合android-gif-drawable的实现方式:Glide用于下载、缓存,android-gif-drawable用于显示。
  3. Glide结合android-gif-drawable,下一节会单独介绍如何实现。总之好不容易实现了之后,发现……还是卡顿,吐血一升;
  4. 重新找原因……各种方法都失败以后,偶然间尝试把Glide.with(context).pauseRequests()删掉,发现流畅无比,终于找到了原因,得到的教训也就是前文第二节的第二点注意事项;
  5. 删掉Glide结合android-gif-drawable的逻辑,恢复Glide单独加载,发现仍然流畅无比,虽然Glide显示Gif还是有个缺点:同一个Gif图源的显示的动画是同步的,不过也属于可接受范围,搞定收工。得到的教训是:百度、谷歌、StackOverflow上的老旧信息也不能全信,Glide在不断进步,显示效率已经很高了。

六、Glide结合android-gif-drawable

先说点理论知识

默认情况下,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<GifDecoder>())
        .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<File>())
        /* 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);
  }

看完代码会发现好像有规律:

  1. 参数中有很多byte[].class,InputStream.class,ByteBuffer.class,File.class,int.class,Integer.class,String.class, Uri.class,URL.class看起来好像是可以指示图片的来源的类型
  2. 参数中有几个Bitmap.class,GifDrawable.class好像是用于指示图片的类型的
  3. 参数中还有很多Factory、Encoder、Decoder、Transcoder字样的类型

带着疑问再来看看Registy的源码:

...
  public <Data> Registry register(Class<Data> dataClass, Encoder<Data> encoder) {
    encoderRegistry.add(dataClass, encoder);
    return this;
  }
  public <TResource> Registry register(Class<TResource> resourceClass,
      ResourceEncoder<TResource> encoder) {
    resourceEncoderRegistry.add(resourceClass, encoder);
    return this;
  }
  public Registry register(DataRewinder.Factory factory) {
    dataRewinderRegistry.register(factory);
    return this;
  }
  public <TResource, Transcode> Registry register(Class<TResource> resourceClass,
      Class<Transcode> transcodeClass, ResourceTranscoder<TResource, Transcode> transcoder) {
    transcoderRegistry.register(resourceClass, transcodeClass, transcoder);
    return this;
  }
  public <Model, Data> Registry append(Class<Model> modelClass, Class<Data> dataClass,
      ModelLoaderFactory<Model, Data> factory) {
    modelLoaderRegistry.append(modelClass, dataClass, factory);
    return this;
  }
...

会发现源码中用了一个单词来作为泛型的名称:Model、Data、TResource,想必此中大有深意。 我也不卖关子了,直接看:

  • Model:指的是图片的来源的类型,比如File、Uri、Url,String,byte[]等;
  • Data:指的是从图片远中读取的数据的类型,比如InputStream、ByteBuffer等;
  • TResource:指的是数据解析出来的类型,比如Bitmap,GifDrawable等。

而Factory、ModelLoaderFactory、Encoder、Decoder、Transcoder则是串联上面三种类型的桥梁:

  • Factory是工厂类,用于生成Data类型的实例对象;
  • ModelLoaderFactory的原型是泛型接口ModelLoader<Model, Data>,从泛型参数名也能猜测出来,它用于从Model到Data的转换;
  • Decoder用于解析,是从Data到TResource的转换;
  • Encoder用于编码,是从TResource到Data的转换,一般用于将图片保存到文件缓存中;
  • Transcoder用于多种TResource之间的转换,比如从Bitmap转换成BitmapDrawable。

用图片展示可能容易理解各部分的作用:

Glide数据流程图

明白了上图中数据的流转过程,再结合上面Glide构造函数的代码,就不难理解Glide如何自动识别图片源,如何自动解码图片,以及如何用Glide自定义图片的下载、加载、解码、编码(保存)过程了。回过头再去看第三节中使用ModelLoader实现自定义网络请求的方式也更容易理解了。

那么具体如何结合Glide和android-gif-drawable呢

了解了上面的原理以后,就知道如何结合Glide和android-gif-drawable了:

  1. 用GifImageView替换的ImageView来展示图片;

  2. android-gif-drawable有自己的GifDrawable(A),与Glide的GifDrawable(B)不一样,所以我们需要注册一个从Data到GifDrawable(A)转换的Decoder;

  3. 因为GifDrawable(A)默认的getConstantState()方法是返回空,但是Glide中需要用到这个方法,所以需要自定义ChatGifDrawable继承GifDrawable(A),并实现这个方法;

  4. 如果需要Gif图的文件缓存,实现一个Encoder;

  5. 注册这些类,让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中也已经删掉了……

结语

通过这一篇又臭又长的流水账,希望能帮助大家更加了解Glide的原理和使用方式,避免以后在使用中再遇到类似的坑了!