《泛智能开启下一代云时代》白皮书来了!点击下载 了解详情
写点什么

一个方案提升 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:002173

评论

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

文盘Rust -- 本地库引发的依赖冲突

京东科技开发者

后端 Clickhouse 本地计算 rust语言 企业号 2 月 PK 榜

ChatGPT风口下的技术“狂飙”,天翼云荣登ZeroCLUE榜首

天翼云开发者社区

令人头秃的js隐式转换面试题,你能做对吗

loveX001

JavaScript

面试官:你是怎样进行react组件代码复用的

beifeng1996

React

有爱相伴,宠爱有家,皮皮App发起关爱流浪动物主题公益活动

联营汇聚

瓴羊Quick BI为企业决策者提供可视化分析服务

小偏执o

面试了20+前端大厂,整理出的面试题

loveX001

JavaScript

百度前端必会react面试题总结

beifeng1996

React

构建云边端一体的分布式云架构,软硬结合驱动边缘计算创新场景

百度开发者中心

云原生 边缘计算 #百度智能云#

擅用瓴羊Quick BI报表分析工具,数据分析事半功倍

夏日星河

基于飞桨PaddleClas完成半导体晶圆图谱缺陷种类识别

飞桨PaddlePaddle

paddle 开源 飞桨

Led透明显示屏的发展超乎你想象

Dylan

LED 显示器 LED显示屏

unittest中使用ddt后生成的测试报告名称如何修改?(如test_api_0修改成test_api_0_titile)

单元测试 自动化测试 unittest 参数化 ddt

万亿级对象存储的元数据系统架构设计和实践

百度开发者中心

对象存储 文件存储 百度沧海

瓴羊Quick BI即席分析工具:创设数据分析捷径

巷子

最初设计时就会避开钽电容,这是为什么呢?三大理由告诉你原因

元器件秋姐

元器件 电容 钽电容

假如面试官问你Babel的原理该怎么回答

loveX001

JavaScript

BeyondCampus-护航高校网络安全

权说安全

网络安全 零信任

目前兰州市等保测评机构有几家?有新增的吗?

行云管家

等保 等级保护 等保测评 兰州

LR性能测试常见问题及处理方法(二)

性能测试 问题排查 LoadRunner

关于微服务架构的思考

HummerCloud

微服务 云原生

疑似45亿条递信息泄露,“三类主体”如何应对?

极盾科技

数据安全

Java 集合中的排序算法浅析

京东科技开发者

jdk 后端 Java、 排序算法 企业号 2 月 PK 榜

面试官:说说React-SSR的原理

beifeng1996

React

【FAQ】集成分析服务的常见问题及解决方案

HMS Core

HMS Core

谈谈Linux内核的噪声

统信软件

Linux 内核

ModStartBlog v6.7.0 后台管理优化,页面宽度调整

ModStart

腾讯前端必会面试题(必备)

loveX001

JavaScript

Unittest接口测试生成报告和日志方法

日志 单元测试 自动化测试 unittest 测试报告

堡垒机行业标杆产品是哪家呢?有哪些功能?

行云管家

网络安全 信息安全 等保 堡垒机

LR性能测试常见问题及处理方法(一)

性能测试 问题排查 LoadRunner

  • 扫码添加小助手
    领取最新资料包
一个方案提升Flutter内存利用率_大前端_靖书_InfoQ精选文章