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

2023-11-16

背景

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

  • 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加载一张很大的本地资源。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

一个方案提升Flutter内存利用率(干货) 的相关文章

随机推荐

  • MYSQL--架构--MGR--理论--02--架构

    MYSQL 架构 MGR 理论 02 架构 1 架构图 1 1 主要组成 APIs接口层 组件层 复制协议模块层 GCS API Paxos 引擎层 1 2 事务进入 MGR 层内部处理过程 应用发来的事务从 MySQL Server 经过
  • QT笔记-生成PDF文件(附带完整源码)绘制表格和文字

    项目链接 https download csdn net download C panpan 88268845 https download csdn net download C panpan 88268845 h public void
  • Ubuntu配置apt-get install自动补全功能

    1 执行以下命令 sudo apt get update sudo apt get upgrade 2 默认会安装bash completion 如果没有安装 请执行以下命令 sudo apt get install bash comple
  • JAVA实现文件的上传和下载

    目录 一 文件的上传和下载的作用 什么是文件上传 文件的下载及需求 为什么使用文件上传 二 文件上传 文件上传的关键 不同的框架提供了不同的获取输入流的方式 Servlet上传案例 文件上传细节 存储位置 文件上传问题 目录分离 三 文件下
  • 前端react框架的部分总结

    前端react框架的部分总结 react的优势 react相对其他框架优势 高性能高效率 实现了前端界面的高性能高效率开发 所以说react很擅长处理组件化的页面 和vue的区别 在React中 一切都是JavaScript 所有的组件的渲
  • Android APP应用启动页白屏(StartingWindow)优化

    转自 https www cnblogs com whycxb p 9312914 html 本人采用这种方法没有效果 启动图片出来第一帧 我应用的第一帧也出来了 启动背景颜色没有调试出来 Theme AppCompat Light Dar
  • Java基础-一些容易被人忽视却重要的Java基础知识(二)

    文章目录 一 重载和重写 1 重载 2 重写 一 重载和重写 1 重载 被重载的方法必须改变参数列表 参数个数或者类型 顺序不一样 被重载的方法可以改变返回类型 被重载的方法可以改变访问修饰符 可以使用新的或更广的检查异常 方法能够在同一类
  • java动态生成pdf文件的方法

    java动态生成pdf文件 文章目录 java动态生成pdf文件 前言 一 生成pdf模板 二 使用步骤 1 使用jar包 2 pdf实现方法 总结 前言 java开发过程中难免会遇到生成文件的需求 这里简单介绍一下关于pdf格式的文件的动
  • python常见的面试题,看你都掌握了吗

    前言 Python是目前编程领域最受欢迎的语言 在本文中 我将总结Python面试中最常见的50个问题 每道题都提供参考答案 希望能够帮助你在2019年求职面试中脱颖而出 找到一份高薪工作 这些面试题涉及Python基础知识 Python编
  • usb转网口转换器经常自动断网

    问题 最近使用一个usb转网口的扩展坞 发现和其它机器通信时 经常会自动断网 原因 和设备的电源管理策略有关 USB设备的 允许计算机自动关闭此设备以节约电源 选项默认是选中的 而网络设备的此选项默认值是未选中 解决办法 打开设备管理器 找
  • MATLAB基本语法详解

    MATLAB基本语法详解 下面内容 变量 M Files 决策 循环容易掌握 命令 数据类型 运算符不需要记住 用了再查 变量 每个MatLab变量可以是数组或者矩阵 最简单的方法指定变量 x 3 定义并初始化 赋值 变量x MATLAB上
  • 搭建redis主从服务 :master_link_status:down 主从无法连接问题汇总

    1 问题描述 主从无法连接问题 2 搭建 https blog csdn net ling811 article details 53637257 https blog csdn net qq 24113267 article detail
  • 双机热备 ip地址_SBC双机热备方案

    概述 随着通信全IP化的进程 现代企业中基于IP的语音 视频 会议 融合通信已广泛应用 同时企业通信也面临着新挑战 包括安全攻击 跨网NAT穿越以及业务稳定运行 高可靠方案尤为重要 因此在组网中部署SBC Session Border Co
  • NPN和PNP三极管原理以及应用电路设计

    一 基本概念与原理 三极管最主要的功能是电流放大 模拟电路 和开关作用 数字电路 常用的三极管有 S9014 S8550等型号 三极管由两个PN结构成 共用的一个电极成为三极管的基极 用字母b表示 其他的两个电极成为集电极 用字母c表示 和
  • 安卓加固基础(二)

    4 反调试 4 1 思路一 一个进程最多只能被一个进程ptrace 本文章主要针对安卓so反调试和最初的加壳方法进行了一下总结 在处于调试状态时 Linux会向 proc pid status写入一些进程状态信息 比如TracerPid字段
  • 【leetcode 力扣刷题】删除字符串中的子串or字符以满足要求

    删除字符串中的子串或者字符以满足题意要求 1234 替换子串得到平衡字符串 680 验证回文串 917 仅仅反转字母 1234 替换子串得到平衡字符串 题目链接 1234 替换子串得到平衡字符串 题目内容 题目中给出了平衡字符串的定义 只有
  • TypeScript 初识笔记

    目录 一 基础静态类型和对象类型 二 类型注解和类型推断 三 元组的使用和类型约束 四 接口 五 类 六 联合类型和类型保护 七 枚举 八 泛型 九 tsconfig json文件 TypeScript 其实就是 JavaScript 的超
  • 教程:在Vivado中指定VSCode作为文本编辑器

    教程 在Vivado中指定VSCode作为文本编辑器 更新历史 20190325 首次发布 20201027 对本文第三步中需要填写的内容做了修改 使得VSCode能够实现光标的自动定位 该修改的做法是 HK Journalist 在评论中
  • Vue组件间通信方式都有哪些?

    一 组件间通信的概念 组件间通信这个词进行拆分 组件 通信 都知道组件是vue最强大的功能之一 vue中每一个 vue可以视之为一个组件通信指的是发送者通过某种媒体以某种格式来传递信息到收信者以达到某个目的 广义上 任何信息的交通都是通信组
  • 一个方案提升Flutter内存利用率(干货)

    背景 我们闲鱼使用的图片方案是自研的外接纹理方案 Android侧创建SurfaceTexture 通过FlutterJNI注册到Flutter engine里 最后返回texture id给Flutter应用层 应用层使用Texture