事情经过
近日发现一个发通知时触发的 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) |
把它改成 Notification
就好了:
NotificationCenter.default.addObserver(self, selector: #selector(self.onReceive(_:)), name: .init("test"), object: nil) |
为了验证修改的有效性,我新建了一个 demo 来测试,发现这样写是不会崩溃的:
func setup() { |
这就变得诡异起来,在项目里确实按照上述改法验证了是有效的,会不会是混编的问题?于是我写了个 OC 来混编测试一下,果然如此:
+ (void)test { |
func setup() { |
同时还发现了这个 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 有关:
// 闭包参数 |
ObjC 发送通知时发现并不存在所谓的 onReceive
方法指针,只有 onReceiveWithOther:
指针,于是就崩溃了。
为什么两种参数导致的崩溃类型不一样?
参数类型不对导致崩溃这很符合预期,但奇怪的是为什么闭包类型的崩溃是 EXC_BAD_ACCESS 而其它类型则是 unrecognized selector sent to instance …
暂时还没有头绪