一个方案提升Flutter内存利用率(干货)

简介: 拿什么拯救你,flutter内存

作者:闲鱼技术——靖书

背景

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

  • Android侧创建SurfaceTexture,通过FlutterJNI注册到Flutter engine里,最后返回texture id给Flutter应用层,应用层使用Texture Widget和textue id去显示图片纹理。
  • 纹理数据则是在Android侧,通过OpenGL将图片纹理写入到SurfaceTexture,然后通过Flutter engine里的共享内存,将纹理数据传入到应用层,最终交给Skia渲染。

Alt text

这里面存在的问题:
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图片加载逻辑,以及图片是如何缓存的
Alt text

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

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

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

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

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

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

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

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

方案二:自定义ImageCache

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

将ImageCache替换成我们自定义的

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

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

对ImageCache进行功能扩展

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

Alt text

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

  1. TextureCacheKey是唯一标识纹理的key,该key是主要是根据宽高,url来判断是否是同一个纹理的。
  2. TextureImageStreamCompleter 则是纹理的管理类,该类继承ImageStreamCompleter,内部持有纹理数据和下载成功的回调。当命中缓存时,返回该对象给应用层,并从中拿到纹理id交给Texture Widget渲染
  3. 当没有命中缓存时,会调用传入的loader方法构造TextureImageStreamCompleter,并且会执行纹理的加载逻辑。同时会构造一个listener回调,注册进TextureImageStreamCompleter。
  4. 当纹理加载成功时,会执行listener方法回调,该方法里主要是计算纹理大小,将它放入缓存队列里,检查缓存大小是否超过最大值,超过则淘汰之前最久未使用的纹理。

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

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

效果

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

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

Alt text

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

展望

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

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


阿里技术
阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。

阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。

3.1k 声望
6.2k 粉丝
0 条评论
推荐阅读
阿里云:加大NoSQL数据库软硬件一体化技术自研
简介:8月25日,在天池平台与阿里云数据库事业部联合主办的阿里云NoSQL数据库峰会上,阿里云公布NoSQL数据库自研2.0计划,进一步加大软硬件一体化技术体系的自研力度,通过聚焦软硬协同、多模融合、云原生三大方...

阿里云开发者阅读 454

闲鱼app数据实时采集探索实验
前言本文章分享一下最近研究闲鱼app商品数据采集。技术栈PythonFridaJADXObjectionAndroid Studio思路使用Android Studio创建x86模拟器并运行,安装闲鱼和frida使用Objection hook URL类,打印调用栈分析出关键函...

Prasanta阅读 1.9k评论 4

Android 开发中的SSL pinning
在日常的安全渗透过程中,我们经常会遇到瓶颈无处下手,这时候如果攻击者从APP进行突破,往往会有很多惊喜。但是目前市场上的APP都会为防止别人恶意盗取和恶意篡改进行一些保护措施,比如模拟器检测、root检测、A...

xiangzhihong阅读 1.1k

Flutter 这一年:2022 亮点时刻
2022 年,我们非常兴奋的看到 Flutter 社区持续发展壮大,也因此让更多人体验到了令人难以置信的体验。每天有超过 1000 款使用 Flutter 的新移动应用发布到 App Store 和 Google Play,Web 平台和桌面应用程序数...

Flutter阅读 903

flutter系列之:在flutter中使用流式布局
我们在开发web应用的时候,有时候为了适应浏览器大小的调整,需要动态对页面的组件进行位置的调整。这时候就会用到flow layout,也就是流式布局。

flydean阅读 818

个推支持小程序消息推送,助力开发者实现用户高触达、高转化
随着小程序技术和应用场景的不断完善,越来越多的开发者搭建了小程序平台,为用户带来更“轻量”的服务。在小程序用户迅猛增长的同时,开发者对于小程序用户精细化触达的需求也愈加强烈。近日,个推消息推送上线了...

个推阅读 812

封面图
Flutter for Web 首次首屏优化——JS 分片优化
Flutter for Web(FFW)从 2021 年发布至今,在国内外互联网公司已经得到较多的应用。作为 Flutter 技术在 Web 领域的有力扩充,FFW 可以让熟悉 Flutter 的客户端同学直接上手写 H5,复用 App 端代码高效支撑业务...

阿里巴巴终端技术阅读 791

封面图

阿里巴巴官方技术号,关于阿里巴巴经济体的技术创新、实战经验、技术人的成长心得均呈现于此。

3.1k 声望
6.2k 粉丝
宣传栏