背景简介

现象:一个打印的页面,在循环打印测试过程中,当app运行超过5分钟后,app被系统自动杀死(android+ios)。

分析:手机上app被杀死,原因无非2种:

  1. 随着时间流逝,app的进程优先级降低,逐渐被系统杀死
  2. app有内存泄漏,占用内存越来越多,手机GC无法回收内存,导致oom系统强制杀死
原因1:

涉及的app的应用保活,保活的方式有很多,android中因为我们的app一直在前台运行,可能受到手机息屏或被切到后台的影响,所以解决的思路是打印时启动一个service和notification,使app进程变成一个前台进程。

这里使用了一个三方库 flutter_background,这个库仅支持android,防止手机息屏,并启动一个前台服务,省得自己写了。

结果:运行时间由5分钟提升到20分钟了,但还是会被系统杀死。
可见,一定有内存泄漏!

原因2:

内存泄漏的分析,flutter提供了分析工具,Observatory。

在Profile模式下,启动时控制台会打印出工具的地址:

ios 集成 uniapp ios 集成 flutter内存泄露_android


把这个地址 127.0.0.1:59987/kmyC2NH73o0=/ws放到chrome浏览器中,就进入了Observatory界面。

这里我们要分析内存,就点击Allocation Profile进入内存分析的页面。

Dart vm

这里简单说说dart虚拟机内存原理。
dart将内存分为新生代和老年代,采用引用计数和标记+复制删除的方式管理对象。
新生代主要存放频繁创建和销毁的对象,采用引用计数法和可达性分析,当一个对象引计数为0时,当发生GC时,就会被vm回收。
老年代主要存放不常被销毁的对象、静态变量、方法、类定义等,如果在多次GC(根据vm规定次数)中,新生代中都存活下来的对象,会被移到老年代。

内存泄漏判定

如何分析内存泄漏呢?

ios 集成 uniapp ios 集成 flutter内存泄露_老年代_02


见上图,依次是三个时间由早及晚的内存对象截图。

我们关注到红框标记出来的Paragraph对象,在三个时间点(中间经历数次GC),在Old Generation中,对象数依次增加,占用内存也不断增加,那么Paragraph有可能发生了内存泄漏!

因为我们app的打印页面是一个静态页面,循环打印过程中,会依次生成要打印的图片,通过打印sdk发送给打印机,那么Paragraph只可能是打印过程中产生的,而且被强引用,导致GC无法被释放。另外关注EngineLayer数量,也是不断增加的。

这里就想到了打印生成打印图片的方法,参看我之前的一篇博客【Flutter获取Widget截图(前台和后台方式)】分享的我们在后台生成图片的方法:创建Widget->父widget嵌套RepaintBoundary->创建RenderView->attach BuildOwner->截图生成ui.Image。

猜想:EngineLayer数量和创建截图有关,而Paragraph是要打印图片中,用Paragraph手动绘制了文字。现在两个对象随着打印不断增多,那么完全可以推测:在通过widget生成截图后,widget被绑定到了renderTree没有被解除绑定,导致无法被系统回收!

查看代码,很快找到了可以改进的地方:

ios 集成 uniapp ios 集成 flutter内存泄露_老年代_03

截图生成后,主动调用RenderObjectToWidgetElement.unmount()方法,使widget从renderTree上移除,便于被内存回收!

验证

修改后重新运行程序10分钟后,查看dart vm内存分布:

ios 集成 uniapp ios 集成 flutter内存泄露_内存泄漏_04

Paragraph在老年代中不见了,而且EngineLayer的数量在GC后也变成了0 !
老年代中几乎没有无视GC, 随打印时间增加的对象。

继续运行30分钟,app稳定无闪退!

ios 集成 uniapp ios 集成 flutter内存泄露_老年代_05