Swift-函數(shù)派發(fā)

前言

對于Swift的學(xué)者來說函數(shù)派發(fā)有很大的誤區(qū):就是認(rèn)為Swift沿用Objective-C的消息派發(fā)機(jī)制,且認(rèn)為Swift與Objective-C公用一套Runtime。Objective-C是C語言實現(xiàn)的;Swift是C++實現(xiàn)的。倘若蘋果大大真是這樣做,那就沒必要開發(fā)新的語言。

在寫Swift代碼的時候需要給方法加@objc、dynamic等關(guān)鍵字有什么作用,還有Swift到底函數(shù)調(diào)用的時候是怎么個方式,下面就和大家介紹Swift的函數(shù)派發(fā)。

概念介紹

函數(shù)派發(fā)就是程序判斷使用哪種途徑去調(diào)用一個函數(shù)的機(jī)制。每次函數(shù)被調(diào)用時都會被觸發(fā),但你又不會太留意的一個東西。了解派發(fā)機(jī)制對于寫出高性能的代碼來說很有必要。

編譯型語言有三種基礎(chǔ)的函數(shù)派發(fā)方式:直接(靜態(tài))派發(fā)(Direct Dispatch)、函數(shù)表派發(fā)(Table Dispatch)?和?消息機(jī)制派發(fā)(Message Dispatch)。

大多數(shù)語言都會支持一到兩種,Java 默認(rèn)使用函數(shù)表派發(fā),但你可以通過?final?修飾符修改成直接派發(fā);C++ 默認(rèn)使用直接派發(fā),但可以通過加上?virtual?修飾符來改成函數(shù)表派發(fā);而 Objective-C 則總是使用消息機(jī)制派發(fā),但允許開發(fā)者使用 C 直接派發(fā)來獲取性能的提高。這樣的方式非常好, 但也給很多開發(fā)者帶來了困擾,。

程序派發(fā)的目的是為了告訴 CPU 需要被調(diào)用的函數(shù)在哪里, 在我們深入 Swift 派發(fā)機(jī)制之前, 先來了解一下這三種派發(fā)方式, 以及每種方式在動態(tài)性和性能之間的取舍.

直接(靜態(tài))派發(fā) (Direct Dispatch):

直接派發(fā)也叫靜態(tài)派發(fā)。在直接派發(fā)中,編譯器直接找到相關(guān)指令的位置。當(dāng)函數(shù)調(diào)用時,系統(tǒng)直接跳轉(zhuǎn)到函數(shù)的內(nèi)存地址執(zhí)行操作。這樣的好處就是執(zhí)行快,同時允許編譯器能夠執(zhí)行例如內(nèi)聯(lián)等優(yōu)化。事實上,編譯期在編譯階段為了能夠獲取最大的性能提升,都盡量將函數(shù)靜態(tài)化。

函數(shù)表派發(fā) (Table Dispatch):

函數(shù)表派發(fā)是編譯型語言實現(xiàn)動態(tài)行為最常見的實現(xiàn)方式。函數(shù)表使用了一個數(shù)組來存儲類聲明的每一個函數(shù)的指針。大部分語言把這個稱為 “virtual table”(虛函數(shù)表),Swift 里稱為 “witness table”。每一個類都會維護(hù)一個函數(shù)表,里面記錄著類所有的函數(shù),如果父類函數(shù)被 override 的話,往表里面添加被 override 的函數(shù)。一個子類新添加的函數(shù),會把新增的函數(shù)添加到vTable,再添加 override 的函數(shù)。運(yùn)行時會根據(jù)這一個表去決定實際要被調(diào)用的函數(shù)。舉個例子:

class?Animal {

? ??func eat() {}

? ??func drink() {}

}

class Bird: Animal {

? ??override func eat() {}

????func fly () {}

}

當(dāng)執(zhí)行Bird類中的eat函數(shù)時整個流程如下:

1.讀取Bird類的函數(shù)表地址Oxb00。

2.讀取到eat函數(shù),也就是0xb00+1。

3.跳轉(zhuǎn)到0x330執(zhí)行具體的操作。

函數(shù)表

從上面的分析中我們可以知道,要具體執(zhí)行fly函數(shù),就必須進(jìn)行兩次讀取和一次跳轉(zhuǎn)。同時編譯器對于函數(shù)表派發(fā)的函數(shù)是無法執(zhí)行優(yōu)化的。這樣,執(zhí)行速度必然就變慢了。

這種基于數(shù)組的實現(xiàn),缺陷在于函數(shù)表無法拓展。子類會在虛數(shù)函數(shù)表的最后插入新的函數(shù),沒有位置可以讓 extension 安全地插入函數(shù)。

消息機(jī)制派發(fā) (Message Dispatch):

Objc的函數(shù)派發(fā)都是基于消息派發(fā)的。這種機(jī)制極具動態(tài)性,既可以通過swizzling修改函數(shù)的實現(xiàn),也可以通過isa-swizzling修改對象。

還是上面那段代碼,然后看一下通過消息派發(fā)執(zhí)行Bird中的drink函數(shù)的步驟:

1.到自己的方法列表中去找,結(jié)果沒找到。

2.去它的父類Animal中去找,發(fā)現(xiàn)找到了,就執(zhí)行相應(yīng)的邏輯。

消息派發(fā)

從中我們可以發(fā)現(xiàn),如果這個方法在NSObject中,那么每次都要找好多次,就會非常慢。解決的方法就是利用方法緩存。這個查找過程就會通過緩存來把性能提高到和函數(shù)表派發(fā)一樣快。

Swift 的派發(fā)機(jī)制

Swift 沒有在文檔里具體寫明什么時候會使用函數(shù)表什么時候使用消息機(jī)制。唯一的承諾是使用?dynamic修飾的時候會通過 Objective-C 的運(yùn)行時進(jìn)行消息機(jī)制派發(fā)。

默認(rèn)情況:

Swift函數(shù)聲明位置有兩種:a.類、結(jié)構(gòu)體、枚舉和協(xié)議的聲明位置;b.它們的extension位置。

默認(rèn)情況下,Swift 使用的派發(fā)方式總結(jié)起來有這么幾點: 1.值類型、協(xié)議的extension、類的extension總是會使用直接派發(fā);2.NSObject 的 extension 會使用消息機(jī)制進(jìn)行派發(fā);3.NSObject 聲明作用域里的函數(shù)都會使用函數(shù)表進(jìn)行派發(fā);4.協(xié)議里聲明的, 并且?guī)в心J(rèn)實現(xiàn)的函數(shù)會使用函數(shù)表進(jìn)行派發(fā)。

Swift默認(rèn)派發(fā)

舉個例:

Value Type(值類型):struct、enum等

struct?MyStruct {

????func structFunc() {}????// Static直接派發(fā)? ??

}

extension?MyStruct {

? ??func structExtensionFunc() {}????// Static直接派發(fā)? ??

}

大概意思懂就行了。 小試牛刀:

默認(rèn)派發(fā)試煉

剛接觸 Swift 的人可能會認(rèn)為 myProtocol.extensionMethod()調(diào)用的是結(jié)構(gòu)體里的實現(xiàn)。分析:當(dāng)myProtocol引用了myStruct且在自己的extension下有協(xié)議自己的extensionMethod函數(shù),該函數(shù)調(diào)用的方式是直接派發(fā)static,所以當(dāng)myProtocol.extensionMethod()找到了這個函數(shù)的地址直接執(zhí)行了函數(shù)體。逆向思考:如果兩種聲明方式都使用了直接派發(fā)的話,基于直接派發(fā)的運(yùn)作方式,我們不可能實現(xiàn)預(yù)想的?override行為。

關(guān)鍵字指定派發(fā)方式:

1.final:允許類里面的函數(shù)使用直接派發(fā),這個修飾符會讓函數(shù)失去動態(tài)性。任何函數(shù)都可以使用這個修飾符,就算是 extension 里本來就是直接派發(fā)的函數(shù)。這也會讓 Objective-C 的運(yùn)行時獲取不到這個函數(shù),不會生成相應(yīng)的 selector。

2.dynamic:可以讓類里面的函數(shù)使用消息機(jī)制派發(fā)。使用 dynamic必須導(dǎo)入 Foundation 框架,里面包括了 NSObject 和 Objective-C 的運(yùn)行時。dynamic 可以讓聲明在 extension 里面的函數(shù)能夠被 override。dynamic 可以用在所有 NSObject 的子類和 Swift 的原聲類。

3.@objc 或 @nonobjc:都可以顯式地聲明了一個函數(shù)是否能被 Objective-C 的運(yùn)行時捕獲到。但使用 @objc 的典型例子就是給 selector 一個命名空間 @objc(abc_methodName),讓這個函數(shù)可以被 Objective-C 的運(yùn)行時調(diào)用。@nonobjc會改變派發(fā)的方式,可以用來禁止消息機(jī)制派發(fā)這個函數(shù),不讓這個函數(shù)注冊到 Objective-C 的運(yùn)行時里。我不確定這跟 final 有什么區(qū)別,因為從使用場景來說也幾乎一樣。我個人來說更喜歡 final,因為意圖更加明顯。

4.final 與 @objc同時使用:可以在標(biāo)記為 final 的同時,也使用 @objc 來讓函數(shù)可以使用消息機(jī)制派發(fā)。這么做的結(jié)果就是,調(diào)用函數(shù)的時候會使用直接派發(fā),但也會在 Objective-C 的運(yùn)行時里注冊響應(yīng)的 selector。函數(shù)可以響應(yīng) perform(selector:) 以及別的 Objective-C 特性,但在直接調(diào)用時又可以有直接派發(fā)的性能。

5. @inline:Swift 也支持 @inline,告訴編譯器可以使用直接派發(fā)。有趣的是,dynamic @inline(__always) func dynamicOrDirect() {} 也可以通過編譯!但這也只是告訴了編譯器而已,實際上這個函數(shù)還是會使用消息機(jī)制派發(fā)。這樣的寫法看起來像是一個未定義的行為,應(yīng)該避免這么做。

指定Modifiers

可見的都會被優(yōu)化 (Visibility Will Optimize)

Swift 會盡最大能力去優(yōu)化函數(shù)派發(fā)的方式. 例如, 如果你有一個函數(shù)從來沒有 override, Swift 就會檢車并且在可能的情況下使用直接派發(fā). 這個優(yōu)化大多數(shù)情況下都表現(xiàn)得很好, 但對于使用了 target / action 模式的 Cocoa 開發(fā)者就不那么友好了. 例如:

override?func?viewDidLoad()?{

? ??super.viewDidLoad()

? ??navigationItem.rightBarButtonItem?=?UIBarButtonItem(title:?"登錄",?style:?.plain,?target:?nil, action:?#selector(ViewController.signInAction))

}

這里編譯器會拋出一個錯誤:Argument of '#selector' refers to a method that is not exposed to Objective-C (Objective-C 無法獲取 #selector 指定的函數(shù))。你如果記得 Swift 會把這個函數(shù)優(yōu)化為直接派發(fā)的話,就能理解這件事情了。這里修復(fù)的方式很簡單:加上@objc 或者 dynamic 就可以保證 Objective-C 的運(yùn)行時可以獲取到函數(shù)了。這種類型的錯誤也會發(fā)生在UIAppearance 上,依賴于 proxy 和 NSInvocation 的代碼。

另一個需要注意的是, 如果你沒有使用 dynamic 修飾的話,這個優(yōu)化會默認(rèn)讓 KVO 失效。如果一個屬性綁定了 KVO 的話,而這個屬性的 getter 和 setter 會被優(yōu)化為直接派發(fā),代碼依舊可以通過編譯,不過動態(tài)生成的 KVO 函數(shù)就不會被觸發(fā)。

派發(fā)總結(jié) (Dispatch Summary):

派發(fā)方式總結(jié)

NSObject 以及動態(tài)性的損失 (NSObject and the Loss of Dynamic Behavior)

NSObject 的函數(shù)表派發(fā) (Table Dispatch in NSObject):

上面, 我提到NSObject子類定義里的函數(shù)會使用函數(shù)表派發(fā)。但我覺得很迷惑,很難解釋清楚,并且由于下面幾個原因,這也只帶來了一點點性能的提升:

a.大部分NSObject的子類都是在obj_msgSend的基礎(chǔ)上構(gòu)建的。我很懷疑這些派發(fā)方式的優(yōu)化,實際到底會給 Cocoa 的子類帶來多大的提升。

b.大多數(shù) Swift 的NSObject子類都會使用 extension 進(jìn)行拓展, 都沒辦法使用這種優(yōu)化。

最后, 有一些小細(xì)節(jié)會讓派發(fā)方式變得很復(fù)雜。

派發(fā)方式的優(yōu)化破壞了 NSObject 的功能,性能提升很棒,我很喜歡 Swift 對于派發(fā)方式的優(yōu)化。但是,UIView子類顏色的屬性理論上性能的提升破壞了 UIKit 現(xiàn)有的模式。

NSObject 作為一個選擇 (NSObject as a Choice)

使用靜態(tài)派發(fā)的話,結(jié)構(gòu)體是個不錯的選擇;而使用消息機(jī)制派發(fā)的話則可以考慮?NSObject; 現(xiàn)在,如果你想跟一個剛學(xué) Swift 的開發(fā)者解釋為什么某個東西是一個?NSObject?的子類,你不得不去介紹 Objective-C 以及這段歷史?,F(xiàn)在沒有任何理由去繼承?NSObject?構(gòu)建類,除非你需要使用 Objective-C 構(gòu)建的框架。

顯式的動態(tài)性聲明 (Implicit Dynamic Modification)

另一個 Swift 可以改進(jìn)的地方就是函數(shù)動態(tài)性的檢測。我覺得在檢測到一個函數(shù)被?#selector?和?#keypath?引用時要自動把這些函數(shù)標(biāo)記為?dynamic,這樣的話就會解決大部分?UIAppearance?的動態(tài)問題,但也許有別的編譯時的處理方式可以標(biāo)記這些函數(shù)。

看一下 Swift 開發(fā)者遇到過的 error:

歧義1:

在Swift4.2聲明 Man 類并且在其 extension 里 override 繼承下來的 eat() 函數(shù),這個函數(shù)調(diào)用會采用消息機(jī)制進(jìn)行調(diào)用,故需要在父類Person的eat函數(shù)硬性添加 @objc 和 dynamic 表示 Objective-C注冊這一個方法,并使用 Objective-C的動態(tài)性。打印結(jié)果:Man eat。

倘若在Swift3下歧義1

但是,在Swift3下父類 Person 的 eat函數(shù)可以不需要 @objc 和 dynamic 修飾并不會報錯。打印結(jié)果是?Person eat。

當(dāng)man.eat()?被觸發(fā)時,eat()會通過函數(shù)表被派發(fā)到Person對象,而Man重寫之后會是用消息機(jī)制,而Man的函數(shù)表依舊保留了Person的實現(xiàn),緊接著歧義就產(chǎn)生了。?

歧義2:

歧義2 聲明協(xié)議和類
歧義2 調(diào)用協(xié)議函數(shù)

相信大家都看得懂代碼,打印結(jié)果是Hello。你們發(fā)現(xiàn)?Man 實現(xiàn)的函數(shù)前面沒有?override?修飾,這是一個提示,也許代碼不會像我們設(shè)想的那樣運(yùn)行。在這個例子里,Man沒有在?Greetable?的協(xié)議記錄表(Protocol Witness Table)里成功注冊, 當(dāng)?sayHi()通過?Greetable?協(xié)議派發(fā)時,默認(rèn)的實現(xiàn)就會被調(diào)用。

解決的方法就是,1.在類聲明的作用域里就要提供所有協(xié)議里定義的函數(shù),即使已經(jīng)有默認(rèn)實現(xiàn)。2.你可以在類的前面加上一個?final?修飾符,保證這個類不會被繼承。

有趣的error:

error

上面的代碼會觸發(fā)一個編譯錯誤?Declarations in extensions can not be overridden yet(聲明在 extension 里的方法不可以被重寫)。這可能是 Swift 團(tuán)隊打算加強(qiáng)函數(shù)表派發(fā)的一個征兆.。又或者這只是我過度解讀, 覺得這門語言可以優(yōu)化的地方。

原文:Method Dispatch in Swift

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1、通過CocoaPods安裝項目名稱項目信息 AFNetworking網(wǎng)絡(luò)請求組件 FMDB本地數(shù)據(jù)庫組件 SD...
    陽明AI閱讀 16,240評論 3 119
  • 最近參加了一場同學(xué)會,聽到了一些贊美,諸如“啊啊啊好瘦啊”、“怎么回事變美了”之類,我知道這是運(yùn)動帶來了肉眼可見的...
    誰_6035閱讀 270評論 0 0
  • 早上四點多就醒了,六點不到半起床,七點坐車,到鄭州快九點,正趕上聚會,后來讓我講面診。新人剛開始一直低頭玩手機(jī),沒...
    林馨兒_d1d3閱讀 97評論 0 0
  • // iOS6.0以后才有的, 這個類是用于決定UICollectionView的item的布局的NS_CLASS...
    XLsn0w閱讀 754評論 0 1
  • 我們的大閨女已經(jīng)快上中班了,離開學(xué)還剩兩周,又要回到集體生活中去,開始有規(guī)律的幼兒園生活。不知是不是因為這個原因,...
    陌上花開_陪你看海閱讀 800評論 1 3

友情鏈接更多精彩內(nèi)容