背景:针对APP中大量的GIF播放场景进行优化,指标以内存占用流畅性为主。

目前Anroid上比较流行的GIF播放方案有Glide和android-gif-drawable,下面我们逐一进行分析,以及如何对现有方案进行优化。

android-gif-drawable

基本使用

链接:https://github.com/koral–/android-gif-drawable

这个是比较流行的GIF播放方案了,它会提供一个创建GifDrawable的方法,我们使用GifDrawable的方案就可以播放GIF了。

GifDrawable的构造器

但是有一点需要注意GifDrawable的内存分配是用malloc()完成的,并且如果使用文件创建的GifDrawbale(很常见)的话,文件也是需要手动关闭的。

所以,GifDrawble需要在View的onDetchToWidow获取Activity的onDestory的时候调用recycle()释放资源,否则会造成内存和FD泄漏。

原理分析

下面简单看下GifDrawable的原理 。 GifDrawable是绘制是基于ANativeWindow机制,通过ANativeWindow不断修改GifDrable中的Bitmap,达到GIF播放的效果。

GifDrawable的初始化 每个GifDrawble在构造器中都会初始化一个GifInfoHandle的对象,这个对象保存了Native层GifInfo的地址,以后我们的每次操作,需要这个GifInfo的地址。

同时GifInfo对象记录了当前的Gif文件信息和读取方法。

我们以打开文件为例

我们看下fileRead的实现

其实DGifOpen的参数是一个函数指针,有点类似于Java中的接口。

也就是说,如果我们使用InputStream或者ByteBuffer创建GifDrawable,会有不同的读取方式。

如果我们使用的byte[],或者ByteBuffer创建的GifDrawble,那么InputFunc的参数分别为

核心播放逻辑

每次绘制的时候,都会去通过这个方法数据中去获取这一帧的数据。

DDGifSlurp是Gif解码的核心逻辑,里面涉及到解析Extension,颜色表之类的数据,内容比较复杂,并且需要些GIF文件的知识,暂时先分析到这里。

这里我们可以看到GifDrawable的核心逻辑是使用在Native层创建GifInfo对象,然后不断从GIF文件中获取下一帧的数据,这个和视频的播放很像。

由此可以看出,构造GifDrawable的最好方式是使用文件或者InputStream,如果使用ByteBuffer或者byte[]的话,整个Gif文件必须存在在内存里,会占用较大的内存。使用文件或者输入流可以保证内存里只存在一帧和共享区域的数据。

Glide

Glide是Android开发中非常常见的图片加载库,在Glide中,集成了播放Gif的功能,Glide在解码资源的时候会根据文件头进行判断,这样省去了我们代码中判断文件是否是Gif的逻辑。

原理分析

Glide 在调用构造器的时候加入了Gif的ResourceDecoder,这个ResourceDecoder会先于BitmapResourceDecoder执行,如果判断到文件头是Gif ,那么就会当做Gif进行处理。

下面我们看下代码验证下这个结论。

Registry的构造器

之所以需要注册这么多组件,这个跟Glide的加载逻辑有关,简单介绍下Glide获取数据的顺序。

这个时序图是从DecodeJob加载网络数据的流程,DecodeJob的Glide的核心逻辑,主要负责从网络或者磁盘缓存中读取图片。

  1. 如果需要从网络上获取图片,那么图片会被成DataFetcer解析成InputStream,ButeBuffer之类的类型,如果该次请求支持磁盘缓存的话,那么数据会被写入DiskCache,然后开始下一步。

  2. Glide根据加载出来的数据类型InputStream,ByteBuffer,File等,和要解析成的数据类型Bitmap,Drawable,BitmapDrawable构造LoadPath,然后根据LoadPath寻找ResourceDecoder解码成需要的类型(RequestBuilder的类型)。

  3. RequestBuilder的默认类型是Drawable,即没有调用as(xxx.class)更改RequestBuilder的类型,会先由GifStreamDecoder判断是否是Gif文件,如果是Gif,那么处理,否则交由其他LoadPath的ResourceDecoder进行处理。

整体加载流程如此,我们看下GifStreamEncoder这块的逻辑,看下如何对GIF进行处理的。

这个handles表示是否需要处理这个数据,可以看出主要是根据Flag和文件头判断的,如果是GIF,那么就使用这个Decoder进行解码。

如果handles,返回true了,那么接下来会回调decode方法,这个decode会将Data类型解析成对应的Resource类型,即Resource<GifDrawable>

最终会调用到GifByteBufferDecoder类的decode(),我们看下实现

可以看出,上面创建了GifDrawable对象,并且从ByteBuffer中获取了首帧。那Gif是如何动起来的呢?

我们看到ImageViewTarget中有这么一段逻辑。

GifDrawble当然实现了这个接口,我们看下start()的相关实现

start()最终会调用到GifFrameLoader的start(),图就不画了,逻辑也不是很多。

最终看到,还是通过Glide去加载了下一帧,最后还回去走DecodeJob那一套逻辑。只不过model换成了GifDecoder。

最后一行又通过Glide进行加载,不过Model变成了GifDecoder,Target变成了DelayTraget,ResourceType是Bitmap。

GifFrameLoader加载完一帧后,会调用onFrameReady()通知GifDrawable重绘,这样GifDrawable会调用draw()方法重新渲染Bitmap。

最后用一张时序图总结下上面的流程。

方案比较

Glide的优劣

Glide中ModelLoader机制会将图片数据解析成ByteBuffer,如果这个GIF比较大的话,就比较费内存了, 但是依赖于BitmapPools机制,不会创建大量临时对象。

同时Glide有优秀的内存缓存和文件缓存,可以复用已经创建过的GifDrawable对象,不需要重新再解码。

android-gif-drawable的优劣

android-gif-drawable可以从文件中逐帧读取GIF,这个在GIF文件巨大时特别有用, 这在内存中保留当前帧和公共区域的数据,而非全部GIF数据。

android-gif-drawable需要自己去做缓存机制,并且需要在何时的时候调用recycle()防止内存泄漏。

经过测试android-gif-drawable的GIF播放质量优于Glide。

验证结论

使用同样的手机,播放了一个40MB的GIF文件,查看内存占用情况。

GifDrawable播放内存占用20多MB,Glide播放占用60多MB,可以看出Glide确实是将整个GIF加载到内存里了。大家可以自行试下。

结合两种方案进行优化

目前打算将Glide和GifDrawble相结合,GifDrawable可以使用Glide的内存和文件缓存,同时可以利用Glide的生命周期避免内存泄漏。

其实就是实现如下调用:

或者能自动判断文件头,如果是GIF的话 ,直接创建GIFDrawable实例,不是的话使用Glide加载。 当然得设置个Flag,如果有的话,才这样加载。

实现

1. 构建LoadPath

Glide的解码核心逻辑是构建LoadPath,我们只要定义自己的LoadPath,放在Glide的默认方案之前就行。

这里使用的GifDrawable的构造器是File,即我们需要通过文件创建这个GifDrawable,其他使用ByteBuffer和InputStream也可以,但是效果不好。

但是我们这个File是临时的,Glide播放其他Gif的时候,这个文件可以拿来复用。

所以我们定义这个类为FileBridge,为File的包装类。

由此而知,我们的LoadPath为ByteBuffer->FileBridge->pl.droidsonroids.gif.GifDrawable。

先定义一个InputStream到FileBridge的Decoder。

当然也需要支持ByteBuffer。

这两个ResourceDecoder都将Glide的数据类型转为了FileBridge,下面我们直接使用FileBridge创建GifDrawable即可。

定义一个Transcoder。

现在我们定义的LoadPath已经完成了,我们把它们注册到Glide里面。

这样就已经完成了,之前我们提到的内存泄漏问题也需要处理下,只需要实现Resource的onRecycle方法即可。

FileBridge不使用时,也需要回收

这样就完成了GifDrawable和Glide的结合,这样目前看起来是个不错的方式,Glide里面缓存能给GifDrawable用,GifDrawable能 利用Glide的生命周期避免内存泄漏。并且能够减少内存占用,优化GIF播放质量。

3 对 “Android中Gif播放优化”的想法;

  1. 你好,请问演示代码中GlideOptions.USE_PL_GIF_IF_NEEDED,这个选项,是通过@GlideOptions这个annotation生成的吗?还是通过别的配置手段生成的?一直没看明白这段代码,求大神解释下!!

    1. 不是的,这个USE_PL_GIF_IF_NEEDED就是一个常量,加在哪里都可以。

      作用是在自定义的ResourceDecoder中进行判断,如果有这个标记就走我们的Decode逻辑(我们的Decoder在Glide内置的ResourceDecoder之前注册)。

      自定义的Decoder会把ByteBuffer或者InputStream写入文件,提供给pl.droidsonroids.gif.GifDrawable初始化。

      如果需要完整代码我可以发你一份。

  2. 大神,我也不理解这个GlideOptions.USE_PL_GIF_IF_NEEDED,请问这个是什么类型的常量,谢谢,跪求源码!发我邮箱吧:tonyloveannie@163.com

发表评论

电子邮件地址不会被公开。 必填项已用*标注