【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

一个方案提升 Flutter 内存利用率

  • 2020-11-08
  • 本文字数:2702 字

    阅读完需:约 9 分钟

一个方案提升Flutter内存利用率

背景

我们闲鱼使用的图片方案是自研的外接纹理方案:


  • Android 侧创建 SurfaceTexture,通过 FlutterJNI 注册到 Flutter engine 里,最后返回 texture id 给 Flutter 应用层,应用层使用 Texture Widget 和 textue id 去显示图片纹理。

  • 纹理数据则是在 Android 侧,通过 OpenGL 将图片纹理写入到 SurfaceTexture,然后通过 Flutter engine 里的共享内存,将纹理数据传入到应用层,最终交给 Skia 渲染。



这里面存在的问题: Flutter 应用层的纹理数据没有缓存,每次都需要重新将 Bitmap 数据渲染成纹理,再交给 Flutter 应用层使用。Native 图片加载会内存缓存,Flutter 自身提供的图片库也存在缓存,这 2 个缓存相互隔离,占用很大的内存空间。而且 Flutter 图片缓存基本都是存放的本地资源图,而我们 Flutter 页面上大部分其实都是网络下载的外接纹理图片,导致缓存资源利用率很低。

分析

针对上述的 3 个问题,我们先抛开技术实现,假设下要解决这 3 个问题,最理想的一个解决方案是什么:


  • 纹理没有缓存,那我们在应用层增加一个纹理的内存缓存就解决了。

  • 当上层的应用层已经缓存纹理,那 Native 侧的 Bitmap 的内存缓存也可以被去掉,只保留图片资源的磁盘缓存。

  • 整个 App 的内存缓存,只有纹理缓存,Flutter 的 ImageCache 缓存,为了避免内存资源的浪费,将这 2 个缓存合成一个


所以最理想的解决方案:整个 App 内只存在一个内存缓存,并且它既能缓存纹理,也能缓存 Flutter 的 Image Widget 加载的图片数据。

解决方案

ImageCache 是官方提供的,我们没办法去掉,而且闲鱼 App 里也有一些地方使用 Image Widget。现在解决方案就变成: 将纹理数据也放到 ImageCache 里缓存。使用纹理时,先从 imageCache 里取。


我们先看下现有的 Flutter 图片加载逻辑,以及图片是如何缓存的



从图中可以看到,Flutter 的图片加载,都会调用 ImageCache.putIfAbsent 方法,通过该方法取缓存,没命中缓存则会使用传入有的 loader 方法,去构造对应的 ImageStreamCompleter,由 Completer 去完成图片加载的逻辑。


当命中缓存时,putIfAbsent 方法会直接返回 Completer,该对象里持有了 imageInfo,ImageWidget 直接拿 imageInfo 的 ui.Image 去渲染。

方案一:扩展 ImageCache,缓存纹理

ImageCache 对外提供取缓存方法就一个 putIfAbsent



一开始我们想的是按照该方法参数,构建对应的 key,loader,以及 ImageStreamCompleter,然后也使用 putIfAbsent 方法去取缓存。


尝试过后发现不行,如下图所示,当图片下载解码成功后,会回调这个 listener 方法,在该方法中,会将图片存放进 ImageCache 的缓存队列



这个 listener 回调有 2 个参数,ImageInfo 里面存放着图片数据 ui.Image。



我们应用层根本没办法去构造 ui.Image,因为该类是 Flutter engine 底层完成图片解码之后 set 到应用层的。应用层根本没办法去主动 set 值。这样就导致在 listener 里,无法计算出 imageSize 的值,自然也没办法存到缓存里。

方案二:自定义 ImageCache

因为 ImageCache 的缓存队列是私有的,只有 putIfAbsent 方法可以往里面存数据。那我们只有另外一条路,从 ImageCache 的源码入手,去自定义 imageCache,然后对其进行功能扩展。


将 ImageCache 替换成我们自定义的


因为 Flutter 提供的 ImageCache 没办法修改代码,所以我们直接把 ImageCache 的源码 copy 出来一份,继承 ImageCache,然后将 PaintingBinding 的 imageCache 替换成自定义的。



如图所示:Flutter 的 PaintingBinding 有暴露出 createImageCache 的方法,我们继承 WidgetsFlutterBinding,重写该方法返回我们自己的 ImageCache, 另外在这里还可以针对 ImageCache 的各种缓存大小做设置。


对 ImageCache 进行功能扩展


为了尽可能不修改 ImageCache 的代码,我们直接定义了新的缓存纹理的方法,对齐了 putIfAbsent 方法的逻辑,核心代码逻辑如下:




该方法主要是参考 putIfAbsent 的逻辑来实现的,为了将纹理也缓存进 ImageCache,主要做了以下几个关键扩展:


  1. TextureCacheKey 是唯一标识纹理的 key,该 key 是主要是根据宽高,url 来判断是否是同一个纹理的。

  2. TextureImageStreamCompleter 则是纹理的管理类,内部持有纹理数据和下载成功的回调。当命中缓存时,返回该对象给应用层,并从中拿到纹理 id 交给 Texture Widget 渲染

  3. 当没有命中缓存时,会调用传入的 loader 方法构造 TextureImageStreamCompleter,并且会执行纹理的加载逻辑。同时会构造一个 listener 方法回调,注册进去。

  4. 当纹理加载成功时,会执行 listener 方法回调,该方法里主要是计算纹理大小,将它放入缓存队列里,检查缓存大小是否超过最大值,超过则淘汰之前最久未使用的纹理。


这里要注意的一个点, 因为普通的图片是 dart 对象,会被 Dart VM 自动回收,但是我们的纹理对象真实的数据是在 Engine 的共享内存里,所以这里需要手动的管理纹理的释放,我们对纹理对了引用计数,只有当没有 widget 持有纹理时,引用计数为 0 时,才会真正的释放。


同理,上层 Texture Widget 在 dispose 时,也会调用下 ImageCache 提供的接口,看下当前使用的纹理是否被缓存或者正在被使用。只有否的时候才会真正的释放纹理。

效果

我们采用搜索结果页作为测试页面,该页面存在很多宝贝大图,以及各种重复的标签小图。使用华为荣耀 20 来测试优化前后的物理内存占用。


操作步骤是:打开 app,进入搜索结果页,搜索相同的关键字后进入搜索结果页,然后静默 10s 后滑动浏览 100 条数据,最后停止操作。期间每秒采样一次物理内存,一共持续 100s,得出如下的数据



蓝色曲线是优化前的内存占用,橘黄色曲线是优化后,进入时可以看到占用的内存基本一致。滑动时内存占用下降是因为触发了 GC 回收 App 的内存导致的。总体上看,优化后总的内存占用比优化前要少,因为 GC 导致的毛刺也比优化前要少。

展望

上述的方案虽然实现了一个 App 内一个内存缓存,并且将纹理和 Flutter 图片都存进去了,节省了内存空间,提高了内存使用率,但还是侵入了 ImageCache 源码,后续 flutter engine 的升级和代码维护,需要有额外的工作。


此外因为 Flutter 侧加载原生图片,都走的 putIfAbsent 方法,并且因为加载原生图片都走的原图加载,我们 app 内时不时存在着这种情况,一张图片可能会占用好几 M 的内存,所以我们直接在 putIfAbsent 加上了大图监控的方法,当发现加载的图片大小超过 2M 时,会进行数据上报,包括图片的 url,图片使用信息,图片大小等。通过该方式,我们发现了好几例图片使用不当的情况:直接使用 Image.network 加载原图,或者是 Image.asset 加载一张很大的本地资源。


本文转载自公众号闲鱼技术(ID:XYtech_Alibaba)。


原文链接


一个方案提升Flutter内存利用率


2020-11-08 10:002301

评论

发布
暂无评论
发现更多内容

云原生数据库极致弹性体验 - Amazon Aurora Serverless v2

亚马逊云科技 (Amazon Web Services)

数据库 云原生

干货|app自动化测试之Appium 原理 与 JsonWP 协议分析

霍格沃兹测试开发学社

干货|app自动化测试之Appium 源码分析

霍格沃兹测试开发学社

AI 时代的视频云转码移动端化——更快、更好、更低、更广

ZEGO即构

AI 音视频开发 视频云转码

瓴羊智能客服,基于钉钉重磅推出一体化的智能服务解决方案

瓴羊企业智能服务

干货|app自动化测试之Andriod WebView如何测试

霍格沃兹测试开发学社

软件测试 | 测试开发 | Jenkins job 机制该如何使用?

测吧(北京)科技有限公司

测试 测试工程师

The main application of radio technology in aerospace field/IPQ4019 IPQ4029 ,802.11AC 2x2 2.4G&5G

wallys-wifi6

IPQ4019 ipq4029

干货|app自动化测试之Capability 使用进阶

霍格沃兹测试开发学社

干货|app自动化测试之设备交互API详解

霍格沃兹测试开发学社

IDaaS 系统ArkID一账通内置插件:图形验证码认证因素的配置流程

龙归科技

单点登录 Idaas

干货|app自动化测试之模拟器控制

霍格沃兹测试开发学社

干货|接口测试必备技能-常见接口协议解析

霍格沃兹测试开发学社

技术分享 | 测试平台开发-前端开发之Vue.js 框架的使用

霍格沃兹测试开发学社

NFT拍卖交易系统开发NFT商城

薇電13242772558

NFT

Python图像处理丨带你认识图像量化处理及局部马赛克特效

华为云开发者联盟

人工智能 企业号九月金秋榜

干货|移动端App自动化之触屏操作自动化

霍格沃兹测试开发学社

持续交付-Blue Ocean 应用

霍格沃兹测试开发学社

软件测试 | 测试开发 | Jenkins 持续集成体系介绍

测吧(北京)科技有限公司

软件测试 测试

干货|app自动化之如何参数化用例

霍格沃兹测试开发学社

干货|app自动化测试之Appium问题分析及定位

霍格沃兹测试开发学社

性能测试实战 | 修改 JMeter 源码,定制化聚合压测报告

霍格沃兹测试开发学社

供应链管理是对产品流、信息流、资金流综合管理

水滴

供应链

技术分享 | Spring Boot 集成 Swagger

霍格沃兹测试开发学社

技术分享 | 测试平台开发-前端开发之Vue.js 框架

霍格沃兹测试开发学社

软件测试中的『草莓酱定律』

BY林子

敏捷测试 草莓酱定律 温伯格

软件测试 | 测试开发 | 测试人生 | 三十而立终圆大厂梦,测试开发开启新征程

测吧(北京)科技有限公司

软件测试 测试 测试开发

干货|app自动化测试之Appium 源码修改定制分析

霍格沃兹测试开发学社

快速上手 Pytest + Requests + Allure2 测试框架实战技能

霍格沃兹测试开发学社

技术分享 | Spring Boot 异常处理

霍格沃兹测试开发学社

软件测试 | 测试开发 | 测试人生 | 97年双非学历的小哥哥,2线城市涨薪100%,我酸了......

测吧(北京)科技有限公司

软件测试 测试

一个方案提升Flutter内存利用率_大前端_靖书_InfoQ精选文章