事情经过

近日发现一个发通知时触发的 EXC_BAD_ACCESS 崩溃,在 DEBUG 时,崩溃指向这一行代码:

崩溃行
[NSNotificationCenter.defaultCenter postNotificationName:@"test" object:nil];

崩溃信息为:

崩溃信息
Thread 1: EXC_BAD_ACCESS (code=1, address=0x8)

经过半天排查(此处省略100万字)发现竟是订阅端设置的参数类型不匹配导致的:

NotificationCenter.default.addObserver(self, selector: #selector(self.onReceive), name: .init("test"), object: nil)
...
@objc func onReceive(other: (() -> Void)? = nil) {
Capsule("收到通知")
}

把它改成 Notification 就好了:

NotificationCenter.default.addObserver(self, selector: #selector(self.onReceive(_:)), name: .init("test"), object: nil)
...
@objc func onReceive(_ sender: Notification?) {
Capsule("收到通知")
}

为了验证修改的有效性,我新建了一个 demo 来测试,发现这样写是不会崩溃的:

func setup() {
list.add(title: "test") { section in
section.add(title: "设置 Observer") {
NotificationCenter.default.addObserver(self, selector: #selector(self.onReceive), name: .init("test"), object: nil)
}
section.add(title: "发通知") {
NotificationCenter.default.post(name: .init("test"), object: nil)
}
}
}
@objc func onReceive(other: (() -> Void)? = nil) {
Capsule("收到通知")
}

这就变得诡异起来,在项目里确实按照上述改法验证了是有效的,会不会是混编的问题?于是我写了个 OC 来混编测试一下,果然如此:

发送端
+ (void)test {
[NSNotificationCenter.defaultCenter postNotificationName:@"test" object:nil];
}
订阅端
func setup() {
list.add(title: "test") { section in
section.add(title: "设置 Observer") {
NotificationCenter.default.addObserver(self, selector: #selector(self.onReceive), name: .init("test"), object: nil)
}
section.add(title: "发通知") {
OC.test()
}
}
}
@objc func onReceive(other: (() -> Void)? = nil) {
Capsule("收到通知")
}

同时还发现了这个 crash 也受 Xcode 缓存影响,如果此时把 OC.test() 改回由 Swift 发送通知,还是会崩溃,但是 clean 之后再运行就不会崩溃了。

另外,如果不匹配的 other 参数改成别的类型,例如 String,则崩溃信息会比较明确:

Thread 1: "-[NSConcreteNotification length]: unrecognized selector sent to instance 0x600000272b80"

而且崩溃在 AppDelegate 这行:

class AppDelegate: UIResponder, UIApplicationDelegate {

总结与思考

这个崩溃信息很有误导性,且复现路径比较苛刻,稍有一个条件不满足就不会复现:

  • ObjC 和 Swift 混编
  • Swift 订阅,参数设置为可选的闭包类型,且默认值为 nil
  • 由 ObjC 端进行发送

尽管到现在为止这个 BUG 已经变得很可控了,但还是有些地方不明白需要继续探索答案:

为什么纯 Swift 不会崩溃,而混编时却会崩溃?

我猜测大概跟 Xcode 生成的 ObjC Header 有关:

Demo-Swift.h
// 闭包参数
- (void)onReceiveWithOther:(void (^ _Nullable)(void))other;
// 字符串参数
- (void)onReceiveWithOther:(NSString * _Nullable)other;

ObjC 发送通知时发现并不存在所谓的 onReceive 方法指针,只有 onReceiveWithOther: 指针,于是就崩溃了。

为什么两种参数导致的崩溃类型不一样?

参数类型不对导致崩溃这很符合预期,但奇怪的是为什么闭包类型的崩溃是 EXC_BAD_ACCESS 而其它类型则是 unrecognized selector sent to instance …

暂时还没有头绪