派發(fā)機(jī)制是程序判斷如何去調(diào)用函數(shù)或方法的機(jī)制,每次調(diào)用方法時(shí)都會(huì)觸發(fā),但一般我們都不會(huì)注意到。了解派發(fā)機(jī)制的工作原理,對(duì)于寫出高性能的代碼來(lái)說(shuō)非常重要,派發(fā)機(jī)制也能解釋一些Swift中的奇妙現(xiàn)象,和Objective-C中所謂的。
編譯型編程語(yǔ)言主要有三種派發(fā)方式:直接派發(fā)(Direct Dispatch), 函數(shù)表派發(fā)(Table Dispatch) 和 消息機(jī)制派發(fā)(Message Dispatch)。
Java默認(rèn)使用函數(shù)表派發(fā)機(jī)制,但是我們可以通過(guò)final關(guān)鍵字來(lái)將其轉(zhuǎn)換為直接派發(fā)。C++默認(rèn)使用直接派發(fā),但可以通過(guò)virtual關(guān)鍵字轉(zhuǎn)化為消息機(jī)制派發(fā)。Objective-C總是使用消息機(jī)制派發(fā),但允許開(kāi)發(fā)者使用C進(jìn)行直接派發(fā)來(lái)提高性能。Swift已經(jīng)實(shí)現(xiàn)了三種派發(fā)機(jī)制的全部支持,但是也給開(kāi)發(fā)者帶來(lái)了很多困擾。
派發(fā)方式
派發(fā)機(jī)制的目的是為了讓程序告訴CPU,當(dāng)調(diào)用一個(gè)具體方法的時(shí)候要去內(nèi)存的哪個(gè)地方找到可執(zhí)行代碼。在了解Swift之前,先來(lái)了解一下三種派發(fā)方式,以及它們?nèi)绾卧谛阅芎蛣?dòng)態(tài)性之間的取舍。
直接派發(fā)(Direct Dispatch)
直接派發(fā)是速度最快的派發(fā)機(jī)制,它生成的匯編指令最少,編譯器也有很大的優(yōu)化空間,例如函數(shù)內(nèi)聯(lián)等等,但這不在本文的討論范圍內(nèi)。因?yàn)樵诰幾g時(shí)就能確定方法的調(diào)用位置,直接派發(fā)也被稱為靜態(tài)派發(fā)(Static Dispatch)。
但是,對(duì)于編程來(lái)說(shuō)直接派發(fā)也是最局限的,因?yàn)樗狈?dòng)態(tài)性,而無(wú)法支持繼承。
函數(shù)表派發(fā)(Table Dispatch)
函數(shù)表派發(fā)是編譯型編程語(yǔ)言動(dòng)態(tài)性的最常見(jiàn)的實(shí)現(xiàn),函數(shù)表維護(hù)了一個(gè)指針數(shù)組,每個(gè)指針都指向類中聲明的函數(shù),每個(gè)聲明的函數(shù)也確保有指針指向它。大部分語(yǔ)言把這個(gè)表稱為虛函數(shù)表(Virtual Table),但在Swift里稱為(Witness Table)。
每個(gè)類都維護(hù)一張屬于自己的函數(shù)表,里面記錄著所有函數(shù);子類會(huì)復(fù)制一張父類的表,在重寫時(shí)修改指針,指向覆蓋的新函數(shù),子類添加的新函數(shù)會(huì)被插入表的最后。每當(dāng)調(diào)用函數(shù)時(shí),根據(jù)函數(shù)表的指針來(lái)確定具體調(diào)用哪個(gè)函數(shù)。
舉個(gè)栗子,有下面兩個(gè)類:
class ParentClass {
func method1() {}
func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
func method3() {}
}
這時(shí),編譯器會(huì)創(chuàng)建兩個(gè)函數(shù)表,一個(gè)是ParentClass的,一個(gè)是ChildClass的:

let obj = ChildClass()
obj.method2()
當(dāng)一個(gè)method2函數(shù)被調(diào)用時(shí),會(huì)經(jīng)歷以下過(guò)程:
- 讀取
0xB00的函數(shù)表。 - 讀取函數(shù)指針?biāo)饕谶@里
method2的偏移量是1,所以得到地址0xB00 + 1。 - 跳轉(zhuǎn)到地址
0x222并讀取內(nèi)容。
查表是一種簡(jiǎn)單、易實(shí)現(xiàn)而且性能可預(yù)知的方式,但是,這種派發(fā)方式比起直接派發(fā)還是慢了一點(diǎn)。從字節(jié)碼角度來(lái)看,查表時(shí)首先要讀取方法表指針,然后根據(jù)偏移量跳轉(zhuǎn)到函數(shù)指針,再讀取函數(shù)指針,所以查表多了兩次讀操作和一次跳轉(zhuǎn)操作,導(dǎo)致了性能損耗。另外一個(gè)原因就是編譯器無(wú)法進(jìn)行任何優(yōu)化。
查表法的缺陷在于,基于數(shù)組實(shí)現(xiàn)的函數(shù)表無(wú)法為extension提供擴(kuò)展。子類添加的新函數(shù)會(huì)插入函數(shù)表的尾部,所以沒(méi)有位置可以讓extension安全地插入函數(shù)。這篇文章詳細(xì)描述了這種局限性。
消息機(jī)制派發(fā) (Message Dispatch)
消息機(jī)制是動(dòng)態(tài)性最高的調(diào)用方式,也是Cocoa的基石,同時(shí)也催生了KVO,UIAppearance,CoreData等技術(shù)。這種派發(fā)機(jī)制的關(guān)鍵在于,開(kāi)發(fā)者可以在運(yùn)行時(shí)修改函數(shù)的調(diào)用。例如 Method Swizzling 可以在運(yùn)行時(shí)修改函數(shù)的實(shí)現(xiàn)和調(diào)用,甚至可以通過(guò) ISA Swizzling 在運(yùn)行時(shí)修改對(duì)象的繼承關(guān)系,由此可以在面向?qū)ο蟮幕A(chǔ)上實(shí)現(xiàn)自定義分發(fā)。

同樣舉一個(gè)栗子:
class ParentClass {
dynamic func method1() {}
dynamic func method2() {}
}
class ChildClass: ParentClass {
override func method2() {}
dynamic func method3() {}
}
Swift會(huì)通過(guò)樹來(lái)簡(jiǎn)歷繼承關(guān)系:

當(dāng)一個(gè)消息被派發(fā),Runtime會(huì)順著繼承關(guān)系向上查找應(yīng)該被調(diào)用的函數(shù),這樣做的效率非常低。但是,這個(gè)查找操作會(huì)建立一個(gè)散列表用于緩存,一旦這個(gè)緩存被建立起來(lái),消息機(jī)制派發(fā)就會(huì)像函數(shù)表派發(fā)一樣快,這篇文章詳細(xì)探討了性能測(cè)試,這篇文章深入介紹了消息派發(fā)機(jī)制的技術(shù)細(xì)節(jié)。
Swift的派發(fā)機(jī)制
Swift的派發(fā)機(jī)制沒(méi)有一個(gè)固定答案,但是影響派發(fā)方式的因素有四個(gè):
- 聲明的位置
- 引用類型
- 指定派發(fā)方式
- 顯式優(yōu)化
Swift沒(méi)有在文檔中寫明什么時(shí)候會(huì)用什么派發(fā)機(jī)制,唯一說(shuō)明的是:使用dynamic修飾的函數(shù),會(huì)用過(guò)OC Runtime進(jìn)行消息機(jī)制派發(fā)。
聲明的位置(Location Matters)
Swift中,一個(gè)函數(shù)有兩種聲明位置可以選擇:類的聲明和extension,根據(jù)聲明位置不同,派發(fā)方式也不同。
class MyClass {
func mainMethod() {}
}
extension MyClass {
func extensionMethod() {}
}
在這個(gè)例子中,mainMethod會(huì)使用函數(shù)表派發(fā),而extensionMethod會(huì)使用直接派發(fā)。具體根據(jù)不同聲明位置,不同的派發(fā)方式如下表格:

總結(jié)起來(lái)有這么幾點(diǎn)規(guī)律:
- 值類型總是直接派發(fā)
- 協(xié)議和類的聲明作用域中的函數(shù),使用函數(shù)表派發(fā)
- 協(xié)議和類的
extension中的函數(shù),使用直接派發(fā) -
NSObject的extension中的函數(shù)使用消息機(jī)制派發(fā)
引用類型(Reference Type Matters)
聲明的引用類型決定了派發(fā)方式,一個(gè)常見(jiàn)的例子就是,協(xié)議拓展和對(duì)象拓展同時(shí)實(shí)現(xiàn)一個(gè)函數(shù)的時(shí)候:
protocol MyProtocol {
}
struct MyStruct: MyProtocol {
}
extension MyStruct {
func extensionMethod() {
print("In Struct")
}
}
extension MyProtocol {
func extensionMethod() {
print("In Protocol")
}
}
let myStruct = MyStruct()
let proto: MyProtocol = myStruct
myStruct.extensionMethod() // -> “In Struct”
proto.extensionMethod() // -> “In Protocol”
可以看到,在這種情況下因?yàn)?code>proto的聲明引用類型為MyProtocol,所以proto.extensionMethod()直接調(diào)用了協(xié)議拓展中的函數(shù),Kotlin的擴(kuò)展也遵循這個(gè)規(guī)律。但是如果把extensionMethod的聲明移動(dòng)到協(xié)議聲明中,則會(huì)使用函數(shù)表派發(fā),最終調(diào)用結(jié)構(gòu)體里的實(shí)現(xiàn)。
由此我們得出結(jié)論,如果兩種聲明方式都使用了直接派發(fā),那么我們不能完成預(yù)想的函數(shù)覆蓋。
指定派發(fā)方式(Specifying Dispatch Behavior)
Swift有一些修飾符可以指定派發(fā)方式:
final
final允許類里面的函數(shù)使用直接派發(fā), 這個(gè)修飾符會(huì)讓函數(shù)失去動(dòng)態(tài)性。任何函數(shù)都可以使用這個(gè)修飾符,就算是extension里本來(lái)就是直接派發(fā)的函數(shù), 這也會(huì)讓Objective-C Runtime獲取不到這個(gè)函數(shù), 不會(huì)生成相應(yīng)的selector。
dynamic
dynamic可以讓類里面所有的函數(shù)使用消息機(jī)制派發(fā),使用時(shí)必須導(dǎo)入Foundation包,里面包括了NSObject和Objective-C的Runtime。dynamic可以用在所有NSObject的子類和所有Swift原生類,也可以讓extension中的函數(shù)能夠被繼承。
@objc & @nonobjc
@objc和@nonobjc顯式地聲明了一個(gè)函數(shù)能否被Objective-C Runtime捕捉到。使用@objc的典型例子就是給selector一個(gè)命名空間,讓這個(gè)函數(shù)可以在運(yùn)行時(shí)被調(diào)用。@nonobjc表示不讓這個(gè)函數(shù)注冊(cè)到Runtime中,由此禁止消息機(jī)制來(lái)派發(fā)這個(gè)函數(shù),和final非常相似。
final @objc
可以同時(shí)使用final和@objc來(lái)修飾函數(shù),這樣做的結(jié)果就是,調(diào)用函數(shù)時(shí)會(huì)直接派發(fā),但可以將函數(shù)注冊(cè)到Objective-C Runtime中,來(lái)讓函數(shù)可以響應(yīng)perform(selector:)或者其他特性。
@inline
可以通過(guò)@inline來(lái)使用直接派發(fā),但是同時(shí)使用dynamic @inline修飾時(shí),會(huì)使用消息機(jī)制派發(fā)。
修飾符總結(jié)

顯式優(yōu)化
Swift會(huì)盡可能優(yōu)化函數(shù)派發(fā)方式,例如,一個(gè)函數(shù)從來(lái)沒(méi)有繼承或被繼承過(guò),Swift就會(huì)檢測(cè)到并且在可能的情況下使用直接派發(fā),在大多數(shù)情況下這樣的優(yōu)化效果非常好,但是對(duì)于Cocoa開(kāi)發(fā)者就不太友好了:
override func viewDidLoad() {
super.viewDidLoad()
navigationItem.rightBarButtonItem = UIBarButtonItem(
title: "Sign In", style: .plain, target: nil,
action: #selector(ViewController.signInAction)
)
}
private func signInAction() {}
這時(shí)編譯器會(huì)報(bào)錯(cuò):
Argument of '#selector' refers to a method that is not exposed to Objective-C
(Objective-C無(wú)法獲取#selector指定的函數(shù))
這里Swift將signInAction優(yōu)化為直接派發(fā),所以沒(méi)有注冊(cè)到Runtime中,#selector 自然無(wú)法獲取。
另一個(gè)需要注意的是, 如果你沒(méi)有使用dynamic修飾的話,這個(gè)優(yōu)化會(huì)默認(rèn)讓KVO失效。如果一個(gè)屬性綁定了KVO的話,而這個(gè)屬性的getter和setter會(huì)被優(yōu)化為直接派發(fā),代碼依舊可以通過(guò)編譯,不過(guò)動(dòng)態(tài)生成的 KVO函數(shù)就不會(huì)被觸發(fā)。
派發(fā)總結(jié)
