背景
- 项目是一个 Flutter 和 Swift 混合项目,售前页是 H5
- Flutter 没有引入 flutter_boast 等框架,还是一个 FlutterViewController 一个 engine 的模式
- 新版本上线带上了
Swift 5.3和Flutter 1.22.32 个框架更新
前情提要
新版本上线之后频频 crash,crash 率迅速的冲上了 1%。
但是堆栈信息不明确,无法定位到具体问题,只能看到是在售前页发生了 crash,而且和 Flutter 有关,KERN_INVALID_ADDRESS说明是出现了僵尸对象,但和前端共同定位一番之后无果,只能暂时搁置。
EXC_BAD_ACCESS KERN_INVALID_ADDRESS 0x0000000000000010
libobjc.A.dylib
objc_msgSend + 16
1 Flutter (缺少)
2 UIKitCore -[UITextSelection commit] + 256
3 UIKitCore -[UITextInteractionAssistant(UITextInteractionAssistant_Internal) activateSelection] + 48
14 WebKit -[WKContentView(WKInteraction) becomeFirstResponderForWebView] + 352
15 WebKit -[WKWebView(WKViewInternalIOS) becomeFirstResponder] + 144
直到测试发现某台测试机可以稳定复现,并提供了复现路径。
在注册后打开售前页,在售前页点击左右切换 sku,必崩。
正文
一
此时是下午一点半,拿到测试机连上 Xcode 调试,按照测试提供的复现路径走了一遍,没有 crash
下载了一个 staging 包走了一遍,crash 了
试了多次还是一样的结果
基于 crash 信息里给的僵尸对象,我开启了 Zombie 检测,但也没有得到什么有效信息
对比了一下2者的区别,Flutter 一个是 debug 模式,一个是 release 模式
然后将本地调试的 Flutter 环境设置为 release 模式之后,终于复现了
此时距离开始调试这个 crash ,已经过去了近 3 个小时
二
复现归复现,但崩溃直接崩在了 main 函数这里,没有任何有效信息。
这时候有个小伙伴教了我一个方式,可以用bt命令显示当前现成调用栈,但是信息还是很少,和上面的 crash 的堆栈信息基本一致。
问题可能出现在1 Flutter (缺少)这里,但看不到,也很尴尬
三
必现路径上,有一个现象是注册崩,登录不崩
在抹平了注册和登录的代码的各种差异后,依然是这样,暂时陷入了僵局
这时候已经晚上八点半了,百般尝试无果之后暂时选择下班回家
在路上我突然想到,Flutter 有 3 种模式 debug profile 和 release,那使用 profile 模式跑一下会不会有新的发现
到家之后,在排除了路上想到的各种可能性后,我最终尝试使用 profile 模式跑了一下
果然,
Flutter(缺少)变成了Flutter -[FlutterTextInputView updateEditingState],这时候,我仿佛看到了曙光
四
打开 github,搜索 flutter/engine,看到了这个方法大概是在键盘的各种监听的时候会触发
但是,注册的 FlutterViewController 在打开售前页之前已经关闭了,为什么在售前页切换 sku 会触发这个方法呢
为了确认是调用了这个方法,我下了个条件断点
breakpoint set --func-regex updateEditingState
果然,在切换 sku 的时候,这个断点被触发了
但有效信息还是不足,继续翻 github,直到看到
Flutter 源码在修饰 delegate 的时候使用了
assign
啊这,这不是面试经常问到的么,delegate 应该用 weak 修饰啊
Flutter 开发者应该考虑到 assign 和 weak 相比更省内存吧
但这时候,一条链在我脑海里串了起来
应该是内存泄漏导致某个地方还持有者这个 delegate
由于使用的是 assign,所以持有的是这个 delegate 的内存地址
售前页在点击切换 sku 的时候触发了键盘操作,这个 delegate 的方法被调用
但由于 delegate 本身释放掉了,就引发了僵尸对象的问题
这和 firebase 上 crash 信息里 KERN_INVALID_ADDRESS也能对得上
五
我迅速去翻了下我们 flutter 的代码,在注册的验证码那里没有看到调用下面的方法
FocusScope.of(context).requestFocus(FocusNode());
但在登录和注册流程中 password 输入完之后点击下一步时调用了这个方法
迅速的加了这行代码之后,重新调试了一下,我已经准备好庆祝了
然而,还是崩溃了,但我觉得,问题应该就在这里
六
我重新看了一下项目中 Focus 相关的代码
发现 password focus 在 state 的 dispose 里也调用了 FocusNode 的 dispose 方法
但验证码里的 focus 没有调用这个方法
在加了 dispose 方法之后,测试了 3 遍之后,发现解决了这个问题
这时候,时间是凌晨一点半
后话
在解决了这个问题之后,其实还有一些事情可以去做
-
dispose 里做了什么,不调用为什么不会释放
-
前端在切换 sku 的时候,为什么会触发键盘事件
但这些问题是后续了,跟本文无关,故此不再赘叙