Swift進階04:方法調度

靜態派發

值類型對象的函數的調用方式是靜態調用,即直接地址調用,調用函數指針,這個函數指針在編譯、鏈接完成之后就已經確定了,存放在代碼段,而結構體內部并不存放方法。因此可以通過地址直接調用

  • 結構體函數符號調試如下:
靜態派發
  • 打開Mach-O可執行文件,其中的 __text段,就是所謂的代碼段,需要執行的匯編指令都在這里
Mach-O文件

對于上面的分析,有個疑問:直接地址調用后面是符號,這個符號是怎么來的?

符號

  • 是從Mach-O文件的 符號表 Symbol Table,但是符號表中并不存儲字符串,字符串存儲在 字符串表 String Table(存放所有的變量名和函數名,以字符串形式存儲),然后根據符號表中的偏移值字符串表中查找對應的字符,然后進行命名重整
image
  • Symbol Table: 存儲符號位于字符串表的位置
  • Dynamic Symbol Table: 動態庫函數位于符號表的偏移位置

還可以通過終端命令nm,獲取項目中的符號表

  • 查看符號表:nm mach-o文件路徑

  • 通過命令還原符號名稱:xcrun swift-demangle 符號

  • Edit Scheme 中的 Debug改成 Release,編譯后查看,在可執行文件目錄下,多了一個后綴為 dSYM的文件,此時,再去 Mach-o文件中查找 teach符號,發現是找不到的。 其主要原因是因為靜態鏈接的函數,實際上是不需要符號的,一旦編譯完成,其地址確定后,當前的符號表就會刪除當前函數對應的符號,在release環境下,符號表中存儲的只是不能確定地址的符號

  • 對于不能確定地址的符號,是在運行時確定的,即函數第一次調用時(相當于懶加載),例如 print,是通過 dyld_stub_bind確定地址的

函數符號命名規則

  • 對于C函數來說,命名的重整規則就是在函數名之前加 _(注意:C中不允許函數重載,因為沒有辦法區分)
#include <stdio.h>
void test(){}
image
  • 對于OC來說,也不支持函數重載,其符號命名規則是-[類名 函數名]
  • 對于Swift來說,是允許函數重載,主要是因為swift中的重整命名規則比較復雜,可以確保函數符號的唯一性

ASLR(隨機地址偏移)

新建一個iOS項目,在 ViewController中定義一下代碼

struct HTStack {
    func teacher() {
        print("teacher")
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        let t = HTStack()
        t.teacher()
        print("end")
    }
}
  • 運行上述代碼,查看mach-o文件,發現mach-o文件中的地址 與 函數調用的地址不一致,主要原因是實際調用時地址多了一個 ASLR(地址空間布局隨機化 address space layout randomizes)
ASLR
  • mach-o文件中查看,程序運行靜態基地址(VM address) 是 0x0000000100000000
image
  • 可以通過image list查看,其中 0x100000000程序運行的首地址,后八位是隨機地址偏移 0x20ef000(即 ASLR)
image
  • 函數地址等于 0x100000000(程序運行首地址)+ 0x20ef000(ASLR) + 0x3A30(符號表地址偏移)= 0x1020F2A30
image

動態派發

匯編指令補充

ARM64匯編指令

  • blr:帶返回的跳轉指令,跳轉到指令后邊跟隨寄存器中保存的地址
  • mov:將某一寄存器的值復制到另一寄存器(只能用于寄存器與寄存器 或者 寄存器與常量之間 傳值,不能用于內存地址)
    • mov x1, x0 將寄存器x0的值復制到寄存器x1中
  • ldr:將內存中的值讀取到寄存器中
    • ldr x0, [x1, x2] 將寄存器x1和寄存器x2 相加作為地址,取該內存地址的值翻入寄存器x0中
  • str:將寄存器中的值寫入到內存中
    • str x0, [x0, x8] 將寄存器x0的值保存到內存[x0 + x8]處
  • bl:跳轉到某地址

探索class的調度方式

首先介紹下V_Table在SIL文件中的格式

//聲明sil vtable關鍵字
decl ::= sil-vtable
//sil vtable中包含 關鍵字、標識(即類名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數名稱
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-name

例如,以HTTacher為例,其SIL中的v-table如下所示

class HTTeacher {
    func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
image
  • sil_vtable:關鍵字
  • HTTeacher:表示是 HTTeacher類的函數表
  • 其次就是當前方法的聲明對應著方法的名稱
  • 函數表 可以理解為 數組,聲明在 class內部的方法在不加任何關鍵字修飾的過程中,是連續存放在我們當前的地址空間中的。這一點,可以通過斷點來印證

函數表源碼探索

下面來進行函數表底層的源碼探索

  • 源碼中搜索 initClassVTable,并加上斷點,然后寫上源碼進行調試
    image
  • 其內部是通過 for循環編碼,然后offset+index偏移,然后獲取method,將其存入到偏移后的內存中,從這里可以驗證函數是連續存放的
  • 對于class中函數來說,類的方法調度是通過V-Taable,其本質就是一個連續的內存空間(數組結構)。

問題:如果更改方法聲明的位置呢?例如 extension中的函數,此時的函數調度方式還是函數表調度嗎?

  • 定義一個 HTTeacher的 extension
extension HTTeacher {
    func teacher4() { print("teacher4") }
}
  • 再定義一個子類 HTStudent繼承自 HTTeacher,查看SIL中的 V-Table
class HTStudent: HTTeacher {}
  • 查看 SIL文件,發現子類只繼承了class中定義的函數,即函數表中的函數
    image

    其原因是因為子類將父類的函數表全部繼承了,如果此時子類增加函數,會繼續在連續的地址中插入,假設extension函數也是在函數表中,則意味著子類也有,但是子類無法并沒有相關的指針記錄函數 是父類方法 還是 子類方法,所以不知道方法該從哪里插入,導致extension中的函數無法安全的放入子類中。所以在這里可以側面證明extension中的方法是直接調用的,且只屬于類,子類是無法繼承的

開發注意點:

  • 需要繼承的方法和屬性,不能寫在extension中。
  • extension中創建的函數,一定是只屬于自己類,但是其子類也有其訪問權限,只是不能繼承和重寫

final、@objc、dynamic修飾函數

final 修飾

  • final 修飾的方法是 直接調度的,可以通過SIL驗證 + 斷點驗證
class HTTeacher {
    final func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
image

@objc 修飾

  • 使用 @objc關鍵字是將 swift中的方法暴露給OC
class HTTeacher {
    @objc func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 通過SIL+斷點調試,發現 @objc修飾的方法是 函數表調度
    image

【小技巧】混編頭文件查看方式:查看項目名-Swift.h頭文件

image

  • 如果只是通過 @objc修飾函數,OC還是無法調用swift方法的,因此如果想要 OC訪問swift,class需要繼承NSObject
<!--swift類-->
class HTTeacher: NSObject {
    @objc func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}

<!--橋接文件中的聲明-->
SWIFT_CLASS("_TtC11HTSwiftDemo9HTTeacher")
@interface HTTeacher : NSObject
- (void)teacher;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

查看 SIL文件發現被 @objc修飾的函數聲明有兩個:swift + OC(內部調用的swift中的teach函數)

image

即在SIL文件中生成了兩個方法

  • swift原有的函數
  • @objc標記暴露給OC來使用的函數: 內部調用swift原有函數

dynamic 修飾

以下面代碼為例,查看 dynamic修飾的函數的調度方式

class HTTeacher: NSObject {
    dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 其中 teach函數的調度還是 函數表調度,可以通過斷點調試驗證,使用 dynamic的意思是可以動態修改,意味著當類繼承自NSObject時,可以使用 method-swizzling

@objc + dynamic

class HTTeacher: NSObject {
    @objc dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 通過斷點調試,走的是 objc_msgSend流程,即 動態消息轉發
    image

swift中實現方法交換

在swift中的需要交換的函數前,使用dynamic修飾,然后通過: @_dynamicReplacement(for: 函數符號)進行交換,如下所示

class HTTeacher {
    dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}

extension HTTeacher {
    @_dynamicReplacement(for: teacher)
    func teacher5() {
        print("teacher5")
    }
}

var t = HTTeacher()
t.teacher()

teacher()方法替換成了 teacher5

image

  • 如果 teacher()方法沒有實現 或者 沒有dynamic修飾符,會報錯
    image

方法調度總結

  • struct值類型,其中函數的調度屬于直接調用地址,即靜態調度
  • class引用類型,其中函數的調度是通過V-Table函數表來進行調度的,即動態調度
  • extension中的函數調度方式是直接調度
  • final修飾的函數調度方式是直接調度
  • @objc修飾的函數調度方式是函數表調度,如果OC中需要使用,class還必須繼承NSObject
  • dynamic修飾的函數的調度方式是函數表調度,使函數具有動態性
  • @objc + dynamic 組合修飾的函數調度,是執行的是 objc_msgSend流程,即 動態消息轉發

內存插件 libfooplugin.dylib的使用

安裝和使用

方式一

  • 在根目錄下創建 .lldbinit ??????文件: vim /.lldbinit
  • 然后輸入 plugin load libfooplugin.dylib路徑

方式二

  • 在通過lldb調試的時候,直接輸入 plugin load libfooplugin.dylib路徑

使用

  • lldb環境下,通過 cat address 地址使用

內存分區調試實踐

堆區

對于堆區的內存來說,就是通過 new & malloc 關鍵字來申請的內存空間,不連續,類似鏈表的結構,最直觀的就是類的實例對象。
定義代碼如下,通過cat查看類實例的內存分區

class HTTeahcer {
    func teacher() {
        print("teacher")
    }
}

var t = HTTeahcer()

image

從上圖可以看出,類的實例對象存儲在堆區,即 heap pointer

棧區

查看以下代碼的 age內存地址位于哪個區?

func test() {
    // 我們在函數內部聲明的age變量就是一個局部變量
    var age: Int = 18
    print(age)
}

test()

image

從結果來看,age位于棧區,即 stack address,此處的age如果用 let修飾,取不到地址

全局區

對于C的分析

下面是C語言的部分代碼,查看其變量的內存地址

//全局已初始化變量
int a = 10;
//全局未初始化變量
int age;

//全局靜態變量
static int age2 = 30;

int main(int argc, const char * argv[]) {
    
    char *p = "CJLTeacher";
    printf("%d", a);
    printf("%d", age2);
    return  0;
}
  • 查看 a(全局已初始化變量)的內存地址

    image

    其中 __DATA.__data表示 segment.section,這里的位置和全局區并不沖突,因為一個是人為的內存分配(內存布局分區),一個是 Mach-O的 segment.section段中,是文件的格式劃分
    image

  • 查看 age(全局未初始化變量)的內存地址

    image

    age在Mach-O文件中,放在了__DATA.__common段,主要放的就是未初始化的符號聲明(mach-o相比內存劃分更細,主要是為了更好的定位符號),當然此時的 age在內存中依然在全局區

  • 查看 age2(全局已初始化靜態變量)的內存地址(其中需要注意:age2必須使用才能找到,否則會報錯)

    image

  • 觀察3個變量的地址,其地址都是相鄰的,因為在內存中都放在了全局區,觀察其內存地址,可以發現,在全局區中,未初始化變量地址已初始化變量地址

    image

  • 如果定義了一個char *p = "CJLTeacher",查看 *pMach-O存儲在__TEXT.cstring段,內存中存儲在常量區

    image

  • 如果是 const修飾的變量呢?存放在Mach-O文件中的__TEXT.__const

    image

  • 如果使用static + const修飾變量,此時變量在哪?

    image

    查看 age4的內存地址,地址特別大,而且使用 cat查看不了,因為mach-o沒有記錄,age4 就是50,即使用static+const修飾的變量就相當于直接替換

對于Swift的分析
let age = 10
  • 對于let修飾的變量,由于是不可變的,所以不能通過 po+cat查看內存,通過匯編 首地址+偏移 來獲取 age的內存,發現是在Mach-O的 __DATA.__common

    image

    從這里可以發現,這與C中是有所區別的。swift的不同之處:已經初始化的全局變量放在__DATA.__common段,猜測是因為 age開始是被標記為未初始化的,當我們執行代碼之后才將 10存儲到對應的內存地址中

  • 如果是 var修飾的變量呢?可以發現與 let是一致的,還是 __DATA.__common

var age2 = 20
image
總結
  • 對于C語言中全局變量,根據是否已經初始化,存儲在Mach-O中存儲位置是不同的
    • 初始化的全局變量:__DATA.__data
    • 初始化的全局變量:__DATA.__common
    • 初始化的全局靜態變量,即static修飾:__DATA.__data
    • 對于char *p類型的字符:__TEXT.cstring
    • const修飾的全局變量:__TEXT.__const
    • static+const修飾的全局變量:Mach-O中沒有記錄
  • 對于 Swift中的全局變量
    • let修飾的全局變量:__DATA.__common
    • var修飾的全局變量:__DATA.__common
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容