背景
在电商类 APP 里,图片到现在为止仍然是最重要的信息承载媒介,不得不说逛淘宝的过程,其实就是一个看图片的过程。而商品详情页中的图片,通常是页面中内存占用最多的内容,占用了整个页面内存的超过 50%。闲鱼在 Flutter 化的过程中,选择了商品详情页作为第一个落地的场景。通过多版本的迭代完善,基于 Flutter 的详情页已经在闲鱼稳定运行。然而正因为详情页的图片量大,导致 Flutter 里图片相关的问题一直挥之不去。
1:内存问题 — 连续 push flutter 界面内存累积
2:安装包问题 — 过渡时期两份重复资源文件。
3:寻址缓存问题 — 原有的寻址缓存策略无法复用。
4:图片复用问题 — Native 和 Flutter 重复下载相同图片。
解决方案 —-FXTexImage_V1
为了解决这些问题,我们尝试着寻找一种新的思路,一种能够将 flutter 与 native 串联起来的思路。而之前做视频播放器的方案给了我们启发。
熟悉 Flutter 的同学应该都知道,Flutter 的视频组件是基于一个 Flutter 提供的一个叫“外接纹理”的技术实现的,关于 flutter 外接纹理,本人另外有一篇文章有更详细的论述,这里不再赘述。https://mp.weixin.qq.com/s/KkCsBvnRayvpXdI35J3fnw
我们将每一张图片假想成一个:静态的视频。图片的内容由一个 external_texture 来负责显示,而这个 external_texture 则由 native 端提供具体的渲染数据。
通过这种方案,我们便可以通过 external_texture 这座桥梁,将 flutter 作为 native 端图片的一个最终展示场所。而所有的下载、缓存、裁剪等逻辑都可以复用原来的 native 图片库。
基于这个基本框架,我们形成了我们第一版本的图片渲染组件:FXTexImage—-V1。这个组件很好的解决了 Flutter 引入的安装包、图片缓存、图片复用等问题。
但是图片最大的问题:内存问题,并没有得到解决。
内存优化 —-FXTexImage_V2
为了用户体验,通常会有连续 push 若干个界面的场景(比如闲鱼的详情页,点击底部的推荐列表,可以一直往下 push 新的详情页),这种场景下,每一个界面都有大量的图片展示。所以在引入 flutter 以后,闲鱼在 iPhone 6P 等机型上通常只能 push10 个左右详情页就挂了。
在考虑到在显示过程中,真正用户可见的页面,其实只有当前栈顶的两个页面,基于这个特征我们就做了优化逻辑:
1:在 push 详情页过程中,我们只保留了当前展示页和当前页的前一页的图片资源,而之前的资源全部都做了释放(只是图片资源的释放,整个页面还有页面中的其他元素还是做了保留)。
2:为了做到用户无感知,我们在 pop 过程中,会预先去加载当前界面下一个界面的图片资源。
通过这种方式,理论上我们可以释放掉不可见的资源,从而保证在持续 Push 界面过程中内存缓慢增长,但是实践过程中发现内存仍然持续增长。
经过排查,我们发现 flutter 1.0 版本以及 0.8.2 版本里,SurfaceTextureRegistry 提供了 release 方法,这里将会把创建的 SurfaceTexture 进行释放。然而测试过程中发现,单单对 SurfaceTexture 释放,并没有完全释放内存,当反复创建对象时仍然会闪退。为此,我们在 AndroidExternalTextureGL 的析构函数中增加了纹理的释放 glDeleteTextures 逻辑。
然而,AndroidExternalTextureGL 的析构是在 flutter 的 GPU 线程调用的,而 external_texture 的 release 方法通常是在主线程,也就是 PlatForm 线程调用的。不同线程调用的问题就是会导致一个诡异的问题:
推测是不同线程释放的逻辑影响了 GL 环境,导致文字渲染出了问题。
所以,为了解决该问题,我们删除了 SurfaceTextureRegistry 的 release 方法里面 SufaceTexture 的释放逻辑,并且将 SurfaceTexture 的释放,放到 AndroidExternalTextureGL 析构阶段,通过 Jni 调用 java 方法实现资源释放。
AndroidExternalTextureGL::~AndroidExternalTextureGL(){
if (state_ == AttachmentState::attached) {
Detach();
if (texture_name_ != 0)
{
glDeleteTextures(1, &texture_name_);
texture_name_ = 0;
}
}
Release();
state_ = AttachmentState::detached;
}
void AndroidExternalTextureGL::Release() {
JNIEnv* env = fml::jni::AttachCurrentThread();
fml::jni::ScopedJavaLocalRef<jobject> surfaceTexture =
surface_texture_.get(env);
if (!surfaceTexture.is_null()) {
SurfaceTextureRelease(env, surfaceTexture.obj());
}
}
void SurfaceTextureRelease(JNIEnv* env, jobject obj) {
env->CallVoidMethod(obj, g_release_method);
FML_CHECK(CheckException(env));
}
g_release_method = env->GetMethodID(g_surface_texture_class->obj(), “release”, “()V”);
CPU 优化 —-FXTexImage_V3
通过外界纹理渲染图片 + 不可见页面资源释放,我们解决了上述提出的一系列问题,但是又引入了新的问题:CPU 偏高,滑动帧率偏低。通过测试,在详情页滑动过程中,IOS 和 Android 的 CPU 都比 Flutter 原生组件高 10% 以上,这个显然无法应用。
经过排查,发现 CPU 高的原因是:
IOS 端:iOS 的 IOSExternalTextureGL 模型是一个拉数据的模型,native 端 register 一个 CVPixelBuffer 的生产者,当需要绘制时,都会调用一次这个生产者的 copyPixelbuffer 方法去拉一次数据。然后将拉到的 CVPixelBuffer 对象转换成 GPU Texture。这里每一次转换都换造成 CPU 较大开销。
并且这种拉数据的机制就要求这个生产者的必须一直保留着这个 CVPixelBuffer 对象(否则界面重刷以后,图片区域就显示白屏)。
Android 端:android 的数据存储在 SurfaceTexture 中。每一次 external_texture layer 需要绘制时候都会从 SurfaceTexture 中去 update 数据到纹理中,由于 SurfaceTexture 使用基于 EGLImage 共享内存,所以虽然没有双份内存的问题,但是每一次 update 都会带来较大的 CPU 开销。
在之前外接纹理的文章中,我们提出了一种新的基于共享上下文的外接纹理方案。并在我们视频的拍摄和编辑中得到了很好的应用。该方案当初提出来,是为了解决视频数据从 CPU -> GPU -> CPU -> GPU 输送的问题而提出来的。
但是在图片这个场景下,新的外接纹理方案下,一张图片在 native 端加载完成以后,立刻被转换成一个 OpenGL 的 Texture,然后图片的资源马上被释放。当界面刷新时,对于同一张图片的重新渲染,IOSExternalTextureGL 不需要再去做将数据(CVPixelBuffer 或者 SurfaceTexture) 转换到 Texture 的逻辑,而是直接使用之前创建好的 Texture。
经过这一步优化,我们很好的限制了 iOS 的 CPU 和内存,Android 的 CPU。
通过测试对比,V3 版本的图片组件,相比于 Flutter 原生图片组件,在详情页正常滑动操作过程中,平均 CPU 高出 3% 左右,虽然仍差于原生组件,单相对是可以接受的。
结果
内存:基于新图片组件,我们很好的限制住了连续 push 下的内存增长速度,顺利的将 iPhone 6P 上的详情页 push 最大数量从 10 个增加到了 30 个以上。
在同一线上版本中,我们通过控制 ABTest 开关,测试新的图片渲染方案和 Flutter 自带图片组件方案的 Abort 率,发现新图片组件下闲鱼的 Abort 率降低 20%。
安装包:新组件下,所有的资源组件与原来 native 资源共用,所有 flutter 期间新引入的资源出了 gif 图,全部删除,减少安装包 900k+。并且后续可以不用继续新增。
寻址策略: 复用 native 图片组件,基于阿里系自己的图片下载组件,这样可以做到随着集团组件升级版本,兼容版本过程中各种新的寻址方式和图片格式。
图片复用:复用 native 图片组件,当图片地址命中缓存,可直接缓存加载,尺寸不一致时可以预先返回缓存图同时加载大图,这样大大增强详情页大图预览的浏览体验。
遗留问题
图片组件已经在闲鱼上全量部署,然而还是有一些问题没有得到很好的解决,上文提到过 CPU 比原生图片组件高 3% 左右,虽然用户没有感官体验,但是还是有优化空间。
还有就是 Flutter 针对 ExternalTexture 的纹理渲染时没有开启抗锯齿,导致小图在大区域渲染时比原生组件效果要差。这里还需要继续排查原因。
最后,FXTexImage 组件还在持续优化中,当解决上述遗留问题以后便会在 Github 上开源。
本文作者:闲鱼技术 - 炉军阅读原文
本文为云栖社区原创内容,未经允许不得转载。