使用Glide替换Picasso经验小结

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的原理和使用方式,避免以后在使用中再遇到类似的坑了!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,398评论 25 707
  • 这么久以来虽然经常用到一些图库,但是自己从来没有真正整理过我们使用过的这些东西有什么不同点,我们为什么要选择这个图...
    黑石ZB阅读 3,708评论 1 16
  • Glide 是一个 android 平台上的快速和高效的开源的多媒体资源管理库,提供 多媒体文件的压缩,内存和磁盘...
    帅气的欧巴阅读 2,762评论 1 18
  • 在一个 Gradle 项目中在你的 build.gradle中添加下面这行代码: 从一个 URL 中加载图片就像 ...
    A_Coder阅读 1,964评论 0 4
  • 是你筑好高楼 立在我身后 年轻的我 如何能够 此生凉苦 无意风稠 怎堪回望 心事独守
    跳豆豆的梦阅读 312评论 0 1