中国卓越技术团队访谈录读者调查,2022年采访嘉宾由你决定! 了解详情
写点什么

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

评论

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

架构师训练营-第十二周作业

Geek_a327d3

架构师训练营 week13 - 学习总结

devfan

普通上班族如何快速买房买车,一个程序员摸索的实操经验分享

陆陆通通

程序员 副业 认知

区块链赋能数字经济,为知识和版权确权定价

CECBC

区块链 知识产权 数字经济

架构师课程第十三周总结

dongge

打破Scrum的五个误区(译)

Bruce Talk

Scrum 敏捷开发 Agile

第十三周作业

Linuxer

Linux Shell编程

yuanhang

Shell

week13 总结

雪涛公子

架构师训练营第十三周作业

叮叮董董

大数据解答(二)

dony.zhang

数据分析

【第十三周】命题作业——Google 搜索排序

赵龙

java快速开发平台功能特点之代码生成器

力软.net/java开发平台

Java 分布式 代码组织 平台应用服务

够开放吗?来,和一群开发者搞事情!

易观大数据

为什么说区块链是制造信任的机器?

CECBC

区块链 不可篡改

初露锋芒的AI战斗机,打开AI军备竞赛的潘多拉盒子

脑极体

Go 云原生应用实战系列(二)

田晓亮

微服务 云原生 Go 语言

架构师训练营第十三章作业

吴吴

北京或先行落地央行数字货币 人民币3.0时代将来临

CECBC

数字货币 银行 人民币

Centos7 IP、名字、防火墙配置

yuanhang

centos7 防火墙 静态IP

Week13 学习总结

赵龙

大数据2学习总结

周冬辉

JavaScript 简介

InfoQ_34a83d636158

搜索引擎如何推荐网页

dongge

详解 Python 的二元算术运算,为什么说减法只是语法糖?

Python猫

Python 编程 翻译

【架构师训练营】第 13周作业

花生无翼

架构师训练营 week13

devfan

CommonMistakes

卓丁

捡到宝啦!阿里内部人手一本的Springboot进阶手册,先学为敬

Java架构师迁哥

云栖大会倒计时8天,新一代CDN的技术突破和应用实践专场有什么看点?

阿里云Edge Plus

CDN CDN加速

你所在的行业,常用的数据分析指标有哪些?

李朋

撑起瞬时千亿交易额的云数据库是怎么炼成的?

撑起瞬时千亿交易额的云数据库是怎么炼成的?

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