swift 與 OC 混編引發(fā)了一個(gè)隱式強(qiáng)制解包 Crash冷尉,由于經(jīng)驗(yàn)不足走了一點(diǎn)彎路。
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)
}
...
分析
只能看出是隱式強(qiáng)制解包引起的廉涕,直觀上看可能的變量就是 init 函數(shù)這三個(gè)入?yún)ⅲ珡倪壿嬌喜粦?yīng)該有解包操作艇拍,比較奇怪狐蜕,直接 debug 吧。
發(fā)現(xiàn) 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 的函數(shù)了卸夕,往前面追溯层释,看什么分支會(huì)走到這個(gè)函數(shù):
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 則跳轉(zhuǎn)到 Crash 鏈路,根據(jù)前面指令可知快集,需要[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]
這個(gè)值來(lái)自于objc_msgSendSuper2
函數(shù)返回值,我們知道這是調(diào)用父類函數(shù)个初,斷點(diǎn)在這個(gè)函數(shù)乖寒,把入?yún)?code>x0信息打出來(lái)(swift lldb 打印寄存器信息比較麻煩):
(lldb) re read x0
x0 = 0x00000002832b1bc0
// 展開(kāi)內(nèi)存數(shù)據(jù)
(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
再看一下調(diào)用代碼就瞬間明白了:
init(filePath: String, for connection: HTTPConnection, extraHeaders: [String: String]?) {
self.extraHeaders = extraHeaders
// crash 在這里
super.init(filePath: filePath, for: connection)
}
結(jié)論
在代碼層面,swift init
函數(shù)省略了返回值勃黍,但這個(gè)代碼隱式表示返回值為非可選類型,所以在指令層面對(duì)super.init
返回值做了檢查晕讲,如果返回為空則直接報(bào)錯(cuò)覆获,又由于super.init
是 OC 代碼類型不安全,所以也能編譯通過(guò)瓢省。
觸發(fā) crash 原因就是 OC 這個(gè)構(gòu)造函數(shù)可能返回nil
弄息,最終解法就是把 swift 構(gòu)造函數(shù)定義為可空構(gòu)造函數(shù)init?
。
也汲取了一個(gè)經(jīng)驗(yàn)勤婚,對(duì)于 swift 重寫 OC 帶返回值的函數(shù)摹量,最好無(wú)腦把 swift 返回值定義為可選類型,因?yàn)槟銦o(wú)法預(yù)估在將來(lái)的迭代過(guò)程中這個(gè)父類的 OC 函數(shù)會(huì)不會(huì)突然返回nil
馒胆。