记一个由 FocusNode 未释放引发的 crash 问题

1,608 阅读4分钟

背景

  • 项目是一个 Flutter 和 Swift 混合项目,售前页是 H5
  • Flutter 没有引入 flutter_boast 等框架,还是一个 FlutterViewController 一个 engine 的模式
  • 新版本上线带上了 Swift 5.3Flutter 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 profilerelease,那使用 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 里也调用了 FocusNodedispose 方法

但验证码里的 focus 没有调用这个方法

在加了 dispose 方法之后,测试了 3 遍之后,发现解决了这个问题

这时候,时间是凌晨一点半

后话

在解决了这个问题之后,其实还有一些事情可以去做

  • dispose 里做了什么,不调用为什么不会释放

  • 前端在切换 sku 的时候,为什么会触发键盘事件

但这些问题是后续了,跟本文无关,故此不再赘叙