背景
假如你的 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 版别各自测试了一下内存状况:
比照内存状况能够得出一个结论:晋级前内存容忍度更高,1.2G 峰值都没问题;晋级后内存容忍度更低,1.1G 峰值就溃散。
这让我联想到了「紧缩内存」:iOS 体系会在内存严重的时分,把一部分不必的内存做紧缩,以腾出内存空间。在需求读取这些紧缩内存的时分,也需求先解压再读取。
听起来很好的机制,为什么会出问题呢?有一个经典事例:
- SDWebImage是 iOS 开发中常用的第三方图片缓存库,它会将使用过的图片缓存在内存中,以供后续快速复用,一起在内存严重的时分会释放掉缓存。有一个细节是,SDWebImage 早期是将缓存放在 NSMutableDictionary 中,这会使得部分图片缓存在一段时刻不必后就被体系紧缩了。当内存峰值来暂时,体系会发送一个内存警告,SDWebImage 在收到警告的时分会选择释放掉缓存。还记得吗?释放之前要先解压,才干释放。在解压的一会儿,内存峰值被推得更高,所以体系就杀掉进程,制造了一次经典的 OOM。后来SDWebImage 采用了体系供给的 NSCache 来做缓存,NSCache 有专门针对内存紧缩做优化,才处理了此问题。
所以,顺藤摸瓜,我在 Flutter 的 issue 中查找了几个关键词:iOS
compress
memory
,榜首个帖子就证明了我的猜测:
文中提到了几个关键点:
- 2.5.3 之后的版别,内存溃散都开端变得多
- 2.5.3 之后的版别,Flutter 确实改变了内存策略,采用了紧缩内存的办法(贴子中叫做紧缩指针)
- 有人实验性地关掉了紧缩内存,处理了此问题
结合咱们晋级的版别便是2.5.3 → 2.10.5,基本上能够锁定便是这个紧缩内存的问题了。
两种计划
现在有两个处理计划:
- 计划一:等待 Flutter 官方处理,咱们再晋级版别就好。
- 计划二:咱们自己魔改 Flutter Engine 源码,关掉内存紧缩。
魔改 Flutter Engine 源码的成本其实是很高的,要理解 Flutter Engine 和 Flutter 的依靠联系,构建办法,以及 Flutter Engine 代码逻辑等等。
原本是想等待计划一的,可是跟着后台用户反应越来越多,处理内存导致的溃散现已刻不容缓了,咱们决议采取计划二。
客户投诉是榜首生产力?
所以我这个一行 Flutter 代码都没写过的人,就硬着头皮上了。在阅览了无数官方 / 民间文档之后,花了三天时刻,硬是整出来了,在 Flutter Engine 中加上了自定义打印:
具体计划二是怎么处理问题的,下文细说。
碰巧的是,就在咱们用计划二处理问题之时,计划一也迎来了曙光:Flutter 紧迫发布了 3.0.5 版别,该版别中 Flutter Engine 封闭了内存紧缩。所以,咱们马上晋级尝试了一下,确实不会溃散了,咱们稍加适配,就上线了。现在依据线上数据反应,内存溃散问题现已完美处理。
Flutter Engine 定制与源码调试
接下来将具体介绍计划二的操作流程,先来个流程图:
下载源码
查了一下 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 目录下,类似这样:
后续假如想再切换 engine 的分支,能够先进入 /src/flutter,然后履行:
$ git reset --hard <commit id>
$ gclient sync --with_branch_heads --with_tags
编译
接着便是编译,咱们会分两个步骤:
- 用 gn 创立编译资料
- 用 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/ 下新增了两个文件夹,这些便是编译资料:
履行编译
资料准备好了,咱们就要开端编译了,假如你是 Intel CPU 的 Mac(x64 架构),那将一切顺利,直接履行指令就行:
$ ninja -C out/ios_debug_unopt && ninja -C out/host_debug_unopt
可是,假如你是 M 系列的 Mac(arm64 架构)那就需求折腾一番了(我估计咱们都是_):
- 修正/src/flutter/sky/tools/create_macos_gen_snapshots.py
- 修正 /src/flutter/lib/snapshot/BUILD.gn
3.修正 /src/third_party/dart/runtime/BUILD.gn
以上修正都是为了处理「构建脚本默许把编译的 host 机器认为是 x64 架构」,而咱们做的修正便是为了适配的 arm64 架构。
因为编译脚本经常更新,所以以上修正计划或许只对当时 commit 收效,不过我总结出一些经验便利咱们修正脚本:
- 关注你的 host 和 target 体系和架构,一般需求修正的点都是环绕这些参数展开
- gen_snapshot 是 Dart 的编译产品,要保证它放在正确的文件夹,而且被正确调用
- 巧用调试打印大法,需求修正的 .gn .py 文件都能够用 print 打印参数,假如不熟悉能够快速预览一下gn和Python的语法(我便是这样)
- 仔细看报错信息,都说得十分具体,完全能够顺藤摸瓜处理问题
- 最好的办法仍是找一台 x64 的 Mac
这个修正计划是我个人的暂时计划,issue 中也有一些大神的其他思路,能够参阅:github.com/flutter/flu…
修正源码
一切顺利的话,咱们闯过了编译这一关。现在能够修正源码了,我这儿随便举个例子,只为了证明咱们修正源码成功:
在/src/flutter/shell/common/engine.cc 的 Run 办法中参加一个打印信息,这会让 engine 在启动的时分就打印这条信息。
别忘了咱们的初衷:在 /src/flutter/tools/gn中封闭 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 项目中运转起来了。
源码调试
Flutter官方文档关于调试部分写的十分完整了,我这儿只举一个 Xcode 源码调试的例子。
咱们翻开一个 Flutter 项目,比方,Runner.xcworkspace,因为方才咱们跑过:
$ flutter run --local-engine-src-path=/path/to/engine/src/ --local-engine=ios_debug_unopt
所以,Generated.xcconfig 文件现已现已被设置了相关参数(没有的话自己设置一下):
接着把 /src/out/ios_debug_unopt/flutter_engine.xcodeproj 拖到 Runner 项目中:
找个会运转到的地方下个断点,比方 FlutterAppDelegate.mm 中 – init 办法。运转项目:
断点成功,接着就能够愉快地调试了。
总结
这次问题排查真的很像一次探案进程,依据蛛丝马迹一点点找出头绪,最终处理问题。进程中虽然踩了不少坑,可是一路走到最后仍是感觉很有一种推理断案的爽快感。特此分享出来,希望能帮咱们处理相同的内存问题。