Swift中結(jié)構(gòu)體的方法調(diào)度&內(nèi)存分區(qū)

函數(shù)方法調(diào)度

  • 結(jié)構(gòu)體的方法調(diào)度

如下結(jié)構(gòu)體

struct YYTeacher {
    func teach() {
        print("teach")
    }
}

var t = YYTeacher()
# 此處添加斷點(diǎn)
t.teach()

匯編模式下,可知結(jié)構(gòu)體函數(shù)調(diào)用方式是靜態(tài)調(diào)用(直接調(diào)用):

通過在MachOView中打開可執(zhí)行文件

  • __Text,__text:代碼段
    編譯時(shí),每一個(gè)swift文件都會(huì)經(jīng)過編譯、匯編形成.o文件(目標(biāo)文件),所有的.o文件,最終會(huì)合成一個(gè)文件,當(dāng)前代碼會(huì)根據(jù)鏈接順序依次在.o中排列好統(tǒng)一放在text字段里。

通過上圖可知:在調(diào)用函數(shù)時(shí),不用再去其他地方查找teach的函數(shù)地址,編譯鏈接完成之后,地址就已經(jīng)確定放在text字段里;所以說結(jié)構(gòu)體的函數(shù)調(diào)度方式是靜態(tài)調(diào)度,意味著結(jié)構(gòu)體會(huì)存儲(chǔ)其中的函數(shù),執(zhí)行效率非常。

  • Symbol Table:符號(hào)表,用于調(diào)試過程

存儲(chǔ)的是符號(hào)位于(String Table)字符串表中的位置,直接存儲(chǔ)符號(hào)

  • String Table:字符串表
    所有的變量名函數(shù)名字符串的形式存放在字符串表中。

符號(hào)經(jīng)過swift命令重整(nm)變成了符號(hào)表中存放的內(nèi)容。
所以也可以通過以下命令在終端拿到符號(hào)表

nm  path:拿到符號(hào)表
nm  path | grep addr:在符號(hào)表中通過指定函數(shù)地址搜索指定符號(hào)

其中:
path-->可執(zhí)行文件的地址
addr-->指定函數(shù)地址
如下圖:

Release模式下,會(huì)多生成一個(gè).dsYM文件用于捕獲崩潰查找debug信息,在線上使用該文件。 符號(hào)表保留那些靜態(tài)鏈接的函數(shù)符號(hào)(在字符串表中的位置信息),因?yàn)橐坏┚幾g完成就能確定地址,這時(shí)符號(hào)表精簡很多,不占用macho文件大小,保留的是那些不能確定地址的符號(hào)(在字符串表中的位置)。

總結(jié):靜態(tài)調(diào)度的函數(shù)一旦編譯完成就能確定地址,再通過地址調(diào)用函數(shù),只是在debug模式下為了方便調(diào)試才將該地址的符號(hào)信息以字符串形式存儲(chǔ)字符串表中,在字符串表中的位置信息存儲(chǔ)符號(hào)表中,并是通過符號(hào)表中去查找到函數(shù)地址再進(jìn)行調(diào)度,要注意先后順序。

  • Dynamic Symbol Table動(dòng)態(tài)庫函數(shù)位于符號(hào)表偏移信息

  • 命令重整規(guī)則

  • 對(duì)于C的函數(shù)來說, 其命令重整就是直接在函數(shù)前加“_”,所以如果在C里面定義兩個(gè)同名的函數(shù),即使參數(shù)和返回值不同,也是不被允許的。

  • 對(duì)于OC的函數(shù)來說,其命令重整則是 -[YYTeacher test:],所以定義兩個(gè)相同名稱、相同參數(shù)個(gè)數(shù)的函數(shù),即使返回值不一樣,也是不被允許的。因?yàn)檎{(diào)用函數(shù)是通過classselector去查找的,它只根據(jù)函數(shù)名參數(shù)個(gè)數(shù)去查找,如果函數(shù)名和參數(shù)個(gè)數(shù)都一樣,查找出來多個(gè)就不知道調(diào)用哪個(gè)函數(shù)。

  • 對(duì)于Swift的函數(shù),命令重整就比較復(fù)雜,確保符號(hào)的唯一性。這樣就使得Swift中可以定義多個(gè)名稱相同、參數(shù)類型不同的函數(shù)。

  • 疑問每一次運(yùn)行靜態(tài)調(diào)用的函數(shù)地址都是一樣的嗎
    答:每一次運(yùn)行函數(shù)地址是絕對(duì)一樣的,因?yàn)樗Q于偏移地址ASLR地址隨機(jī)化)。

首先需了解:
程序的靜態(tài)基地址:在Load Commands__TEXT字段里,VM Address就是靜態(tài)基地址。

程序運(yùn)行首地址:在lldb中通過image list命令來查看首地址。

隨機(jī)偏移地址:在可執(zhí)行程序隨機(jī)裝載到內(nèi)存中時(shí)的隨機(jī)地址,就是我們當(dāng)前這application偏移的地址??赏ㄟ^程序運(yùn)行首地址 - 程序的靜態(tài)基地址得到。

最終:靜態(tài)函數(shù)的地址 = 符號(hào)表中函數(shù)地址 + 隨機(jī)偏移地址

通過上圖可知:
偏移地址 = 程序運(yùn)行首地址 - 程序的靜態(tài)基地址即0x5a47000

計(jì)算一下:靜態(tài)函數(shù)的地址 = 符號(hào)表中函數(shù)地址 + 隨機(jī)偏移地址 即
0x105a48db0 = 0x100001DB0 + 0x5a47000

  • 類的方法調(diào)度
  • 一般情況下,類中的方法是通過V-Table來進(jìn)行調(diào)度。
    首先了解V-TableSIL中怎樣表示的,如下圖:

這張表的本質(zhì)其實(shí)就類似我們理解的數(shù)組,聲明在class內(nèi)部的方法在加任何關(guān)鍵字修飾的過程中,連續(xù)存放在我們當(dāng)前的地址空間中。

首先了解ARM64下的幾個(gè)匯編指令

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

通過以下例子在匯編模式下:

class YYTeacher {
    func teach() {print("teach")}
    func teach1() {print("teach1")}
    func teach2() {print("teach2")}
    func teach3() {print("teach3")}
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        var t = YYTeacher()
        t.teach()
        t.teach1()
        t.teach2()
        t.teach3()
    }
}

可以看出上面的函數(shù)都是按順序放在函數(shù)表中。

接下來通過SIL中查源碼斷點(diǎn)來看一下:

可看出V-Table就是一個(gè)數(shù)組結(jié)構(gòu)。

  • extension聲明的方法

如果更改方法聲明的位置,將方法放在extension中聲明:

extension YYTeacher {
    func teach4() {
        print("teach4")
    }
}

匯編模式下可看出:如果方法聲明放在extension中,則是直接地址調(diào)用。為什么呢?舉個(gè)例子:在Swift中,一個(gè)類有子類,有extension,extension可以寫在任意Swift文件中,如果子類所在文件優(yōu)extension所在文件加載,子類的函數(shù)表會(huì)首先繼承父類的函數(shù)表,其次是自己的函數(shù)列表,當(dāng)加載到extension時(shí)發(fā)現(xiàn)有函數(shù),這時(shí)子類中沒有指針記錄哪些是父類方法哪些是自己的方法,就沒法將extension中的方法按順序的插入自己的函數(shù)表中。

擴(kuò)展:OC中分類方法的調(diào)用

  • final關(guān)鍵字:意味著當(dāng)前方法能被子類繼承只能調(diào)用,該方法也不會(huì)加入V-Table中,聲明之后直接調(diào)用。
class YYTeacher {
    final func teach() { print("teach")  }
    func teach1() { print("teach1") }
}

var t = YYTeacher()
t.teach()
t.teach1()

匯編模式下直接地址調(diào)用

SILV-Table中也沒有加入final修飾的teach函數(shù):

  • @objc關(guān)鍵字:暴露頭文件和當(dāng)前方法OC調(diào)用,在匯編模式下可知其方法還是通過V-Table進(jìn)行調(diào)度。
    SIL中可看出:編譯后生成了兩個(gè)函數(shù)YYTeacher.teach()@objc YYTeacher.teach(),而在@objc YYTeacher.teach()函數(shù)內(nèi)部又調(diào)用YYTeacher.teach()函數(shù)。

OC-Swift 橋接演示:
OC項(xiàng)目中新建Swift文件并選擇Create Bridging Header,Swift中:

class YYTeacher: NSObject {
    @objc func teach() {
        print("teach")
    }
    func teach1() {
        print("teach1")
    }
}

要在OC中使用Swift文件,就需要導(dǎo)入頭文件,頭文件查看方式如下:

如果YYTeacher不繼承NSObject,該頭文件中則沒有與YYTeacher相關(guān)的類信息,就不能訪問到YYTeacher這個(gè)類。
繼承NSObject后頭文件中才有下列信息:

接下來在OC文件中:

OC中,只能訪問到有@objc修飾的teach函數(shù),而沒有@objc修飾的teach1則不能被訪問到。

  • dynamic關(guān)鍵字
    匯編模式下可看出:dynamic修飾的函數(shù)依然是通過V-Table函數(shù)表調(diào)用,表示可以動(dòng)態(tài)修改。
  • Swift 中的函數(shù)可以是靜態(tài)調(diào)用,靜態(tài)調(diào)用會(huì)更快。Swift的代碼直接被編譯優(yōu)化成靜態(tài)調(diào)用的時(shí)候,就不能從Objective-C 中的SEL字符串來查找到對(duì)應(yīng)的IMP了。這樣就需要在 Swift 中添加一個(gè)關(guān)鍵字 dynamic,告訴編譯器這個(gè)方法是可能被動(dòng)態(tài)調(diào)用的,需要將其添加到查找表中。

  • 繼承自NSObjectSwift類,其繼承自父類的方法具有動(dòng)態(tài)性,其他自定義方法、屬性需要加dynamic修飾才可以獲得動(dòng)態(tài)性。

  • 如果方法的參數(shù)、屬性類型為Swift特有、無法映射到Objective-C的類型(如CharacterTuple),則此方法、屬性無法添加dynamic修飾, 一旦添加就會(huì)編譯報(bào)錯(cuò)。

  • 通過dynamic修飾的方法可以被動(dòng)態(tài)替換

class YYTeacher {
    dynamic func teach() {print("teach")}
}

extension YYTeacher {
    @_dynamicReplacement(for: teach)
    func teach1() {
        print("teach1")
    }
}

var t = YYTeacher()
t.teach()

這時(shí)調(diào)用t.teach()打印的則是teach1@_dynamicReplacement(for:teach)extension中將teach()動(dòng)態(tài)替換成teach1()。

  • @objc + dynamic
    在匯編模式下可知,被@objc + dynamic修飾的方法變成了動(dòng)態(tài)消息轉(zhuǎn)發(fā):

內(nèi)存分區(qū)

內(nèi)存分區(qū)模型如下圖:

  • 棧區(qū)(Stack):存放的是函數(shù)內(nèi)部聲明的局部變量和函數(shù)運(yùn)行過程中的上下文
func test() {
    var age : Int = 10
    print(age)
}

上面例子中的age就存放在內(nèi)存中。

  • 堆區(qū)(Heap):存放的是通過new & malloc關(guān)鍵字來申請(qǐng)的內(nèi)存空間,不連續(xù),類似鏈表的結(jié)構(gòu),最直觀就是對(duì)象。
class YYTeacher {
    var age : Int = 10
}
var t = YYTeacher()

上面例子中的t里面存放的地址就是在堆區(qū)地址。

  • 全局區(qū)
int a = 10;
int age;
 
static int age2 = 30;

int main(int argc, const char * argv[]) {
    char *p = "YYTeacher";
    printf("%d", a);
    printf("%d", age2); // 如果不訪問age2,直接在lldb中獲取age2的地址,是獲取不到的,因?yàn)椴皇褂脛t不記錄。
    return 0;
}

在上面例子中,

注意:SEGMENTSECTIONMacho文件對(duì)格式的劃分,而內(nèi)存分區(qū)是人為對(duì)內(nèi)存布局的分區(qū),所以對(duì)于上面例子中a存放在全局區(qū)和在Macho文件中存放__DATA.__data里面互不沖突。

從上面圖片中可以看出,全局已初始化變量a和age2的地址比較接近,而且比全局未初始化變量的地址,可以更詳細(xì)的對(duì)全局區(qū)進(jìn)行分區(qū):

如果例子中加入全局已初始化靜態(tài)常量

int a = 10;
int age;
 
static int age2 = 30;
static const int age3 = 30;

int main(int argc, const char * argv[]) {
    char *p = "YYTeacher";
    printf("%d", a);
    printf("%d", age2);
    int b = age3;
    return 0;
}

因?yàn)?code>age3是靜態(tài)不可修改的,macho文件直接會(huì)記錄age3符號(hào)信息,賦值過程中對(duì)于編譯器來說age3這個(gè)符號(hào)根本不存在,就是一個(gè)值30,這里的int b = age3就相當(dāng)于int b = 30

對(duì)于Swift來說,let age = 10
這種情況下,因?yàn)閍ge是不可變的,所以不允許通過po withUnsafePointer(to: &age){print($0)}這種方式來獲取age的地址。

可以通過以下方式在匯編模式下來獲取age的地址為0x100008028

可知age的符號(hào)信息在macho文件中存放在__DATA.__common里面.
綜上可知:和C/OC相比,Swift對(duì)于全局變量在Macho文件中的劃分規(guī)則不一樣的.

  • 常量區(qū)
    例子中p的符號(hào)信息在Macho文件中位于__TEXT.__cstring(常量字符串)里,內(nèi)存分區(qū)中位于常量區(qū)。
最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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