NVIDIA 初创加速计划,免费加速您的创业启动 了解详情
写点什么

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

评论

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

网络攻防学习笔记 Day144

穿过生命散发芬芳

高可用 9月日更

玩转TypeScript工具类型(下)

有道技术团队

typescript 大前端 网易有道

阿里,快手,拼多多等7家大厂Java面试真题,Java面试题高级

Java 程序员 后端

Linux用户密码管理

在即

9月日更

乘着汽车智能化的浪潮,“汽车人”的职业方向选择(三)

SOA开发者

软件定义汽车 车载控制单元

为什么不推荐Python初学者直接看项目源码

Felix

Python 编程 开发 Programing 阅读代码

iOS开发面试拿offer攻略之数据结构与算法篇附加安全加密

iOSer

ios 数据加密 iOS面试 iOS逆向 iOS算法

深入理解掌握零拷贝技术

Linux服务器开发

网络协议 零拷贝 Linux服务器开发 Linux内核 用户态

Percolator模型及其在TiKV中的实现

vivo互联网技术

数据库 Percolator 分布式,

与springcloud整合的框架源码读取入口

Java 编程 架构 微服务

堡垒机作用之事后审计详细讲解-行云管家

行云管家

运维 网络安全 运维审计 事后审计

国庆高质量出行,可视化开启智慧旅游

ThingJS数字孪生引擎

大前端 物联网 可视化 旅游 数字孪生

阿里,快手,拼多多等7家大厂Java面试真题,Java笔试题及答案详解

Java 程序员 后端

测试开发之前端篇-CSS层叠式样式表

禅道项目管理

CSS html

基于Tensorflow + Opencv 实现CNN自定义图像分类

华为云开发者联盟

tensorflow KNN OpenCV CNN

对Python爬虫编写者充满诱惑的网站,《可爱图片网》,瞧人这网站名字起的

梦想橡皮擦

9月日更

二十不惑的年纪,我简直走了狗屎运(4面拿字节跳动offer)

Java 程序员 架构 面试 计算机

金融级分布式事务解决方案DTC

tom

千万级数据迁移与分表的技术方案-企业产品实战

谙忆

Java 后端 分库分表 引航计划

低代码应用:软件开发的一体化最新形态!

优秀

低代码

JavaScript进阶(七)call, apply, bind

Augus

JavaScript 9月日更

mydumper备份工具介绍与使用

Simon

MySQL

2021年公有云市场的5大趋势

浪潮云

云计算

Tapdata 实时数据中台在智慧教育中的实践

tapdata

FunTester框架Redis性能测试之list操作

FunTester

redis 性能测试 测试框架 压力测试 FunTester

主机安全是什么意思?安全体检包含哪些方面?

行云管家

运维 服务器 主机 主机安全 安全体检

Alibaba内部“Java架构核心宝典”来袭,全新技术限时开源

Java 编程 程序员 架构 面试

作为一线技术人员,如何更好地提升自己

谙忆

管理 成长 引航计划

CSS 轻松制作 SVG 动画

devpoint

css3 SVG 9月日更

一部好看过武侠小说的热血互联网史!

博文视点Broadview

幻读是啥,会有什么问题?如何解决?

Java MySQL 数据库 面试 后端

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