天天看點

Swift 派發機制

前言

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

  • 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 之後的函數。一個子類新添加的函數,都會被插入到這個數組的最後。運作時會根據這一個表去決定實際要被調用的函數;

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

Swift 派發機制

消息派發

消息派發是動态性最強的派發方式,也是性能最差的一種方式;方法調用包裝成消息,發給運作時(相當于中間人),運作時會找到類對象,類對象會儲存類的資料資訊,或通過父類查找,直到命中執行,如果沒找到方法,抛出異常,運作時提供了很多動态的方法用于改變消息派發的行為,相比函數表派發有很強的動态性,由于運作時支援的功能很多,方法查找的過程比較長,是以性能比較低;

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 就可能會使用直接派發的方式。