记一次实时视频滤镜的内存调优

Google I/O 2017 大会上,Android Studio 3.0 Canary 版本发布。据说 Profiler 直接重写,所以下下来玩玩看。首先拿公司的项目 Meet 开刀,打开 Meet,开启 Android Monitor。不看不知道,一看吓一跳,在 AS3.0 下看 Meet 的内存占用简直吓死人。

Graphics Memory

点开 Memory 一栏,内存的分类比以往版本详尽许多。同时也有一部分内存,在老版本的 Android Studio 上并没有展示出来,其中最重要的一栏就是 Graphics Memory。

此时在网上并没有查到有关 Graphics Memory 到底是什么的说明,所以只能先通过实践来看看它到底是哪一部分的内存占用了。Graphics Memory,从字面上的意思来理解,便是图形相关的内存占用。马上想到项目中的滤镜是在 Java 层使用的 OpenGL,那么也很有可能 Graphics Memory 就是 OpenGL 相关操作带来的内存消耗。

为了佐证我的这一观点,随手就在项目里多生成了一大堆 FrameBuffer。果不其然,Graphics 一栏蹭蹭蹭地往上涨。基本上就可以证明,如此庞大的 Graphics Memory 占用,主要来自于项目中的滤镜模块。

自己实现的滤镜库

为了更方便地接入 Meet 项目中的其他模块,滤镜模块并没有使用当前比较流行的 GPUImage 库,而是我自己写了一个带有类似功能的库(实际上更强劲一点)。当时做出这样的选择,主要是便于跟项目其他模块对接,而且 GPUImage 中的大量滤镜效果在我们项目中也并没有什么需求,而且 GPUImage 仅仅支持 Rgb。

这个自己写的滤镜库,支持 Oes、Rgb、Yuv 三种纹理的绘制方法(GPUImage 只有Rgb...(啧啧啧...逃))。而且如 GPUImage 一样,不仅仅支持普通的滤镜效果,更是支持多种滤镜效果的叠加。也正是因为这个滤镜效果叠加的功能,使得在此处,必须要使用到 FrameBuffer 来实现滤镜效果的叠加。

内存优化点

对于一个滤镜,如果是由 N 个滤镜效果叠加起来的,那么就需要创建 N-1 个 FrameBuffer。考虑极端情况,如果有1000个滤镜效果进行叠加,那内存绝壁爆了。

那么我们第一点需要考虑的就是:

1. FrameBuffer 的复用:N个变2个

在第 X 个滤镜效果绘制完毕后,其所对应的上一个 FrameBuffer 实际上就已经失去了用处。而一个 FrameBuffer 所占用的内存是比较巨大的,因为其保存着屏幕分辨率大小的图片数据。所以我想到:如果能够在单一的滤镜下多个滤镜效果中复用 FrameBuffer,那么对于内存是会有较大帮助的。

所以想到了循环使用 FrameBuffer:对于单一滤镜的多个滤镜效果,仅仅使用两个 FrameBuffer,交替进行绘制。这样的话,N 个 FrameBuffer 就被优化成了 2个 FrameBuffer。

理论上来讲,这样做内存几乎降了一个数量级。但在实际的测试上却遇到了问题:

1.1 复用 FrameBuffer 所带来的奇葩问题

由于 Android 机的碎片化,各大厂商会对 ROM 进行深度定制,其所对于 OpenGL ES 的支持也都不太一样。所以经常会遇到一些“五星级神机”,在滤镜效果上出现各种奇奇怪怪的问题。

由于上面我们采用了 2 个 FrameBuffer 轮流绘制的机制,QA 手中一部分的 “五星级神机” 出现了网状黑线的情况而滤镜效果却没有任何影响。这个问题困扰了很久,怀疑是脏数据还没有来得及清理,就又被画了上去,但无论怎样 Clean 都没用。

于是退而求其次—将 FrameBuffer 由2个变成3个。

2. FrameBuffer 的复用:2个改3个

为了解决上述所遇到的奇葩问题,我们采用了3个 FrameBuffer 进行轮流绘制。经过 QA 同学的严格测试,这样既减少了内存占用,又不会出现奇怪的黑色网格。通过观察 Android Studio 3.0 Canary 上的 Profiler,无论是使用多少个滤镜效果叠加,其所占用的内存均不会超过3个 FrameBuffer 所占用的内存。

3. FrameBuffer 的复用:全局复用

像我们这种音视频类的项目,实际上是会存在频繁的滤镜切换的。爱美之心人皆有之,找到适合自己风格的滤镜进行视频聊天,也是需要不断地尝试的。

但在切换滤镜时,项目中以前的做法是,不同的滤镜都会使用不同的 FrameBuffer。那么如果从滤镜 A 切换至滤镜 B,A 所使用的 FrameBuffer 们有两条路:要么销毁,要么就留在那等待下一次切换至 A 效果后继续使用。而 B 的 FrameBuffer 们则会被创建或者利用起来。

较优的方案必然是在切换滤镜时,及时销毁不用的 FrameBuffer 们,并为切换后的滤镜创建新的 FrameBuffer 们。虽然在内存上来看,并没有什么浪费。但对于切换的成本来说,并不是最优的。这样做,频繁切换滤镜就会造成一定程度上的内存抖动。同时也会消耗一定时间去销毁以及创建 FrameBuffer。

那么最优的方案应该是:全局复用 FrameBuffer

即无论是否切换滤镜,其所使用的 FrameBuffer 们均为同一群。这样既不会有创建和销毁 FrameBuffer 的开销,又能避免一定程度上的内存都懂。还有一个好处是,就算切换滤镜时忘了销毁 FrameBuffer,也不会增加内存空间的占用。

灵感来自于 iOS 版 GPUImage 作者的博客: http://www.sunsetlakesoftware.com/2014/03/17/switching-gpuimage-use-cached-framebuffers

后记

Android 版的 GPUImage 的确没有 iOS 版的写的好,无论从性能还是从内存上,都略逊一筹。在看了一部分 iOS 版 GPUImage 之后,打算后期有空把其上面拥有的优点也移至我们自己的滤镜库中。

感谢同事们允许我多占用一些需求时间,来自己写滤镜库及内存调优。

本人对于 OpenGL 相关的知识并不是很系统,如有什么地方写错,烦请赐教!Thanks