這是重識 Objective-C Runtime系列文章的其中一篇:
重識 Objective-C Runtime - Smalltalk 與 C 的融合
重識 Objective-C Runtime - 看透 Type 與 Value
重識 Objective-C Runtime - 何為對象何為類
重識 Objective-C Runtime - Calling Conventions
重識 Objective-C Runtime - 能寫完上面的就不錯了
對於 C 語言來說,Type 就個比較虛幻的東西,它唯一的目的便是讓編譯器知道一段數據的長度,來決定如何存取,舉個例子:
這段代碼聲明了一個 int 類型的變量和一個 char 類型的變量,有初始化和類型強轉過程,在 x86_64 架構下,這兩行代碼的匯編如下:
匯編看起來混亂,但卻能最真實的反映出程序的運行過程,逐行解釋下:
move 指令就是簡單的值拷貝,這條指令中出現的 movl
表示按低 32 位的長度來拷貝(也就是一個 int 的長度),與之相似的還有 8 位的 movb
(char)、16 位的 movw
(short)、64 位的 movq
(long in 64) 等;$123
即字面常量值;-4(%rbp)
代表 base pointer - 棧基地址寄存器,偏移 4 字節的位置。這個指令執行後內存如下所示:
將剛才 4 字節長度內存賦值給 %eax
寄存器,它是最常用的通用寄存器之一,名為 accumulator,在 64 位架構下,rax
表示這個寄存器的完全體,eax
表示它的低 32 位,ax
表示低 16 位,ah
表示第 8~16 位,al
表示最低的 8 位。這樣摳門的設計一部分因為兼容歷史的 32 架構,一方面也是為了更充分利用寄存器這個寶貴的資源:
按 8 位長度 (char) 將 a 寄存器的最低 8 位移動到 c 寄存器(count register)的低 8 位。這一個指令就在做 int 到 char 的類型轉換,把 123 存在寄存器的低 32 位上,再把寄存器的最低 8 位取出來,相當於把 00000000000000000000000001111011 截斷成了 01111011。
最後,再把剛才的結果按 8 字節的長度拷貝到 %rbp 偏移 5 的位置,完成這個 char 類型棧變量的賦值:
因此,對於 C 這種靜態語言,Type 信息只用於編譯器解析,除了靜態檢查外還影響生成:
相應長度的指令 (是 movq、movl 還是 movb ?)
寄存器長度的選用(是 rax、eax 還是 al ?)
棧變量內存大小的確定,也可以說是 sp 的位置( sp 表示 Stack Pointer, 它和 Base Pointer 配合管理棧內存的分配與回收,所謂“分配”棧內存只是用如 subq $32, %rsp
的指令將 sp 向低地址移動)
然而,對於動態語言,Type 不僅在編譯期起到上述作用,還需要保留到運行時,讓動態調用得以實現,被稱作 Type Encodings
,對於 Objective-C 所有 Type 的編碼,都可以在這個官方文檔中查到,裡面的編碼和用 @encode()
生成的一致,比如:
Objective-C Class 中每個實例變量的 Type 信息全部被編碼,Runtime 也提供了 ivar_getTypeEncoding
來訪問。
同時,為支持消息的轉發和動態調用,Objective-C Method 的 Type 信息也被以 “返回值 Type + 參數 Types” 的形式組合編碼,還需要考慮到 self
和 _cmd
這兩個隱含參數:
注:上面的方法的 Encoding 使用新的格式,舊的格式中包含調用棧大小和布局信息,如
i24@0:8i16i20
,表示調用棧幀共 24 字節大小,後面每個參數跟著的數字表示該參數在調用棧的偏移值,在 x86_64 和 ARM 成為主流後,調用的 Calling Conventions 發生巨大變化,開始借助寄存器傳參,所以在“參數壓棧”時代的這種編碼方式逐漸被廢棄。
方法的編碼可以使用 method_getTypeEncoding
獲取,在 Cocoa 層,被 NSMethodSignature
封裝,並提供了一些便捷的解析方法。
多說一句,純 Swift 聲稱自己是靜態的語言,因為在編譯後,任何結構都會被 Name Mangling
壓縮成一個符號,比如下面的方法:
經過 Name Mangling 的符號是 _TFC12TestSwift4Sark3foofT3barSi_Si
,雖然把結構都拍扁了,但該有的信息都在,Module、Class、Method、參數和返回值類型等,按照一定的格式進行了編碼,感興趣可以看這篇文章。