MOO音乐是 TME 旗下的新锐音乐服务,其团队是公司内最早实践 Flutter 的先行者之一。本系列文章将提炼 MOO APP 开发中遇到的情况,就 Flutter 内存占用治理方面,分享日常开发的一些基本认知、注意要点、排查方法和优化方案。

一、前言

内存问题几乎是所有软件开发都会碰到的标配问题。追求极致的内存瘦身,可以说是作为一名开发者的本能。MOO 音乐整体采用 Flutter 混合开发架构,在享受到了 Flutter 带来的卓越的跨平台开发效率的同时,也要面对这个新事物带来的一些新的挑战,内存治理便是我们关注的一个重点方向。

二、Flutter 内存管理机制

我们关注的内存一般可以分为三大块,包括应用整体内存、Dart 堆和栈内存、External 内存。

1. 应用整体内存

包括客户端平台内存,以及 Flutter 引擎和 Dart-VM 自身的内存。通过整体内存的变化,可高效直观地判断功能模块是否有问题。

2. Dart堆和栈内存

Dart 以 Isolate 划分独立的线程、堆和栈内存,不同 Isolate 是隔离的,并且是各自独立 GC 的。其中堆内存划分成两个空间:新生代内存和老生代内存空间。

新生代(New Generation) 内存空间较小,划分为等分的两部分,采用复制-清除法管理内存,效率高,执行频率比老生代高一些。经过一轮复制清除后,存活下来的对象会被转移到老生代空间。

老生代(Old Generation) 内存空间较大,内存管理要经历 标记 - 清除 - 整理 三个步骤。

标记算法采用对象可达性算法, GC root 维护了一个根对象列表,从这些跟对象出发,遍历所有可达对象,标记活动对象,这样就可以识别出没被标记的待清理的对象。

内存清理之后会出现零散的内存碎片,需要移动整理来腾出完整的可用内存池。进行清除和整理的时候,会使引擎所有线程都停止处理 - ‘stop the world’。

从标记到内存清除和整理, GC 是一个耗时的执行过程,虽然引擎自身也做了一些优化,如多线程并行执行、增量执行、闲时执行,以减少 GC 的性能影响。从开发的角度来说,从实现细节去减少内存占用,以及开发完功能后进行内存泄漏排查,是必不可少的开发步骤。

3. External内存

原生对象占用的内存空间,如文件、解码的图片数据,虽不属于 Dart 的内存,但通过嵌入层把这些对象包裹成 Dart 可访问的对象,供 Dart 侧访问和操作。这块内存虽然不会影响 Dart 运行性能,但是出问题很容易导致 OOM,通常 Dart 对象引用未正常释放也会导致相关原生对象不回收。

三、内存泄漏的常见场景

引擎无法从业务侧自动判断哪些对象该不该清理,即使抛出 OOM 也不会强制清理,这就需要我们从编码细节上去主动规避,以下是一些常见导致内存泄漏的案例:

1. 监听反注册缺失

排查内存泄漏的过程中,我们发现图片内存大幅度超出了图片缓存自身 size 限制的增长,并且不会被 GC 回收,经过排查发现我们封装的一个底层图片处理类,注册了图片事件流监听后,并没有在适当的时机做反注册处理。

在 dispose 方法中添加了反注册之后,图片内存就可以正常释放了。

2. 长列表直接构建列表项

通过对列表数据遍历的方式,一次生成所有数据对应的 widget 列表,直接塞进 Column 里展示给用户,当加载了几页数据之后,数据量稍大就会轻易导致 OOM 或导致严重卡顿。

正确的处理方式是使用列表组件自带的滑窗创建列表项功能来动态创建列表项,如果列表结构比较复杂可以考虑使用 Sliver 系列组件。

3. 延时、持续执行的闭包引用

Flutter 提供的延时和持续执行的对象有 Animation、Timer、Future 等,在结束执行之前,回调函数引用到的相关对象都会被强引用保留在内存中。

上面代码由于在 Timer.periodic 回调内引用了 context,MaintainInMemoryWidget 对象会被保留在内存中,如下图。

这种情况,需要确保在功能退出时做相关清理或结束执行的操作。

4. 永久活动对象引用

除非需要永久保留或有明确的清理实现,否则不推荐将一些对象挂载到永久存活的对象下面。

如应用根节点实例化的 Provider model,常规我们都会把清理动作放置在 dispose 方法内执行,但应用根节点实例化的 model 应用周期内不会执行 dispose,这很容易让人忽略内存清理操作。

类似的,还有持有在单例对象属性和静态变量的对象,都需要配套功能退出后的清理操作。

5. 退出清理中断

进行退出清理的某行代码抛错后,后续的处理便不会执行到。

这种情况需要尽可能保证清理操作的健壮性,避免结束前抛异常。

6. 第三方组件质量问题

做技术选型的时候,组件或方案的 Like 数量或 Git star 会作为质量参考的一个尺度,但实际情况即使是官方提供的库也还是会存在一些坑,如:

  • video_player 视频组件,在注销之后内存并未全部释放
  • shared_preference 数据量大了之后高频操作引起高 IO 内存问题

注意尽可能对引入组件进行性能和内存测试,避免功能开发完后才发现问题,增加维护成本。

7. Flutter Engine 自身的问题

如 iOS 渲染 emoji 内存占用 +130 MB,且关闭页面无法被回收,目前只能从引擎内部去挖掘解决方案。

四、内存泄漏的排查实战

为了便于我们定位具体问题代码,Android Studio 或 VS Code 插件帮我们包装了相关内存工具,这些工具都基于 debug 模式下 Dart VM service 暴露的接口开发的,Dart VM service 自身也带有协助排查内存问题的工具 - Dart VM Observatory,attach 之后访问 service 提供的 http 链接即可使用该工具,内存排查功能路径为:Isolate (main) --> Allocation Profile。

工具有了,如何排查定位导致内存泄漏的问题代码呢?

  • 对同一个功能或页面进行反复相同的进入、退出操作;
  • 然后执行强制 GC,查看不同操作后的内存快照;
  • 对比该功能关联的对象实例增加情况;
  • 如果强制 GC 后实例只增不减或该回收的对象没有被回收,没有特殊的延时处理一般就可以判断相关代码有问题。

下面以 Image 内存泄漏排查为例,展示具体的问题代码定位过程,目标是排查列表项内存泄漏,功能进出动作对应着列表项的滑窗动态创建和销毁。

1. 我们打开 APP 其中一个页面,如某个列表页,通过 Xcode 观察应用整体初始内存 470MB。

往下翻了几页,内存持续上涨,并且表现为一直翻页一直涨,这种情况大概可以确定页面存在内存泄漏问题,如下图。

2. 打开自带工具 Allocation Profile 界面后,点击右上角 GC 按钮,强制回收所有可回收对象。

回收了可回收对象后,剩余的便是当前 Isolate 状态下的内存快照,都是强引用内存对象。

3. 如下面两图,根据初始内存快照和当前快照的对比,可明显看到图片实例有明显的增长,并且随着不断翻页持续增长,更明显超过了图片缓存的大小的限制。

从这里我们可以确定 Image 对象实例存在引用没被释放的问题,下一步是找出 Image 引用的持有对象。

4. 点击 Image 链接,如下图所示。

进入新页面后找到 strongly reachable,展开可看到内存快照中具体的 Image 对象列表,如下图所示。

5. 进入 Image 实例详情界面,如下图所示。

6. 查找对象引用未释放的节点,通常要翻查外部对象引用链,从上图可以看出该 Image 实例最终被 _CacheImage 引用,是图片缓存正常持有的引用。

根据图片缓存的 LRU 机制,可以断定会存在图片缓存释放掉但被其他对象持有的 Image 实例对象,我们可以继续尝试别的 Image 实例排查其引用情况。

7. 经过随机翻查了一些 Image 实例,发现了一些线索,如下图所示。

这个 Image 实例引用已经被 ImageCache 对象释放,但没有被正常回收。

8. 从引用链可以看出,
DisplayDecorationImagePainter 对象是我们封装的类,ImageStreamListener 对象持有了相关引用,正是因为这个引用链导致了 Image 对象实例没被回收。

9. 翻查了一下代码,发现是由于
DisplayDecorationImagePainter 注销时没有给 ImageStream 反注册监听。

10. 补充了反注册代码之后,重复前面的操作,持续滚动翻页,图片实例数量维持在 122MB 左右,内存也没有持续增长,如下图所示。

到此,图片内存问题已经得到解决。

排查过程就是在对象实例的引用链上寻找代码线索的过程,针对图片泄漏的排查,可以设置一个较小的图片缓存阈值,减少图片对象缓存,干扰对象少了排查效率就大大更高。

排查泄漏是个频率较高的重复操作,通常排查特定功能都会关注特定的相关对象,自带的排查工具对关注的对象没有较好的过滤功能,而且操作路径长,引用链显示也不够直观。可以考虑自研排查工具来解决这个效率问题。

五、内存优化策略

1. 图片内存优化

各种导致内存增长的资源中,图片引起的问题是尤为明显和常见的,一张高清图动辄几百K,MOO 音乐很多列表都使用 GIF 动图,大小可以达几MB乃至十几MB,图片所占内存跟图片大小有关,影响更大的是图片缓存尺寸导致的内存增长,一旦出问题就很容易导致 OOM。

要实现图片内存优化,我们从图片加载流程入手,分析可以从哪些处理节点作为优化的切入点,下图是 NetworkImage 的加载过程。

i. 图片缓存尺寸(即解码尺寸)优化

从源码可以看到,Image.network、Image.asset、Image.file、Image.memory 都有执行设置缓存尺寸的 resize,如果没有设置 cacheWidth 和 cacheHeight,默认使用的是图片自身的像素尺寸。

根据框架源码,具体图片所占内存的计算方法为:图片占用内存大小 = 解码宽度像素 * 解码高度像素 * 4,也就是图片解码数据内存占用跟解码面积成正比。

下面做个简单的实验来验证设置缓存尺寸对内存大小的影响程度。

直接加载一张 @3x 尺寸为 2058x1800 的图片。

不设置缓存尺寸引擎会以原图的尺寸作为解码尺寸,也就是 2058x1800,解码内存达到了 18.8MB,如下图所示。

这里的缓存尺寸只设置了 cacheWidth,cacheHeight 会自动根据图片原比例计算得出。

设置了缓存尺寸,图片解码内存占用只有 5.2MB,如下图所示。

那么,缓存尺寸该如何取值呢?

相对屏幕物理尺寸取值,图片尺寸 和 显示逻辑尺寸 * dpr(设备像素比) 取较小者即可。

屏幕逻辑像素和物理像素,以 iPhone 为参考如下:

设计切图尺寸若基于 750 作为 @1x 尺寸基准,如果不设置缓存尺寸,内存将会是设置了缓存尺寸的 3 倍 到 4 倍。

同样,BoxDecoration 的 Image 属性也需要设置缓存尺寸,为了提高开发效率,可考虑封装 Image 组件和 BoxDecoration 组件,实现自动按照布局尺寸去设置缓存尺寸。

ii. 图片资源裁剪

另外,network 图片在产生解码内存之前,会先将图片数据请求下来,获得一份二进制源图数据,即使图片解码完成,这份数据仍然留存在内存里,如下图所示。

可以根据显示尺寸,利用图片服务的裁剪能力对图片尺寸进行裁剪,可以减少这部分的内存占用,也有利于提升加载效率和解码效率。

iii. 将图片缓存到本地

使用 cached_network_image 组件,可以将网络下载下来的图片缓存到本地,大幅度提升二次加载的效率。

iv. 针对 asset 图片做压缩处理

设计师切图一般给到的是 24 位 png 格式高清图片,可以使用 tinypng 工具进行手动压缩也可以使用 tinypng 提供的压缩服务,将 24 位压缩成 8 位以及删除一些不必要的元数据,压缩效果可达 50% 以上,虽是有损压缩,但是视觉上并无明显差异,是被设计师认可的压缩方式。

减少图片数据内存增长,也有利于提升解码效率,还可以减少安装包大小。

v. 调整图片缓存阈值

了解下 ImageCache 对象(
PaintingBinding.instance.imageCache):

缓存存储分为三种情况:请求处理中、使用中以及暂未使用图片缓存。

针对 _cache 的部分,内部实现了 LRU 机制,默认 100MB 或 1000 张图 满⾜其⼀,就标记最先缓存的对象给释放其引用。

可针对设备配置,适当降低缓存阈值,有助于降低 OOM 的概率,配合图片本地缓存,浏览体验不会有明显的影响。

vi. 样式图片和内容图片缓存隔离

我们可以将图片分为两大类,样式图片和内容图片:

样式图片:作为 APP 的 UI 风格的构成部分,通常被访问到的频率较高,作为样式的构成,我们一般不希望这种图片的加载存在用户能感知到的延迟,甚至有时候我们会选择提前预加载在缓存中。

内容图片:通常从接口获取,作为内容呈现给用户,用户习惯上可以容忍一定的加载延时。

对样式图片我们需要尽可能将高频访问的图片保留在内存中,而针对内容图片,我们可以选择更实时的方式去清理,然而框架自带的缓存机制对图片缓存的管理是无差别的。

解决方案是改造 ImageCache 类,加一个存储类型_assetsCache,存储 asset 类型图片缓存 ,需要的话也可以支持 LRU,指定缓存大小阈值。

在 _assetsCache 的基础上,我们可以高频的执行 _cache.clear() 来清理不再访问的缓存。

选择触发清理缓存的时机

  • 可以选择页面退出时触发,以及类弹框功能退出时触发。
  • 长列表无限加载场景,也可考虑滑窗实时创建和清理,同时在距离滑窗一定范围内对图片进行预加载,提高图片渲染效率。

2. 页面栈维度内存优化

用户长时间的浏览操作,在不同的页面之间穿梭,少不了持续不断的 push 页面到页面栈,随着页面不断地增加,内存也在持续增长。我们不得不考虑在页面栈的维度去做内存优化。

在原来的页面栈基础上,我们只需要保留顶层两个页面,第三层及以下的页面全部都被销毁回收内存。这种模式下,用户不断的打开新页面,内存也不会有明显的增长。

  • 当新打开一个页面,原来第二层的页面被执行销毁,回收该页面的所有内存。

  • 当页面栈执行了 pop 操作,倒数第三层的页面变成第二层,开始执行页面重建,包括数据请求、Widget 树构建以及图片加载。

  • 动态创建销毁页面的的方式,可能会丢失用户交互过程所产生的状态变更,影响用户体验。针对这种情况可以增加支持设置页面是否 KeepAlive,选择性地保留一些不好还原浏览状态的页面。

当然,针对 KeepAlive 的页面,我们仍然可以执行对该页面图片缓存的强制清理。

六、结语

内存排查在引用链上寻找编码问题线索会有一定难度,需要多操作熟悉引用链的一些常见对象和展示规律,也可以尝试引入开源工具或自研工具来提升排查效率。

内存治理无法一蹴而就,需要提升对内存问题的警觉性,在编码细节上多留意强引用的释放时机,业务功能开发完后在转测前后去检查相关引用释放情况,确保避免内存随着浏览时间不断堆积。

腾讯音乐MOO音乐应用的Flutter内存治理实战分享相关推荐

  1. 京东1.8联合会员腾讯视频QQ音乐转手教程 京东app换绑

    京东1月8日晚上0点开启了一个1.8买1送8的联合会员活动,一年京东会员仅售218元,而且送5斤车厘子,1年腾讯视频,QQ音乐绿钻,喜马拉雅vip,麦当劳月卡等.很多看官都在问我不需要这些会员,那我能 ...

  2. 安卓Native逆向之MOO音乐解密( .bkcflac,bkcmp3文件解密)

    安卓Native逆向之MOO\QQ音乐解密( .bkcflac,bkcmp3文件解密) 1.背景 2.Java层逆向 3.Native层逆向 4.Java实现 1.背景 本文写于2021年1月5日,解 ...

  3. 国外计算机音乐专业,音乐留学干货 | 国外电子音乐专业留学如何?

    原标题:音乐留学干货 | 国外电子音乐专业留学如何? 说到电子音乐, 你脑海中是否出现各种嗨翻全场的画面, 是夜场DJ, 还是百万调音师? 1.什么是电子音乐制作? 电子音乐制作又名MIDI(Musi ...

  4. [工具]再更新音乐下载软件,MP3音乐无损音乐下载器

    昨天发的音乐下载软件我没有多做几次测试,导致误导大家了,昨天分享的那个只能听音乐不能下载,知道后我又赶紧为大家找了一款(一个网站),不知道什么情况链接不能发自动回复里,我也不敢直接放文章里,大家回复转 ...

  5. 【折腾的一个小玩意】基于jquery+百度音乐的音乐外链小工具

    [折腾的一个小玩意]基于jquery+百度音乐的音乐外链小工具 现在百度mp3好像关掉了,虾米的加载又慢,于是我就根据以前发过的百度音乐的代码弄了这个东西.代码托管到百度BAE的,应用还在审核中,不知 ...

  6. android 控制音乐,Android音乐控制接口RemoteController使用

    Android RemoteController使用 原文链接 RemoteController在API 19 引进,用来给音乐控制提供标准接口.长久以来,音乐播放在Android平台没有一个标准的接 ...

  7. 浅谈音乐与计算机,浅析电脑音乐在音乐教育中的应用

    一.电脑音乐概述 从传统意义上讲,电脑音乐与MIDI是同一个概念,其核心技术是计算机音序软件的运用和电子乐器的数字化(MIDI的含义为"乐器之间的数字接口").从FM调频到PCM采 ...

  8. 国外部分音乐人工智能/音乐科技研究机构科研项目简介

    转载自我的个人网站 https://wzw21.cn/2022/02/26/music-ai-tech-group/ 本文对国外部分音乐人工智能/音乐科技科研机构的科研项目与教学课程设置作简要介绍,包 ...

  9. 计算机教学音乐,计算机音乐的教学和应用研究

    摘要: 计算机音乐(简称:电脑音乐)是20世纪80年代以来的一种音乐形式,它是现代电子技术,计算机以及其他一些电子音频处理设备同音乐相结合的产物.它的出现大大改变了音乐领域中固有的方法和程式,使原有的 ...

最新文章

  1. 3分钟销量破千 这款笔记本告诉你大家喜欢的轻薄本什么样!
  2. mysql 触发器判断不插入数据_mysql关于触发器怎么判断数据存在时更新不存在时添加呢!...
  3. 摘录nginx 信号处理方法部分代码
  4. docker 二进制安装
  5. do还是doing imagine加to_imagine doing还是todo
  6. hihocoder A Game 区间dp
  7. SQL Server 全文索引创建
  8. 【kafka】在 Kafka Streams 中启用 Exactly-Once
  9. 第十一届中国开源黑客松+中国程序员节重磅来袭,这里将有你不能错过的精彩。...
  10. 32蜂鸣器天空之城代码_stm32版蜂鸣器播放爱若琉璃
  11. ShowDoc v2.4.8 发布,IT团队的在线 API 文档工具
  12. iOS11、iPhoneX、Xcode9 的注意点汇总
  13. 三大开源bi工具_Superset,基于web的开源BI工具,github三万star
  14. Ghelper安装及使用
  15. oracle 播布客 视频,播布客视频-Managing Indexes笔记
  16. java操作hfds----刘雯丽
  17. 数独游戏开发——计时器实现
  18. 【Java常用类】Instant:瞬时
  19. UVa Problem 10205 Stack ’em Up (完美洗牌术)
  20. AOP -- 注解 @Aspect 、@Pointcut

热门文章

  1. 实时系统与非实时系统的区别
  2. mysql聚类函数排序_聚类算法大盘点 - 如鱼饮水,冷暖自知 - OSCHINA - 中文开源技术交流社区...
  3. 《Mining of Massive Datasets》读书笔记
  4. 综合布线实训装置-网络综合布线模拟墙-综合布线实训平台
  5. 操作系统面试题(转载)
  6. Python - 在定义函数时,为什么默认参数不能放在必选参数前面?
  7. hive 开窗函数OVER(PARTITION)详解(一)
  8. MySQL学习记录04where条件子句、联表查询、子查询
  9. 小i机器人用AI打造金融业认知智能企业
  10. 软件无线电技术学习资料大汇总