前言
對于編譯型語言來看,有主要三種類型的函數派發方式,分别為:
- Direct Dispatch:直接派發
- Table Dispatch:函數表派發
- Message Dispatch:消息派發
分析三種派發方式主要從性能及動态性兩方面讨論,這兩個特性相對而言是沖突的,性能要求高,則動态性差,反之亦然,其中直接派發又被稱為靜态派發,函數表派發與消息派發稱為動态派發,大多數語言都會支援上面派發方式的一種到多種。如
- C 使用直接派發;
- Java 預設使用函數表派發,可以通過 final 修飾符修改成直接派發;
- C++ 預設使用直接派發,但可以通過加上 virtual 修飾符來改成函數表派發;
- OC 使用直接派發、消息派發方式;(普通方法采用消息派發的方式,load 方法使用直接派發的方式)
直接派發
直接派發是三種形式裡面最快速的,在編譯時就确定了方法的調用位址,彙編代碼中,直接跳到方法的位址執行,生成的彙編指令最少。
優點:編譯器可以對這種派發方式進行更多優化,比如函數内聯等。
缺點:缺乏動态性,無法實作繼承等;
函數表派發
函數表是編譯型語言常見的派發方式,函數表使用數組來存儲類中聲明的每個函數的指針。對于這個表,大部分語言叫
Virtual table(虛函數表)
。根據 Swift 編譯生成的 SIL 檔案分析,Swift 中存在兩種函數表,其中協定使用的是
witness_table
(SIL 檔案中名為 sil_witness_table),類使用的是
virtual_table
(SIL 檔案中名為 sil_vtable)。
每一個類都會維護一個函數表,裡面記錄着類所有的函數,如果父類函數被 override,表裡面隻會儲存被 override 之後的函數。一個子類新添加的函數,都會被插入到這個數組的最後。運作時會根據這一個表去決定實際要被調用的函數;
一個函數被調用時會先去讀取對象的函數表(讀取第一次),再根據類的位址加上該的函數的偏移量得到函數位址(讀取第二次),最後跳到那個位址上去(跳轉一次)。整個過程是兩次讀取一次跳轉,比直接派發慢一些。
消息派發
消息派發是動态性最強的派發方式,也是性能最差的一種方式;方法調用包裝成消息,發給運作時(相當于中間人),運作時會找到類對象,類對象會儲存類的資料資訊,或通過父類查找,直到命中執行,如果沒找到方法,抛出異常,運作時提供了很多動态的方法用于改變消息派發的行為,相比函數表派發有很強的動态性,由于運作時支援的功能很多,方法查找的過程比較長,是以性能比較低;
OC 消息派發過程在這不展開說,後續有博文專門說這個。
Swift 中的函數派發
分析SIL檔案,我們可以分析出Swift中派發方式的規律,關于SIL相關知識,可以參照該文 iOS編譯簡析 。
本文隻給出關鍵指令
swiftc main.swift -emit-sil | xcrun swift-demangle > main.sil
。
派發方式與 SIL 檔案中關鍵指令對應關系
- sil_witness_table/sil_vtable:函數表派發
- objc_method:消息機制派發
- 不在上述範圍内的屬于直接派發;
Swift 語言支援三種派發方式。采用何種方式跟以下四種因素相關:
- 聲明的位置
- 引用類型
- 指定行為
- 顯式地優化
直接派發 | 函數表派發 | 消息派發 | |
---|---|---|---|
NSObject | @nonobjc 或者 final 修飾的方法 | 聲明作用域中方法 | 擴充方法及被 dynamic 修飾的方法 |
Class | 不被 @objc 修飾的擴充方法及被 final 修飾的方法 | 聲明作用域中方法 | dynamic 修飾的方法或者被 @objc 修飾的擴充方法 |
Protocol | 擴充方法 | 聲明作用域中方法 | @objc 修飾的方法或者被 objc 修飾的協定中所有方法 |
Value Type | 所有方法 | 無 | 無 |
其他 | 全局方法,staic 修飾的方法;使用 final 聲明的類裡面的所有方法;使用 private 聲明的方法和屬性會隐式 final 聲明; |
通過該表格你大概就可以了解一下 Swift 語言中的一些限制了:
- extension 中定義的方法如果想 overrite,需要在方法上加上 @objc 修飾符;因為如果不加 @objc,走的是直接派發,無法重寫方法。
Swift 派發優化
内聯優化
Swift 編譯時在直接派發方式的基礎上還可以進行優化,如函數内聯。
内聯主要原理是:将一些函數的實作直接編譯入調用函數的位置中去,減少函數指針的棧調用,提高運作效率。當開啟編譯優化 (Optimization Level) 時,編譯器會在直接派發方式基礎上根據函數實際情況進行内聯優化。下列情況編譯器預設不會進行内聯優化:
- 函數體過長(無形中增加了包體積,重複代碼);
- 函數包含動态派發;
- 函數中包含遞歸調用;
Swift 中顯式内聯優化修飾符
-
聲明這個函數 never 永遠不被編譯成 inline 的形式,即使開啟了編譯器優化;@inline(never)
-
聲明這個函數總是編譯成 inline 的形式, 這個修飾符隻對函數體過長這種不會被内聯優化的情況生效,其他情況也不生效;@inline(__always)
内聯除了可以提高運作效率這個優點之外,還有另外一個好處,将部分關鍵函數進行内聯優化,可以增大逆向難度。
盡量直接派發
Swift 會盡可能的優化派發方式,一些函數表派發方法會優化成直接派發。編譯器可以通過
whole module optimization
檢查繼承關系,對某些沒有标記 final 的類通過計算,如果能在編譯期确定執行的方法,則使用直接派發。比如一個函數沒有 override,Swift 就可能會使用直接派發的方式。