多角度體會 Swift 方法派發(fā)
作者丨鏡畫者
轉(zhuǎn)自:掘金
https://juejin.cn/post/7002201394916622343
我們知道 Swift 有三種方法派發(fā)方式:靜態(tài)派發(fā)(直接派發(fā))、VTable 派發(fā)(函數(shù)表派發(fā))、消息派發(fā)。下面我們分別從 SIL 中間語言,以及匯編的角度體會 Swift 的方法派發(fā)方式。
問題引子
在展開正文之前,我們先來看一個問題:
有一個 Framework (僅有一個類和一個方法)和一個 Swift App 工程(調(diào)用該方法),代碼如下,將 Framework 編譯后直接集成在 App 工程中:
// Framework
public class SwiftMethodDispatchTable {
public func getMethodName() -> String {
let name = "SwiftMethodDispatchTable"
print("Method name: \(name)")
return name
}
}
// App,ViewController.swift
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
readMethodName()
}
func readMethodName() {
let name = SwiftMethodDispatchTable().getMethodName()
print("read method name:", name)
}
}
我們?nèi)绻麑?Framework 中的 getMethodName 方法改個名字(getMethodName_new),重新編譯,并將編譯后的二進(jìn)制文件(如下圖所示)覆蓋 App 工程中集成的 Framework 二進(jìn)制文件,且 Headers 與 Modules 目錄保持不變,App 中調(diào)用的方法名稱也保持不變(仍然是 getMethodName)。
那么問題來了: App 重新編譯后執(zhí)行結(jié)果是什么?
-
編譯報錯 -
運行時報錯 -
正常運行
下面我們回到正題,最后我們再來回答這個問題。
SIL
我們先來看三個相似的類,它們都包含了使用不同派發(fā)方式的 getMethodName 方法,同時我們通過 printMethodName 方法來觀察它們的派發(fā)方式區(qū)別:
-
靜態(tài)派發(fā)
final public class SwiftMethodDispatchStatic {
public init() {}
func printMethodName() -> String {
let name = getMethodName()
return name
}
public func getMethodName() -> String {
let name = "SwiftMethodDispatchStatic"
print("Method name: \(name)")
return name
}
}
-
vtable 派發(fā)
public class SwiftMethodDispatchTable {
public init() {}
func printMethodName() -> String {
let name = getMethodName()
return name
}
public func getMethodName() -> String {
let name = "SwiftMethodDispatchTable"
print("Method name: \(name)")
return name
}
}
-
消息派發(fā)
public class SwiftMethodDispatchMessage {
public init() {}
func printMethodName() -> String {
let name = getMethodName()
return name
}
@objc dynamic public func getMethodName() -> String {
let name = "SwiftMethodDispatchMessage"
print("Method name: \(name)")
return name
}
}
下面我們分別生成 SIL 對比看看(命令行執(zhí)行 swiftc -emit-silgen -O xxx.swift 生成,以下摘錄關(guān)鍵部分)。
靜態(tài)派發(fā)
// SwiftMethodDispatchStatic.printMethodName()
sil hidden [ossa] @$s25SwiftMethodDispatchStaticAAC05printB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String {
// %0 "self" // users: %3, %1
bb0(%0 : @guaranteed $SwiftMethodDispatchStatic):
debug_value %0 : $SwiftMethodDispatchStatic, let, name "self", argno 1 // id: %1
// function_ref SwiftMethodDispatchStatic.getMethodName()
%2 = function_ref @$s25SwiftMethodDispatchStaticAAC03getB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String // users: %8, %5, %4
debug_value %3 : $String, let, name "name" // id: %4
%5 = begin_borrow %3 : $String // users: %7, %6
%6 = copy_value %5 : $String // user: %9
end_borrow %5 : $String // id: %7
destroy_value %3 : $String // id: %8
return %6 : $String // id: %9
} // end sil function '$s25SwiftMethodDispatchStaticAAC05printB4NameSSyF'
sil_vtable [serialized] SwiftMethodDispatchStatic {
#SwiftMethodDispatchStatic.init!allocator: (SwiftMethodDispatchStatic.Type) -> () -> SwiftMethodDispatchStatic : @$s25SwiftMethodDispatchStaticAACABycfC // SwiftMethodDispatchStatic.__allocating_init()
#SwiftMethodDispatchStatic.deinit!deallocator: @$s25SwiftMethodDispatchStaticAACfD // SwiftMethodDispatchStatic.__deallocating_deinit
}
在以上的 SIL 中重點看這兩行:
// function_ref SwiftMethodDispatchStatic.getMethodName()
%2 = function_ref @$s25SwiftMethodDispatchStaticAAC03getB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchStatic) -> @owned String // user: %3
function_ref 關(guān)鍵字表明 getMethodName 方法是通過方法指針來調(diào)用的,并且通過符號 s25SwiftMethodDispatchStaticAAC03getB4NameSSyF 來定位方法地址,同時 sil_vtable 中也未包含該方法。
vtable 派發(fā)
// SwiftMethodDispatchTable.printMethodName()
sil hidden [ossa] @$s24SwiftMethodDispatchTableAAC05printB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String {
// %0 "self" // users: %3, %2, %1
bb0(%0 : @guaranteed $SwiftMethodDispatchTable):
debug_value %0 : $SwiftMethodDispatchTable, let, name "self", argno 1 // id: %1
%2 = class_method %0 : $SwiftMethodDispatchTable, #SwiftMethodDispatchTable.getMethodName : (SwiftMethodDispatchTable) -> () -> String, $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String // users: %8, %5, %4
debug_value %3 : $String, let, name "name" // id: %4
%5 = begin_borrow %3 : $String // users: %7, %6
%6 = copy_value %5 : $String // user: %9
end_borrow %5 : $String // id: %7
destroy_value %3 : $String // id: %8
return %6 : $String // id: %9
} // end sil function '$s24SwiftMethodDispatchTableAAC05printB4NameSSyF'
sil_vtable [serialized] SwiftMethodDispatchTable {
#SwiftMethodDispatchTable.init!allocator: (SwiftMethodDispatchTable.Type) -> () -> SwiftMethodDispatchTable : @$s24SwiftMethodDispatchTableAACABycfC // SwiftMethodDispatchTable.__allocating_init()
#SwiftMethodDispatchTable.printMethodName: (SwiftMethodDispatchTable) -> () -> String : @$s24SwiftMethodDispatchTableAAC05printB4NameSSyF // SwiftMethodDispatchTable.printMethodName()
#SwiftMethodDispatchTable.getMethodName: (SwiftMethodDispatchTable) -> () -> String : @$s24SwiftMethodDispatchTableAAC03getB4NameSSyF // SwiftMethodDispatchTable.getMethodName()
#SwiftMethodDispatchTable.deinit!deallocator: @$s24SwiftMethodDispatchTableAACfD // SwiftMethodDispatchTable.__deallocating_deinit
}
在 vtable 的方式中,方法的引用方式就變成了:
%2 = class_method %0 : $SwiftMethodDispatchTable, #SwiftMethodDispatchTable.getMethodName : (SwiftMethodDispatchTable) -> () -> String, $@convention(method) (@guaranteed SwiftMethodDispatchTable) -> @owned String // user: %3
這里的 class_method 關(guān)鍵字表明 getMethodName 使用類對象方法的方式,即函數(shù)表的方式,通過 sil_vtable 中的信息也能印證這一點,SwiftMethodDispatchTable 類的 vtable 表包含了這個方法。
消息派發(fā)
// SwiftMethodDispatchMessage.printMethodName()
sil hidden [ossa] @$s26SwiftMethodDispatchMessageAAC05printB4NameSSyF : $@convention(method) (@guaranteed SwiftMethodDispatchMessage) -> @owned String {
// %0 "self" // users: %3, %2, %1
bb0(%0 : @guaranteed $SwiftMethodDispatchMessage):
debug_value %0 : $SwiftMethodDispatchMessage, let, name "self", argno 1 // id: %1
%2 = objc_method %0 : $SwiftMethodDispatchMessage, #SwiftMethodDispatchMessage.getMethodName!foreign : (SwiftMethodDispatchMessage) -> () -> String, $@convention(objc_method) (SwiftMethodDispatchMessage) -> @autoreleased NSString // user: %3
%3 = apply %2(%0) : $@convention(objc_method) (SwiftMethodDispatchMessage) -> @autoreleased NSString // user: %5
// function_ref static String._unconditionallyBridgeFromObjectiveC(_:)
%4 = function_ref @$sSS10FoundationE36_unconditionallyBridgeFromObjectiveCySSSo8NSStringCSgFZ : $@convention(method) (@guaranteed Optional<NSString>, @thin String.Type) -> @owned String // user: %7
%5 = enum $Optional<NSString>, #Optional.some!enumelt, %3 : $NSString // users: %9, %7
%6 = metatype $@thin String.Type // user: %7
%7 = apply %4(%5, %6) : $@convention(method) (@guaranteed Optional<NSString>, @thin String.Type) -> @owned String // users: %13, %10, %8
debug_value %7 : $String, let, name "name" // id: %8
destroy_value %5 : $Optional<NSString> // id: %9
%10 = begin_borrow %7 : $String // users: %12, %11
%11 = copy_value %10 : $String // user: %14
end_borrow %10 : $String // id: %12
destroy_value %7 : $String // id: %13
return %11 : $String // id: %14
} // end sil function '$s26SwiftMethodDispatchMessageAAC05printB4NameSSyF'
sil_vtable [serialized] SwiftMethodDispatchMessage {
#SwiftMethodDispatchMessage.init!allocator: (SwiftMethodDispatchMessage.Type) -> () -> SwiftMethodDispatchMessage : @$s26SwiftMethodDispatchMessageAACABycfC // SwiftMethodDispatchMessage.__allocating_init()
#SwiftMethodDispatchMessage.printMethodName: (SwiftMethodDispatchMessage) -> () -> String : @$s26SwiftMethodDispatchMessageAAC05printB4NameSSyF // SwiftMethodDispatchMessage.printMethodName()
#SwiftMethodDispatchMessage.deinit!deallocator: @$s26SwiftMethodDispatchMessageAACfD // SwiftMethodDispatchMessage.__deallocating_deinit
}
在消息派發(fā)中,引用方式變成了:
%2 = objc_method %0 : $SwiftMethodDispatchMessage, #SwiftMethodDispatchMessage.getMethodName!foreign : (SwiftMethodDispatchMessage) -> () -> String, $@convention(objc_method) (SwiftMethodDispatchMessage) -> @autoreleased NSString // user: %3
objc_method 關(guān)鍵字表明了方法已經(jīng)轉(zhuǎn)為了使用 OC 中的方法派發(fā)方式,即消息派發(fā),并且方法簽名中,返回類型已經(jīng)變?yōu)榱?NSString,vtable 中也沒有了 getMethodName 方法。
從 SIL 的角度看 Swift 中的方法派發(fā)方式,是不是很明白。
匯編
我們接著再從匯編層面看看以上幾種派發(fā)方式的區(qū)別。
在開始前我們先準(zhǔn)備點樣例代碼,在最前面的 App 工程中我們增加上面提到的幾個方法:
@IBAction func StaticDispatch(_ sender: Any) {
_ = SwiftMethodDispatchStatic().getMethodName()
}
@IBAction func VTableDispatch(_ sender: Any) {
_ = SwiftMethodDispatchTable().getMethodName()
}
@IBAction func MessageDispatch(_ sender: Any) {
_ = SwiftMethodDispatchMessage().getMethodName()
}
我們在調(diào)用處打上斷點,并通過匯編模式進(jìn)行調(diào)試觀察(選擇 Debug 菜單中的 Debug Workflow,勾選 Always Show Disassembly):
下面我們分別看看不同派發(fā)方式中匯編代碼的區(qū)別
靜態(tài)派發(fā)
下面是 StaticDispatch 方法對應(yīng)的匯編代碼:
在第 17 行 bl 0x1047414cc 這條指令后有一個注解:symbol stub for …getMethodName...,可以猜想一定和 getMethodName 方法有關(guān),那么我們看看 0x1047414c 這個地址到底指向什么。
bl 指令表示跳轉(zhuǎn)到指定的子程序名處執(zhí)行代碼
symbol stub 表示代碼的符號占位,實際代碼要根據(jù)占位符號進(jìn)行重新定位。
執(zhí)行以下命令:
image lookup --address 0x1047414cc
結(jié)果如下:
Address: SwiftMethodDispatchAppDemo[0x00000001000054cc] (SwiftMethodDispatchAppDemo.__TEXT.__stubs + 36)
Summary: SwiftMethodDispatchAppDemo`symbol stub for: SwiftMethodDispatch.SwiftMethodDispatchStatic.getMethodName() -> Swift.String
這里我們可以看到地址 0x1047414cc 對應(yīng)的偏移地址是 0x00000001000054cc,我們根據(jù)這個地址在 MachO 文件的 __TEXT,__stubs 這個 section 中進(jìn)行查找。
我們使用 MachOView 這個工具來查看 app 的二進(jìn)制文件信息
下載這個工程編譯運行即可:
github.com/emptyglass123/MachOView
打開 MachOView 后我們在 TEXT 段中找 __TEXT,__stubs 這個 section,查找偏移地址為 000054cc 的記錄:
對應(yīng)的值是一串符號:_$s19SwiftMethodDispatch0abC6StaticC03getB4NameSSyF,經(jīng)過 demangle 轉(zhuǎn)換后得到的值為:
SwiftMethodDispatch.SwiftMethodDispatchStatic.getMethodName() -> Swift.String
這與 image lookup 命令得到的結(jié)果一致,因為 SwiftMethodDispatch 是一個動態(tài)庫,以上符號需要在動態(tài)庫中進(jìn)行重定位,我們在 MachOView 中再打開 SwiftMethodDispatch。
在 Symbol Table 中搜索符號:_$s19SwiftMethodDispatch0abC6StaticC03getB4NameSSyF,該符號對應(yīng)的代碼段偏移地址是:32F4
繼續(xù)在 __TEXT,__text section 中查找 32F4 地址對應(yīng)的記錄,從下圖中標(biāo)記的這一行開始即是 getMethodName 這個方法:
在 Xcode 中調(diào)試 App 的匯編代碼可以對比代碼是一致的:
從上面的查找過程可以發(fā)現(xiàn) Swift 方法在使用靜態(tài)派發(fā)時,幾乎是直接使用了方法的內(nèi)存地址(因為是外部符號,需要經(jīng)過動態(tài)庫的符號重定位)。如果靜態(tài)派發(fā)的方法存儲在 App 的二進(jìn)制文件中,則調(diào)用的地址即為方法的內(nèi)存入口地址,無需任何轉(zhuǎn)換(可以自行驗證)。
vtable 派發(fā)
我們再看看 vtable 派發(fā)的情況,類似地通過斷點查看匯編代碼:
單步調(diào)試停止到第 16 行處,查看 x8 這個寄存器的值,在 Xcode 的debug 區(qū)執(zhí)行命令:
register read x8
結(jié)果如下:
x8 = 0x0000000100c782a0 type metadata for SwiftMethodDispatch.SwiftMethodDispatchTable
ldr x8, [x8, #0x60] 這條命令表示把 x8 + 0x60 所在內(nèi)存地址的數(shù)據(jù)(4字節(jié))讀取到 x8 寄存器中。
我們再看下 x8 + 0x60 的地址(0x0000000100c78300)是什么內(nèi)容:
image lookup --address 0x0000000100c78300
Address: SwiftMethodDispatch[0x0000000000008300] (SwiftMethodDispatch.__DATA.__data + 160)
Summary: type metadata for SwiftMethodDispatch.SwiftMethodDispatchTable + 96
這里存儲的是 SwiftMethodDispatchTable 這個類的 metadata 信息,內(nèi)部具體結(jié)構(gòu)還有待研究,不過可以知道的是對象定義的方法包含在其中。
再通過 MachOView 查看 SwiftMethodDispatch 的二進(jìn)制文件信息,定位偏移地址 0x8300 的內(nèi)容:
可以看到 0x8300 這個地址的值指向了另外一個地址:2CCC(Data LO 中低位地址在前,高位地址在后)
再重新定位 0x2ccc 這個地址指向的值,這是一個函數(shù)的入口地址:
我們在 Xcode 中繼續(xù)調(diào)試到 19 行,再進(jìn)入調(diào)用棧,可以看到進(jìn)入了 SwiftMethodDispatchTable.getMethodName() 方法,與上面看到的二進(jìn)制匯編代碼是一致的,通過 image lookup 查找偏移地址也是匹配的:
image lookup --address 0x100c72ccc
Address: SwiftMethodDispatch[0x0000000000002ccc] (SwiftMethodDispatch.__TEXT.__text + 152)
Summary: SwiftMethodDispatch`SwiftMethodDispatch.SwiftMethodDispatchTable.getMethodName() -> Swift.String at SwiftMethodDispatchTable.swift:18
到這一步已經(jīng)成功調(diào)用到了 getMethodName 方法。
從上面的過程我們可以看到,基于函數(shù)表派發(fā)的方式,調(diào)用方法時提供的是類的 metadata 數(shù)據(jù)的偏移地址(0x60),基于這個偏移地址可以再次定位到方法的實際入口地址??梢哉J(rèn)為經(jīng)歷了一個查表的過程,不過這張函數(shù)表在編譯時已經(jīng)確定了,Swift 動態(tài)庫提供的 swiftmodule 接口文件已經(jīng)足以在編譯期定位方法在 metadata 中的偏移地址。
消息派發(fā)
最后我們再看下消息派發(fā)的匯編代碼:
這次的代碼較多一點,我們單步調(diào)試停在第 16 行處,查看并計算 x8 + 0xb80 指向的地址:
(lldb) register read x8
x8 = 0x0000000100bb0000 (void *)0x00000001020acb98: ObjectiveC._convertBoolToObjCBool(Swift.Bool) -> ObjectiveC.ObjCBool
(lldb) image lookup --address 100bb0b80
Address: SwiftMethodDispatchAppDemo[0x000000010000cb80] (SwiftMethodDispatchAppDemo.__DATA.__objc_selrefs + 128)
Summary: "getMethodName"
根據(jù) cb80 這個偏移地址在 App 的二進(jìn)制文件中查找:
可以看到這個地址保存的內(nèi)容是一個指針,地址為 0x5A9C,再看看這個指針的內(nèi)容是什么:
0x5A9C 指向的內(nèi)容是一個字符串 getMethodName,其實 __TEXT,__objc_methname 這個 section 就是保存的 OC 方法 SEL 名稱。
在 Xcode 中運行至 17 行,再讀取 x8 寄存器的內(nèi)容,可以看到結(jié)果也是 getMethodName 這個字符串:
在 Xcode 中可以看到第 19 行調(diào)用了 objc_msgSend 這個方法,我們進(jìn)入方法調(diào)試:
走到第 16 行時停下,查看 x17 寄存器的值:
register read x17
x17 = 0x0000000100c7387c SwiftMethodDispatch`@objc SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String at <compiler-generated>
這個地址即是 getMethodName 對應(yīng) OC 方法的入口地址:
image lookup --address 0x0000000100c7387c
Address: SwiftMethodDispatch[0x000000000000387c] (SwiftMethodDispatch.__TEXT.__text + 3144) Summary: SwiftMethodDispatch`@objc SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String at
在動態(tài)庫的 MachO 中可以查看:
可以看到 getMethodName 其實對應(yīng)有 2 個方法,一個是 OC 版的,一個是 Swift 版的,在內(nèi)存中是 2 個地址:
@objc SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String
SwiftMethodDispatch.SwiftMethodDispatchMessage.getMethodName() -> Swift.String
在 Xcode 進(jìn)入 16 行的 br x17 指令,可以看到已經(jīng)進(jìn)入了 getMethodName 方法(OC 版):
這就證實了對 getMethodName 方法的調(diào)用已經(jīng)轉(zhuǎn)換成了對 getMethodName 這個消息的發(fā)送,走的是 OC 的消息發(fā)送邏輯(由 Swift 編譯生成的 OC 兼容版本),有興趣可以看看 objc_msgSend 的匯編解析。
從上面的過程可以看到,在 Swift 中如果方法被標(biāo)記為需要通過消息發(fā)送的方式執(zhí)行,那么方法的 SEL 就會存儲在二進(jìn)制中的 __TEXT,__objc_methname 這個 section 中,在調(diào)用時通過 SEL 來查找對應(yīng)的方法入口。
問題回顧
現(xiàn)在我們再回到最前面的問題,Swift 方法修改名稱后,在不修改接口信息的情況下,還能調(diào)用嗎。
根據(jù) Swift 方法派發(fā)的特性,問題中 getMethodName 方法使用的是函數(shù)表派發(fā),由于接口未改動,它的偏移地址是不變的,在 App 運行時編譯都是能正常通過的,在運行時通過類的 metadata 的偏移地址直接定位到方法的入口地址,并未涉及到新方法名的重定位,因此改名后的方法可以順利被執(zhí)行。
但是如果稍作修改,在 getMethodName 源碼的上方添加另一個方法,偏移地址就發(fā)生了改變,運行時就會執(zhí)行新添加的方法,如果方法的參數(shù)類型與返回值不符則會報錯,相符則仍然可以順利執(zhí)行。
Demo 工程
本文的 Demo 工程見 Github.
(github.com/pmtao/SwiftMethodDispatchDemo)
轉(zhuǎn)自:掘金 鏡畫者
https://juejin.cn/post/7002201394916622343
-End-
最近有一些小伙伴,讓我?guī)兔φ乙恍?nbsp;面試題 資料,于是我翻遍了收藏的 5T 資料后,匯總整理出來,可以說是程序員面試必備!所有資料都整理到網(wǎng)盤了,歡迎下載!

面試題】即可獲取
