Swift進(jìn)階04:方法調(diào)度

靜態(tài)派發(fā)

值類型對(duì)象的函數(shù)的調(diào)用方式是靜態(tài)調(diào)用,即直接地址調(diào)用,調(diào)用函數(shù)指針,這個(gè)函數(shù)指針在編譯、鏈接完成之后就已經(jīng)確定了,存放在代碼段,而結(jié)構(gòu)體內(nèi)部并不存放方法。因此可以通過地址直接調(diào)用

  • 結(jié)構(gòu)體函數(shù)符號(hào)調(diào)試如下:
靜態(tài)派發(fā)
  • 打開Mach-O可執(zhí)行文件,其中的 __text段,就是所謂的代碼段,需要執(zhí)行的匯編指令都在這里
Mach-O文件

對(duì)于上面的分析,有個(gè)疑問:直接地址調(diào)用后面是符號(hào),這個(gè)符號(hào)是怎么來(lái)的?

符號(hào)

  • 是從Mach-O文件的 符號(hào)表 Symbol Table,但是符號(hào)表中并不存儲(chǔ)字符串,字符串存儲(chǔ)在 字符串表 String Table(存放所有的變量名和函數(shù)名,以字符串形式存儲(chǔ)),然后根據(jù)符號(hào)表中的偏移值字符串表中查找對(duì)應(yīng)的字符,然后進(jìn)行命名重整
image
  • Symbol Table: 存儲(chǔ)符號(hào)位于字符串表的位置
  • Dynamic Symbol Table: 動(dòng)態(tài)庫(kù)函數(shù)位于符號(hào)表的偏移位置

還可以通過終端命令nm,獲取項(xiàng)目中的符號(hào)表

  • 查看符號(hào)表:nm mach-o文件路徑

  • 通過命令還原符號(hào)名稱:xcrun swift-demangle 符號(hào)

  • Edit Scheme 中的 Debug改成 Release,編譯后查看,在可執(zhí)行文件目錄下,多了一個(gè)后綴為 dSYM的文件,此時(shí),再去 Mach-o文件中查找 teach符號(hào),發(fā)現(xiàn)是找不到的。 其主要原因是因?yàn)?code>靜態(tài)鏈接的函數(shù),實(shí)際上是不需要符號(hào)的,一旦編譯完成,其地址確定后,當(dāng)前的符號(hào)表就會(huì)刪除當(dāng)前函數(shù)對(duì)應(yīng)的符號(hào),在release環(huán)境下,符號(hào)表中存儲(chǔ)的只是不能確定地址的符號(hào)

  • 對(duì)于不能確定地址的符號(hào),是在運(yùn)行時(shí)確定的,即函數(shù)第一次調(diào)用時(shí)(相當(dāng)于懶加載),例如 print,是通過 dyld_stub_bind確定地址的

函數(shù)符號(hào)命名規(guī)則

  • 對(duì)于C函數(shù)來(lái)說(shuō),命名的重整規(guī)則就是在函數(shù)名之前加 _(注意:C中不允許函數(shù)重載,因?yàn)闆]有辦法區(qū)分)
#include <stdio.h>
void test(){}
image
  • 對(duì)于OC來(lái)說(shuō),也不支持函數(shù)重載,其符號(hào)命名規(guī)則是-[類名 函數(shù)名]
  • 對(duì)于Swift來(lái)說(shuō),是允許函數(shù)重載,主要是因?yàn)閟wift中的重整命名規(guī)則比較復(fù)雜,可以確保函數(shù)符號(hào)的唯一性

ASLR(隨機(jī)地址偏移)

新建一個(gè)iOS項(xiàng)目,在 ViewController中定義一下代碼

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

class ViewController: UIViewController {

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

動(dòng)態(tài)派發(fā)

匯編指令補(bǔ)充

ARM64匯編指令

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

探索class的調(diào)度方式

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

//聲明sil vtable關(guān)鍵字
decl ::= sil-vtable
//sil vtable中包含 關(guān)鍵字、標(biāo)識(shí)(即類名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數(shù)名稱
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:關(guān)鍵字
  • HTTeacher:表示是 HTTeacher類的函數(shù)表
  • 其次就是當(dāng)前方法的聲明對(duì)應(yīng)著方法的名稱
  • 函數(shù)表 可以理解為 數(shù)組,聲明在 class內(nèi)部的方法在不加任何關(guān)鍵字修飾的過程中,是連續(xù)存放在我們當(dāng)前的地址空間中的。這一點(diǎn),可以通過斷點(diǎn)來(lái)印證

函數(shù)表源碼探索

下面來(lái)進(jìn)行函數(shù)表底層的源碼探索

  • 源碼中搜索 initClassVTable,并加上斷點(diǎn),然后寫上源碼進(jìn)行調(diào)試
    image
  • 其內(nèi)部是通過 for循環(huán)編碼,然后offset+index偏移,然后獲取method,將其存入到偏移后的內(nèi)存中,從這里可以驗(yàn)證函數(shù)是連續(xù)存放的
  • 對(duì)于class中函數(shù)來(lái)說(shuō),類的方法調(diào)度是通過V-Taable,其本質(zhì)就是一個(gè)連續(xù)的內(nèi)存空間(數(shù)組結(jié)構(gòu))。

問題:如果更改方法聲明的位置呢?例如 extension中的函數(shù),此時(shí)的函數(shù)調(diào)度方式還是函數(shù)表調(diào)度嗎?

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

    其原因是因?yàn)?code>子類將父類的函數(shù)表全部繼承了,如果此時(shí)子類增加函數(shù),會(huì)繼續(xù)在連續(xù)的地址中插入,假設(shè)extension函數(shù)也是在函數(shù)表中,則意味著子類也有,但是子類無(wú)法并沒有相關(guān)的指針記錄函數(shù) 是父類方法 還是 子類方法,所以不知道方法該從哪里插入,導(dǎo)致extension中的函數(shù)無(wú)法安全的放入子類中。所以在這里可以側(cè)面證明extension中的方法是直接調(diào)用的,且只屬于類,子類是無(wú)法繼承的

開發(fā)注意點(diǎn):

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

final、@objc、dynamic修飾函數(shù)

final 修飾

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

@objc 修飾

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

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

image

  • 如果只是通過 @objc修飾函數(shù),OC還是無(wú)法調(diào)用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文件發(fā)現(xiàn)被 @objc修飾的函數(shù)聲明有兩個(gè):swift + OC(內(nèi)部調(diào)用的swift中的teach函數(shù))

image

即在SIL文件中生成了兩個(gè)方法

  • swift原有的函數(shù)
  • @objc標(biāo)記暴露給OC來(lái)使用的函數(shù): 內(nèi)部調(diào)用swift原有函數(shù)

dynamic 修飾

以下面代碼為例,查看 dynamic修飾的函數(shù)的調(diào)度方式

class HTTeacher: NSObject {
    dynamic func teacher() { print("teacher") }
    func teacher1() { print("teacher1") }
    func teacher2() { print("teacher2") }
    func teacher3() { print("teacher3") }
}
  • 其中 teach函數(shù)的調(diào)度還是 函數(shù)表調(diào)度,可以通過斷點(diǎn)調(diào)試驗(yàn)證,使用 dynamic的意思是可以動(dòng)態(tài)修改,意味著當(dāng)類繼承自NSObject時(shí),可以使用 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") }
}
  • 通過斷點(diǎn)調(diào)試,走的是 objc_msgSend流程,即 動(dòng)態(tài)消息轉(zhuǎn)發(fā)
    image

swift中實(shí)現(xiàn)方法交換

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

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()方法沒有實(shí)現(xiàn) 或者 沒有dynamic修飾符,會(huì)報(bào)錯(cuò)
    image

方法調(diào)度總結(jié)

  • struct值類型,其中函數(shù)的調(diào)度屬于直接調(diào)用地址,即靜態(tài)調(diào)度
  • class引用類型,其中函數(shù)的調(diào)度是通過V-Table函數(shù)表來(lái)進(jìn)行調(diào)度的,即動(dòng)態(tài)調(diào)度
  • extension中的函數(shù)調(diào)度方式是直接調(diào)度
  • final修飾的函數(shù)調(diào)度方式是直接調(diào)度
  • @objc修飾的函數(shù)調(diào)度方式是函數(shù)表調(diào)度,如果OC中需要使用,class還必須繼承NSObject
  • dynamic修飾的函數(shù)的調(diào)度方式是函數(shù)表調(diào)度,使函數(shù)具有動(dòng)態(tài)性
  • @objc + dynamic 組合修飾的函數(shù)調(diào)度,是執(zhí)行的是 objc_msgSend流程,即 動(dòng)態(tài)消息轉(zhuǎn)發(fā)

內(nèi)存插件 libfooplugin.dylib的使用

安裝和使用

方式一

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

方式二

  • 在通過lldb調(diào)試的時(shí)候,直接輸入 plugin load libfooplugin.dylib路徑

使用

  • lldb環(huán)境下,通過 cat address 地址使用

內(nèi)存分區(qū)調(diào)試實(shí)踐

堆區(qū)

對(duì)于堆區(qū)的內(nèi)存來(lái)說(shuō),就是通過 new & malloc 關(guān)鍵字來(lái)申請(qǐng)的內(nèi)存空間,不連續(xù),類似鏈表的結(jié)構(gòu),最直觀的就是類的實(shí)例對(duì)象。
定義代碼如下,通過cat查看類實(shí)例的內(nèi)存分區(qū)

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

var t = HTTeahcer()

image

從上圖可以看出,類的實(shí)例對(duì)象存儲(chǔ)在堆區(qū),即 heap pointer

棧區(qū)

查看以下代碼的 age內(nèi)存地址位于哪個(gè)區(qū)?

func test() {
    // 我們?cè)诤瘮?shù)內(nèi)部聲明的age變量就是一個(gè)局部變量
    var age: Int = 18
    print(age)
}

test()

image

從結(jié)果來(lái)看,age位于棧區(qū),即 stack address,此處的age如果用 let修飾,取不到地址

全局區(qū)

對(duì)于C的分析

下面是C語(yǔ)言的部分代碼,查看其變量的內(nèi)存地址

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

//全局靜態(tài)變量
static int age2 = 30;

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

    image

    其中 __DATA.__data表示 segment.section,這里的位置和全局區(qū)并不沖突,因?yàn)橐粋€(gè)是人為的內(nèi)存分配(內(nèi)存布局分區(qū)),一個(gè)是 Mach-O的 segment.section段中,是文件的格式劃分
    image

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

    image

    age在Mach-O文件中,放在了__DATA.__common段,主要放的就是未初始化的符號(hào)聲明(mach-o相比內(nèi)存劃分更細(xì),主要是為了更好的定位符號(hào)),當(dāng)然此時(shí)的 age在內(nèi)存中依然在全局區(qū)

  • 查看 age2(全局已初始化靜態(tài)變量)的內(nèi)存地址(其中需要注意:age2必須使用才能找到,否則會(huì)報(bào)錯(cuò))

    image

  • 觀察3個(gè)變量的地址,其地址都是相鄰的,因?yàn)樵趦?nèi)存中都放在了全局區(qū),觀察其內(nèi)存地址,可以發(fā)現(xiàn),在全局區(qū)中,未初始化變量地址已初始化變量地址

    image

  • 如果定義了一個(gè)char *p = "CJLTeacher",查看 *p,Mach-O存儲(chǔ)在__TEXT.cstring段,內(nèi)存中存儲(chǔ)在常量區(qū)

    image

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

    image

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

    image

    查看 age4的內(nèi)存地址,地址特別大,而且使用 cat查看不了,因?yàn)閙ach-o沒有記錄,age4 就是50,即使用static+const修飾的變量就相當(dāng)于直接替換

對(duì)于Swift的分析
let age = 10
  • 對(duì)于let修飾的變量,由于是不可變的,所以不能通過 po+cat查看內(nèi)存,通過匯編 首地址+偏移 來(lái)獲取 age的內(nèi)存,發(fā)現(xiàn)是在Mach-O的 __DATA.__common

    image

    從這里可以發(fā)現(xiàn),這與C中是有所區(qū)別的。swift的不同之處:已經(jīng)初始化的全局變量放在__DATA.__common段,猜測(cè)是因?yàn)?age開始是被標(biāo)記為未初始化的,當(dāng)我們執(zhí)行代碼之后才將 10存儲(chǔ)到對(duì)應(yīng)的內(nèi)存地址中

  • 如果是 var修飾的變量呢?可以發(fā)現(xiàn)與 let是一致的,還是 __DATA.__common

var age2 = 20
image
總結(jié)
  • 對(duì)于C語(yǔ)言中全局變量,根據(jù)是否已經(jīng)初始化,存儲(chǔ)在Mach-O中存儲(chǔ)位置是不同的
    • 初始化的全局變量:__DATA.__data
    • 初始化的全局變量:__DATA.__common
    • 初始化的全局靜態(tài)變量,即static修飾:__DATA.__data
    • 對(duì)于char *p類型的字符:__TEXT.cstring
    • const修飾的全局變量:__TEXT.__const
    • static+const修飾的全局變量:Mach-O中沒有記錄
  • 對(duì)于 Swift中的全局變量
    • let修飾的全局變量:__DATA.__common
    • var修飾的全局變量:__DATA.__common
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容