背景

假如你的 Flutter 版别号小于等于 2.5.3 或大于等于 3.0.5,以下描绘的问题将不会发生在你的使用中,可是我信任大部分使用都会射中此区间。

事情发生在最近,咱们的使用(稿定设计)新上线的 iOS 版别溃散数据飙升。依据溃散日志和用户反应,大部分新增溃散都来自于同一个原因:内存不足。有的直接变成 OOM,不易排查。有的则是申请内存失利,导致后续逻辑错误的溃散。

结合「处处开花,多点爆炸」的状况来看,应该是某种偏底层的内存办理问题。这就有点挠头了,因为这个版别并没有做什么内存相关的改动。所以我采取了二分法,花了两个小时试了版别中所有 PR,发现罪魁祸首是 Flutter 版别晋级:2.5.3 → 2.10.。

那么问题就转化为:Flutter 在 2.5.3 → 2.10. 中做了什么改动,导致了内存溃散问题。

剖析问题

依据用户反应,咱们发现了一个必现内存溃散的操作途径,所以我尝试在 Flutter 2.5.3 版别和 2.10.5 版别各自测试了一下内存状况:

解决 Flutter 引起的 iOS 内存崩溃问题
比照内存状况能够得出一个结论:晋级前内存容忍度更高,1.2G 峰值都没问题;晋级后内存容忍度更低,1.1G 峰值就溃散。

这让我联想到了「紧缩内存」:iOS 体系会在内存严重的时分,把一部分不必的内存做紧缩,以腾出内存空间。在需求读取这些紧缩内存的时分,也需求先解压再读取。

听起来很好的机制,为什么会出问题呢?有一个经典事例:

  • SDWebImage是 iOS 开发中常用的第三方图片缓存库,它会将使用过的图片缓存在内存中,以供后续快速复用,一起在内存严重的时分会释放掉缓存。有一个细节是,SDWebImage 早期是将缓存放在 NSMutableDictionary 中,这会使得部分图片缓存在一段时刻不必后就被体系紧缩了。当内存峰值来暂时,体系会发送一个内存警告,SDWebImage 在收到警告的时分会选择释放掉缓存。还记得吗?释放之前要先解压,才干释放。在解压的一会儿,内存峰值被推得更高,所以体系就杀掉进程,制造了一次经典的 OOM。后来SDWebImage 采用了体系供给的 NSCache 来做缓存,NSCache 有专门针对内存紧缩做优化,才处理了此问题。

所以,顺藤摸瓜,我在 Flutter 的 issue 中查找了几个关键词:iOScompress memory,榜首个帖子就证明了我的猜测:

解决 Flutter 引起的 iOS 内存崩溃问题

文中提到了几个关键点:

  1. 2.5.3 之后的版别,内存溃散都开端变得多
  2. 2.5.3 之后的版别,Flutter 确实改变了内存策略,采用了紧缩内存的办法(贴子中叫做紧缩指针)
  3. 有人实验性地关掉了紧缩内存,处理了此问题

结合咱们晋级的版别便是2.5.3 → 2.10.5,基本上能够锁定便是这个紧缩内存的问题了。

两种计划

现在有两个处理计划:

  • 计划一:等待 Flutter 官方处理,咱们再晋级版别就好。
  • 计划二:咱们自己魔改 Flutter Engine 源码,关掉内存紧缩。

魔改 Flutter Engine 源码的成本其实是很高的,要理解 Flutter Engine 和 Flutter 的依靠联系,构建办法,以及 Flutter Engine 代码逻辑等等。

原本是想等待计划一的,可是跟着后台用户反应越来越多,处理内存导致的溃散现已刻不容缓了,咱们决议采取计划二。

客户投诉是榜首生产力?

所以我这个一行 Flutter 代码都没写过的人,就硬着头皮上了。在阅览了无数官方 / 民间文档之后,花了三天时刻,硬是整出来了,在 Flutter Engine 中加上了自定义打印:

解决 Flutter 引起的 iOS 内存崩溃问题

具体计划二是怎么处理问题的,下文细说。

碰巧的是,就在咱们用计划二处理问题之时,计划一也迎来了曙光:Flutter 紧迫发布了 3.0.5 版别,该版别中 Flutter Engine 封闭了内存紧缩。所以,咱们马上晋级尝试了一下,确实不会溃散了,咱们稍加适配,就上线了。现在依据线上数据反应,内存溃散问题现已完美处理。

Flutter Engine 定制与源码调试

接下来将具体介绍计划二的操作流程,先来个流程图:

解决 Flutter 引起的 iOS 内存崩溃问题

下载源码

查了一下 Flutter官方文档,发现下载源码就有一页文档,可想而知这个坑有多深

Fork Flutter Engine 库房

翻开github.com/flutter/eng…,fork 一份到自己的库房。比方,我的是github.com/JPlay/engin…。

fork 是为了修正源码后有个地方能存修正过的代码。(这儿 fork 就好,不必 clone)

安装 depot_tools

depot_tools是 Google 供给用来用来办理项目代码的东西集,它内含了许多套件,罗列一下咱们将会用到的:

  • gclient – 源码办理东西,能够帮助你拉取项目源码以及依靠
  • gn – 创立编译资料,特别适合 Flutter 这种跨渠道多编译方针的项目
  • ninja – 编译东西,担任编译 gn 生成的编译资料

开端安装 depot_tools:

$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

安装完结之后,在 ~/.bashrc或许 ~/.zshrc中参加以下指令,把depot_tools 设置成环境变量,便利后续使用:

export PATH=/path/to/depot_tools:$PATH

拉源码

不像往常咱们用 git 直接拉,这儿必须使用 gclient,因为有很多依靠只要 gclient 才干拉下来。

咱们先新建一个名为 engine 文件夹(名字随意),后续源码都会放在这儿,在 engine 里边新建一个配置文件,名字必须是.gclient,使用文本编辑器添加以下内容如下:

solutions = [
  {
    "managed": False,
    "name": "src/flutter",
    "url": "git@github.com:JPlay/engine.git@57d3bac3dd5cb5b0e464ab70e7bc8a0d8cf083ab",
    "custom_deps": {},
    "deps_file": "DEPS",
    "safesync_url": "",
  },
]

这儿要说一下 url 的值:

  • git@github.com:JPlay/engine.git 是我方才 fork 的 git 库房
  • 57d3bac3dd5cb5b0e464ab70e7bc8a0d8cf083ab 是咱们当时 Flutter 版别 2.10.5 对应的 commit id

在你的 Flutter 目录下的 /bin/internal/engine.version 能够找到,比方我的是:

$ cat /Users/JPlay/development/flutter/bin/internal/engine.version

完结配置之后,挂上代理,就能够在 engine 文件夹下,履行拉代码的操作了:

$ gclient sync --verbose

这儿必须着重一下,这儿的代码量超越 10GB,进程适当慢。假如中途有任何报错或许卡住,基本上都是网络问题,主张仔细看下日志,大部分是 clone 某个库房失利或许拜访地址失利,主张用 git clone 或许 curl 试试看网络是否晓畅。

PS:我的榜首个代理便是能拉大部分代码,而小部分代码死活拉不下来而浪费了我大半天时刻,后来换了一个代理就顺利拉下来了。

成功之后,你会发现代码全都集中在 engine/src 目录下,类似这样:

解决 Flutter 引起的 iOS 内存崩溃问题

后续假如想再切换 engine 的分支,能够先进入 /src/flutter,然后履行:

$ git reset --hard <commit id>
$ gclient sync --with_branch_heads --with_tags

编译

接着便是编译,咱们会分两个步骤:

  1. 用 gn 创立编译资料
  2. 用 ninja 履行编译

想简单了解一下 gn 和 ninja 的看这儿,想具体了解 gn 的看这儿,想具体了解 ninja 的看这儿

值得一提的是,因为 Flutter 的编译产品是分渠道的,咱们现在主要需求的是 iOS 和 Android,这在 macOS 上都能搞定。

在编译 iOS / Android 产品的一起,还需求而外编译一个 host 产品,这是因为咱们需求编译出一个与当时版别对应的的 Dark SDK。

因为代码版别、方针渠道、方针架构都不仅有,所以接下来拿 iOS arm64 方针来举例,其他状况请酌情仿制。

创立编译资料

gn 供给了一堆参数来帮助咱们创立编译资料:

usage: gn [-h] [--unoptimized] [--enable-unittests]
          [--runtime-mode {debug,profile,release,jit_release}] [--interpreter]
          [--dart-debug] [--no-dart-version-git-info] [--full-dart-debug]
          [--target-os {android,ios,mac,linux,fuchsia,win,winuwp}] [--android]
          [--android-cpu {arm,x64,x86,arm64}] [--ios] [--ios-cpu {arm,arm64}]
          [--mac] [--mac-cpu {x64,arm64}] [--simulator] [--linux] [--fuchsia]
          [--winuwp] [--linux-cpu {x64,x86,arm64,arm}]
          [--fuchsia-cpu {x64,arm64}] [--windows-cpu {x64,arm64}]
          [--simulator-cpu {x64,arm64}] [--arm-float-abi {hard,soft,softfp}]
          [--goma] [--no-goma] [--xcode-symlinks] [--no-xcode-symlinks]
          [--depot-tools DEPOT_TOOLS] [--lto] [--no-lto] [--clang]
          [--no-clang] [--clang-static-analyzer] [--no-clang-static-analyzer]
          [--target-sysroot TARGET_SYSROOT]
          [--target-toolchain TARGET_TOOLCHAIN]
          [--target-triple TARGET_TRIPLE]
          [--operator-new-alignment OPERATOR_NEW_ALIGNMENT]
          [--macos-enable-metal] [--enable-vulkan] [--enable-fontconfig]
          [--enable-vulkan-validation-layers] [--enable-skshaper]
          [--no-enable-skshaper] [--always-use-skshaper]
          [--embedder-for-target] [--coverage] [--out-dir OUT_DIR]
          [--full-dart-sdk] [--no-full-dart-sdk] [--ide IDE]
          [--disable-desktop-embeddings] [--build-glfw-shell]
          [--no-build-glfw-shell] [--build-embedder-examples]
          [--no-build-embedder-examples] [--bitcode] [--stripped]
          [--no-stripped] [--prebuilt-dart-sdk] [--no-prebuilt-dart-sdk]
          [--fuchsia-target-api-level FUCHSIA_TARGET_API_LEVEL]
          [--use-mallinfo2] [--asan] [--lsan] [--msan] [--tsan] [--ubsan]
          [--trace-gn] [--verbose]

这儿咱们会用到几个:

参数名 说明
–unoptimized 默许是会进行优化(optimized)的,假如设置了不优化(unoptimized),则编译产品会留下一些便利调试的内容,比方:log, asset,dSYM 之类的
–simulator 接在渠道之后,指定方针是否为模拟器
–runtime-mode 指定方针运转时模式,有debug,profile,release,jit_release,概况看官方文档
–ios-cpu / –android-cpu 指定方针 CPU 架构,iOS 有 arm 和 arm64,Android 有arm,x64,x86,arm64
–ios –android 指定方针渠道,假如是编译 host,则不需求设置此参数

具体说明能够输入:/path/to/gn --help 查看

咱们在src/目录下创立一个 iOS 调试用的编译资料:

$ ./flutter/tools/gn --runtime-mode=debug --unoptimized
$ ./flutter/tools/gn --ios --runtime-mode=debug --unoptimized

榜首行是生成 host 资料,第二行是 iOS 资料(没有输入架构,默许是 arm64) 。所以在src/out/ 下新增了两个文件夹,这些便是编译资料:

解决 Flutter 引起的 iOS 内存崩溃问题

履行编译

资料准备好了,咱们就要开端编译了,假如你是 Intel CPU 的 Mac(x64 架构),那将一切顺利,直接履行指令就行:

$ ninja -C out/ios_debug_unopt && ninja -C out/host_debug_unopt

可是,假如你是 M 系列的 Mac(arm64 架构)那就需求折腾一番了(我估计咱们都是_):

  1. 修正/src/flutter/sky/tools/create_macos_gen_snapshots.py

解决 Flutter 引起的 iOS 内存崩溃问题

  1. 修正 /src/flutter/lib/snapshot/BUILD.gn

解决 Flutter 引起的 iOS 内存崩溃问题

3.修正 /src/third_party/dart/runtime/BUILD.gn

解决 Flutter 引起的 iOS 内存崩溃问题

以上修正都是为了处理「构建脚本默许把编译的 host 机器认为是 x64 架构」,而咱们做的修正便是为了适配的 arm64 架构。

因为编译脚本经常更新,所以以上修正计划或许只对当时 commit 收效,不过我总结出一些经验便利咱们修正脚本:

  1. 关注你的 host 和 target 体系和架构,一般需求修正的点都是环绕这些参数展开
  2. gen_snapshot 是 Dart 的编译产品,要保证它放在正确的文件夹,而且被正确调用
  3. 巧用调试打印大法,需求修正的 .gn .py 文件都能够用 print 打印参数,假如不熟悉能够快速预览一下gn和Python的语法(我便是这样)
  4. 仔细看报错信息,都说得十分具体,完全能够顺藤摸瓜处理问题
  5. 最好的办法仍是找一台 x64 的 Mac

这个修正计划是我个人的暂时计划,issue 中也有一些大神的其他思路,能够参阅:github.com/flutter/flu…

修正源码

一切顺利的话,咱们闯过了编译这一关。现在能够修正源码了,我这儿随便举个例子,只为了证明咱们修正源码成功:

解决 Flutter 引起的 iOS 内存崩溃问题
/src/flutter/shell/common/engine.cc 的 Run 办法中参加一个打印信息,这会让 engine 在启动的时分就打印这条信息。

别忘了咱们的初衷:在 /src/flutter/tools/gn中封闭 iOS 的内存紧缩,以处理内存问题:

解决 Flutter 引起的 iOS 内存崩溃问题

修正完之后,重新编译一下:(这次是增量更新,很快):

$ ninja -C out/ios_debug_unopt && ninja -C out/host_debug_unopt

接着,进入一个 Flutter 项目目录,履行:

$ flutter run --local-engine-src-path=/path/to/engine/src/ --local-engine=ios_debug_unopt

能够看到控制台输出:

解决 Flutter 引起的 iOS 内存崩溃问题

使用成功运转了起来,而且输出了咱们自定义的信息。到此咱们取得了阶段性的成功,现已把咱们修正的代码成功在 Flutter 项目中运转起来了

源码调试

Flutter官方文档关于调试部分写的十分完整了,我这儿只举一个 Xcode 源码调试的例子。

咱们翻开一个 Flutter 项目,比方,Runner.xcworkspace,因为方才咱们跑过:

$ flutter run --local-engine-src-path=/path/to/engine/src/ --local-engine=ios_debug_unopt

所以,Generated.xcconfig 文件现已现已被设置了相关参数(没有的话自己设置一下):

解决 Flutter 引起的 iOS 内存崩溃问题

接着把 /src/out/ios_debug_unopt/flutter_engine.xcodeproj 拖到 Runner 项目中:

解决 Flutter 引起的 iOS 内存崩溃问题

找个会运转到的地方下个断点,比方 FlutterAppDelegate.mm 中 – init 办法。运转项目:

解决 Flutter 引起的 iOS 内存崩溃问题

断点成功,接着就能够愉快地调试了。

总结

这次问题排查真的很像一次探案进程,依据蛛丝马迹一点点找出头绪,最终处理问题。进程中虽然踩了不少坑,可是一路走到最后仍是感觉很有一种推理断案的爽快感。特此分享出来,希望能帮咱们处理相同的内存问题。