Swift 派發機制

語言: CN / TW / HK

前言

對於編譯型語言來看,有主要三種類型的函式派發方式,分別為:

  • 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 之後的函式。 一個子類新新增的函式,都會被插入到這個陣列的最後。執行時會根據這一個表去決定實際要被呼叫的函式;

一個函式被呼叫時會先去讀取物件的函式表(讀取第一次),再根據類的地址加上該的函式的偏移量得到函式地址(讀取第二次),最後跳到那個地址上去(跳轉一次)。 整個過程是兩次讀取一次跳轉,比直接派發慢一些。

函式表派發.jpeg

訊息派發

訊息派發是動態性最強的派發方式,也是效能最差的一種方式;方法呼叫包裝成訊息,發給執行時(相當於中間人),執行時會找到類物件,類物件會儲存類的資料資訊,或通過父類查詢,直到命中執行,如果沒找到方法,丟擲異常,執行時提供了很多動態的方法用於改變訊息派發的行為,相比函式表派發有很強的動態性,由於執行時支援的功能很多,方法查詢的過程比較長,所以效能比較低; 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 中顯式內聯優化修飾符

  • @inline(never) 宣告這個函式 never 永遠不被編譯成 inline 的形式,即使開啟了編譯器優化;
  • @inline(__always) 宣告這個函式總是編譯成 inline 的形式, 這個修飾符只對函式體過長這種不會被內聯優化的情況生效,其他情況也不生效;

內聯除了可以提高執行效率這個優點之外,還有另外一個好處,將部分關鍵函式進行內聯優化,可以增大逆向難度。

儘量直接派發

Swift 會盡可能的優化派發方式,一些函式表派發方法會優化成直接派發。編譯器可以通過 whole module optimization 檢查繼承關係,對某些沒有標記 final 的類通過計算,如果能在編譯期確定執行的方法,則使用直接派發。比如一個函式沒有 override,Swift 就可能會使用直接派發的方式。


有一個技術的圈子與一群同道眾人非常重要,來我的技術公眾號及部落格,這裡只聊技術乾貨。 - 微信公眾號:CoderStar - 部落格:CoderStar's Blog