swift 与 OC 混编引发了一个隐式强制解包 Crash,由于经验不足走了一点弯路。
Crash 信息
Crash 信息大致如下:
[inlined: Swift runtime failure: Unexpectedly found nil while implicitly unwrapping an Optional value
源代码如下:
@implementation HTTPFileResponse
- (id)initWithFilePath:(NSString *)fpath forConnection:(HTTPConnection *)parent {...}
...
class MyHTTPFileResponse: HTTPFileResponse {
let extraHeaders: [String: String]?
init(filePath: String, for connection: HTTPConnection, extraHeaders: [String: String]?) {
self.extraHeaders = extraHeaders
// crash 在这里
super.init(filePath: filePath, for: connection)
}
...
分析
只能看出是隐式强制解包引起的,直观上看可能的变量就是 init 函数这三个入参,但从逻辑上不应该有解包操作,比较奇怪,直接 debug 吧。
发现 Crash 字符串读取指令:
0x1049d7dc8 <+48>: add x8, x8, #0x1d0 ; "Unexpectedly found nil while implicitly unwrapping an Optional value"
0x1049d7dcc <+52>: str x8, [sp, #0x38]
那就找到读取sp, #0x38
的位置:
0x1049d7ecc <+308>: ldr x3, [sp, #0x38]
…
0x1049d7f0c <+372>: bl 0x104ad4058 ; symbol stub for: Swift._assertionFailure(_: Swift.StaticString, _: Swift.StaticString, file: Swift.StaticString, line: Swift.UInt, flags: Swift.UInt32) -> Swift.Never
Swift._assertionFailure
那这就是抛出 Crash 的函数了,往前面追溯,看什么分支会走到这个函数:
0x1049d7ea8 <+272>: ldur x0, [x29, #-0x40]
0x1049d7eac <+276>: subs x8, x0, #0x0
0x1049d7eb0 <+280>: cset w8, eq
0x1049d7eb4 <+284>: tbnz w8, #0x0, 0x1049d7ec8 ; <+304> at MyHTTPFileResponse.swift
0x1049d7eb8 <+288>: b 0x1049d7ebc ; <+292> at MyHTTPFileResponse.swift
0x1049d7ebc <+292>: ldur x8, [x29, #-0x40]
0x1049d7ec0 <+296>: str x8, [sp, #0x28]
0x1049d7ec4 <+300>: b 0x1049d7f14 ; <+380> at <compiler-generated>
0x1049d7ec8 <+304>: ldr x6, [sp, #0x40]
0x1049d7ecc <+308>: ldr x3, [sp, #0x38]
+284
行判定若w8
第 0 位不为 0 则跳转到 Crash 链路,根据前面指令可知,需要[x29, #-0x40]
读取的值为空。
0x1049d7e8c <+244>: bl 0x104ad335c ; symbol stub for: objc_msgSendSuper2
0x1049d7e90 <+248>: mov x8, x0
0x1049d7e94 <+252>: ldur x0, [x29, #-0x50]
0x1049d7e98 <+256>: stur x8, [x29, #-0x40]
这个值来自于objc_msgSendSuper2
函数返回值,我们知道这是调用父类函数,断点在这个函数,把入参x0
信息打出来(swift lldb 打印寄存器信息比较麻烦):
(lldb) re read x0
x0 = 0x00000002832b1bc0
// 展开内存数据
(lldb) x 0x00000002832b1bc0
0x2832b1bc0: b5 72 a0 00 01 00 00 03 80 b6 9b 82 02 00 00 00 .r..............
0x2832b1bd0: 90 86 61 01 01 00 00 00 00 36 7f 80 02 00 00 00 ..a......6......
// 找到 isa
(lldb) p/t 0x0300000100a072b5
(Int) 0b0000001100000000000000000000000100000000101000000111001010110101
// 查看 class 变量指针信息
(lldb) image lookup -a 0b100000000101000000111001010110000
Address: AnyDemo[0x00000001001eb2b0] (AnyDemo.__DATA.__objc_data + 10248)
Summary: (void *)0x0000000100a0e0d0: _Any0000MyHTTPFileResponse
再看一下调用代码就瞬间明白了:
init(filePath: String, for connection: HTTPConnection, extraHeaders: [String: String]?) {
self.extraHeaders = extraHeaders
// crash 在这里
super.init(filePath: filePath, for: connection)
}
结论
在代码层面,swift init
函数省略了返回值,但这个代码隐式表示返回值为非可选类型,所以在指令层面对super.init
返回值做了检查,如果返回为空则直接报错,又由于super.init
是 OC 代码类型不安全,所以也能编译通过。
触发 crash 原因就是 OC 这个构造函数可能返回nil
,最终解法就是把 swift 构造函数定义为可空构造函数init?
。
也汲取了一个经验,对于 swift 重写 OC 带返回值的函数,最好无脑把 swift 返回值定义为可选类型,因为你无法预估在将来的迭代过程中这个父类的 OC 函数会不会突然返回nil
。
网友评论