Swift底層進(jìn)階--004:內(nèi)存分區(qū) & 方法調(diào)度

內(nèi)存分區(qū)
內(nèi)存五大區(qū)
  • 內(nèi)存分區(qū)按地址從高到低排列: 棧區(qū)->堆區(qū)->全局靜態(tài)區(qū)->常量區(qū)-> 代碼區(qū)
  • 棧區(qū)的地址比堆區(qū)的地址大很多
  • 棧區(qū)從高地址往低地址分配空間,堆區(qū)、全局靜態(tài)區(qū)、常量區(qū)代碼區(qū)都是從低地址往高地址分配空間
  • 當(dāng)棧區(qū)堆區(qū)邊界碰撞,就會(huì)出現(xiàn)開發(fā)中的溢出。
棧區(qū)

棧區(qū)
Stack棧區(qū)

  • 從高地址往低地址分配空間,向下延伸,是連續(xù)的內(nèi)存空間
  • 棧區(qū)存放局部變量、函數(shù)調(diào)用上下文,由系統(tǒng)自動(dòng)管理,使用完由系統(tǒng)回收
堆區(qū)

堆區(qū)
Heap堆區(qū)

  • 從低地址往高地址分配空間,向上延伸,堆空間是不連續(xù)的,結(jié)構(gòu)類似鏈表
  • 通過new、malloc在堆區(qū)分配內(nèi)存空間,由開發(fā)者手動(dòng)管理,使用完手動(dòng)釋放
全局靜態(tài)區(qū)

使用c語言測(cè)試

全局靜態(tài)區(qū)
a、b、c都在全局靜態(tài)區(qū)

  • 從低地址往高地址分配空間
  • 已初始化的全局變量,存儲(chǔ)在__DATA.__data
  • 未初始化的全局變量,存儲(chǔ)在__DATA.__common
  • 未初始化比已初始化的全局變量地址更高

swiftc的差異

Swift和C的差異
main.swift中定義變量age1和常量age2。

  • age1可以正常獲取地址并打印,它存儲(chǔ)在__DATA.__common
  • age2由于是不可變,不允許使用withUnsafePointer獲取地址

使用斷點(diǎn)查看匯編代碼尋找age2的地址

匯編代碼
通過首地址+偏移地址,找到 age2地址并打印,它同樣存儲(chǔ)在__DATA.__common

常量區(qū)

使用c語言測(cè)試

常量區(qū)
a、b都在常量區(qū)

  • 從低地址往高地址分配空間
  • 常量存儲(chǔ)在__DATA.__data

查看硬編碼的字符串存放位置

char *p="Zang";

上述代碼中的字符串"Zang"存儲(chǔ)在哪里?

硬編碼的字符串存放位置
通過查看Mach-O文件,"Zang"存儲(chǔ)在__TEXT.__cstring段,內(nèi)存分區(qū)中的常量區(qū)

代碼區(qū)

代碼區(qū)
代碼段__TEXT.__text:里面存放了要執(zhí)行的匯編代碼。每一個(gè)swift文件都會(huì)經(jīng)過編譯,然后匯編形成.o文件(目標(biāo)文件),最終.o文件會(huì)合成為一個(gè)文件,當(dāng)前代碼會(huì)按照鏈接順序依次在.o文件里排列好,放在.o文件的__TEXT.__text段。

使用static const修飾的變量

使用c語言測(cè)試

使用static const修飾的變量

  • a處于全局區(qū),存儲(chǔ)在__DATA.__data
  • b處于常量區(qū),存儲(chǔ)在__DATA.__data
  • c提示找不到地址,因?yàn)槭褂?code>static const修飾的變量,Mach-O沒有記錄。c實(shí)際只是一個(gè)別名,沒有獨(dú)立內(nèi)存空間
方法調(diào)度
靜態(tài)調(diào)度

值類型的函數(shù)調(diào)用方式是靜態(tài)調(diào)度。
例如結(jié)構(gòu)體中的?法調(diào)度就是靜態(tài)調(diào)度,通過地址直接調(diào)用。在編譯、鏈接完成之后,當(dāng)前的函數(shù)地址就已經(jīng)確定,存放在代碼段__TEXT.__text,結(jié)構(gòu)體內(nèi)并不存儲(chǔ)函數(shù)地址。

struct LGTeacher{
    func test() {
        print("test")
    }
}

var t=LGTeacher()
t.test();

通過斷點(diǎn)查看匯編代碼:

函數(shù)地址
函數(shù)地址在編譯、鏈接后已經(jīng)確定,通過callq指令的跳轉(zhuǎn),直接地址調(diào)用。

打開Mach-O文件:

Mach-O
函數(shù)地址存儲(chǔ)在代碼段__TEXT.__text,而結(jié)構(gòu)體內(nèi)并不存儲(chǔ)函數(shù)地址。

函數(shù)地址后面的符號(hào),又是如何存儲(chǔ)的?

符號(hào)

打開Mach-O文件,來到Symbol Table
Symbol Table
符號(hào)存儲(chǔ)在Symbol Table符號(hào)表里面
Symbol Table:符號(hào)表,里面存儲(chǔ)的是符號(hào)位于String Table字符串表的偏移地址
命名重整:包含工程名類名、函數(shù)名、參數(shù)、參數(shù)類型等信息

Symbol Table雖然是符號(hào)表,但里面并不直接存儲(chǔ)符號(hào)。
打開Mach-O文件,來到String Table

String Table
符號(hào)字符串實(shí)際存儲(chǔ)在String Table字符串表里面
String Table:字符串表,里面存儲(chǔ)了所有變量名和函數(shù)名,它們都以字符串形式進(jìn)行存儲(chǔ)。符號(hào)字符串也在其內(nèi)
通過首地址+偏移地址可以找到相應(yīng)符號(hào)

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

Dynamic Symbol Table

通過命令操作符號(hào)表
  • 查看符號(hào)表:nmMach-O路徑】

    查看符號(hào)表

  • 搜索符號(hào):nmMach-O路徑】| grep【地址】

    搜索符號(hào)

  • 還原符號(hào)名稱:xcrun swift-demangle【符號(hào)】

    還原符號(hào)名稱

還原符號(hào)表

Release模式編譯項(xiàng)目,Mach-O中的符號(hào)表只保留不能確定地址的符號(hào)。同時(shí)在可執(zhí)行文件目錄下,多出一個(gè).dSYM文件。因?yàn)殪o態(tài)鏈接的函數(shù),實(shí)際上是不需要符號(hào)的。一旦編譯完成,其地址確定后,當(dāng)前符號(hào)表會(huì)刪除當(dāng)前函數(shù)對(duì)應(yīng)的符號(hào)。這樣可以減小Mach-O文件的大小。

  • 可執(zhí)行文件目錄下,多出一個(gè).dSYM文件
    執(zhí)行文件目錄
  • Release模式編譯后的Mach-O文件,符號(hào)表中的符號(hào)少了很多,只保留不能確定地址的符號(hào)
    Release模式編譯后的Mach-O文件
什么是不能確定地址的符號(hào)?

打開Mach-O文件,來到Lazy Symbol

Lazy Symbol
Lazy Symbol:懶加載符號(hào)表,里面存儲(chǔ)不能確定地址的符號(hào)。它們是在運(yùn)行時(shí)才能確定,即函數(shù)第一次調(diào)用時(shí)。

例如print函數(shù),通過dyld_stub_bind確定地址,很遺憾我在Xcode Version 12.3版本中沒有找到

print

函數(shù)的命名重整規(guī)則

c語言:_函數(shù)名

c語言
原函數(shù)cFunc,重整后函數(shù)符號(hào):_cFunc。簡(jiǎn)單的在函數(shù)名前面加_。所以c語言不允許函數(shù)重載,因?yàn)橹卣?guī)則過于簡(jiǎn)單,函數(shù)重載在編譯后根本無法區(qū)分。

oc-[類名 函數(shù)名]

oc
原函數(shù)ocFunc,重整后函數(shù)符號(hào):-[ocTest ocFunc]。對(duì)于oc來說,同樣不支持函數(shù)重載。

swift:包含工程名、類名、函數(shù)名、參數(shù)名、參數(shù)類型等信息

swift
原函數(shù)func test(abc : Int),重整后函數(shù)符號(hào):_$s4demo4test3abcySi_tF
原函數(shù)func test(abc : String),重整后函數(shù)符號(hào):_$s4demo4test3abcySS_tF
swift支持函數(shù)重載,它的命名重整規(guī)則也比coc復(fù)雜得多,包含工程名、類名、函數(shù)名、參數(shù)名參數(shù)類型等信息,目的是確保函數(shù)符號(hào)的唯一性。

ASLR

ASLR:隨機(jī)地址偏移(address space layout randomizes
每次APP啟動(dòng),都會(huì)隨機(jī)生成一個(gè)地址偏移值。造成編譯后Mach-O文件中的地址與App運(yùn)行時(shí)的地址產(chǎn)生偏差。

test方法上設(shè)置斷點(diǎn),使用真機(jī)運(yùn)行,可以看到運(yùn)行時(shí)test函數(shù)地址:0x100ab2cf8

運(yùn)行時(shí)函數(shù)地址

打開Mach-O文件,來到Symbol Table,搜索test,可以看到編譯時(shí)test函數(shù)地址:0x0100006CF8

編譯時(shí)函數(shù)地址
可以看到test函數(shù)地址,在運(yùn)行時(shí)和編譯時(shí)有明顯的差異

公式:

  • ASLR隨機(jī)偏移值 = 運(yùn)行時(shí)基地址 - 編譯時(shí)基地址
  • 運(yùn)行時(shí)函數(shù)地址 = 編譯時(shí)函數(shù)地址 + ASLR隨機(jī)偏移值

首先找到App運(yùn)行時(shí)基地址,使用image list打印鏡像文件的地址。第一個(gè)鏡像文件地址就是App運(yùn)行時(shí)的基地址:0x100aac000

運(yùn)行時(shí)基地址

再打開Mach-O文件,通過Load Comands->LC_SEGMENT_64(__TEXT)->VM Address,找到App編譯時(shí)的基地址:0x100000000

編譯時(shí)的基地址

通過剛才的公式進(jìn)行驗(yàn)證:
ASLR隨機(jī)偏移值:0x100aac000 - 0x100000000 = 0x000aac000
運(yùn)行時(shí)函數(shù)地址:0x0100006CF8 + 0x000aac000 = 0x100ab2cf8

通過公式進(jìn)行驗(yàn)證

通過公式計(jì)算出的結(jié)果,和斷點(diǎn)里輸出的運(yùn)行時(shí)函數(shù)地址完全一致

動(dòng)態(tài)調(diào)度

結(jié)構(gòu)體中的?法都是靜態(tài)調(diào)度,而類中的方法通過V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度。

V-table在SIL文件中的格式:

//聲明sil vtable關(guān)鍵字
decl ::= sil-vtable
//sil vtable中包含的關(guān)鍵字、標(biāo)識(shí)(當(dāng)前的類名)、所有方法
sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了聲明以及函數(shù)名稱
sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na me

通過?個(gè)簡(jiǎn)單的源?件進(jìn)行演示:

class LGTeacher{
    func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    init() {}
}

將上述代碼生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

LGTeacher函數(shù)表

  • 首先sil_vtable是關(guān)鍵字,后面LGTeacher表明當(dāng)前是LGTeacher Class的函數(shù)表
  • 其次就是當(dāng)前?法聲明對(duì)應(yīng)著?法名稱
  • 函數(shù)表本質(zhì)可以理解為數(shù)組,聲明在Class內(nèi)部的方法在不加任何關(guān)鍵字修飾的過程中,會(huì)連續(xù)存放在我們當(dāng)前的地址空間中

我們可以通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:

匯編驗(yàn)證
很明顯test1test2、test3這三個(gè)函數(shù),是連續(xù)存放在當(dāng)前的地址空間中

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)到某地址

我們還可以通過源碼進(jìn)行驗(yàn)證,搜索initClassVTable,設(shè)置斷點(diǎn)并調(diào)試:

源碼驗(yàn)證
initClassVTable的核心代碼,通過for循環(huán),從i等于0截止到VTableSize的大小。循環(huán)過程中,先通過offset+i偏移,再調(diào)用getMethod(i)得到對(duì)應(yīng)的method,將其存入偏移后的內(nèi)存中。從上述代碼可以看出,函數(shù)是連續(xù)存放在當(dāng)前的地址空間中。

extension中聲明的函數(shù),是通過V-table進(jìn)行調(diào)度嗎?
class LGTeacher {
    func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    init() {}
}

extension LGTeacher{
    func test4() {}
}

通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:

extension中的函數(shù)調(diào)用
extension中的函數(shù),并不是通過V-table函數(shù)表進(jìn)行調(diào)度,而是直接地址調(diào)用

子類繼承父類,函數(shù)表會(huì)變成什么樣?

class LGTeacher {
    func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    init() {}
}

class LGChild : LGTeacher {
    override func test2() {}
    func test5() {}
}

extension LGTeacher{
    func test4() {}
}

將上述代碼生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

LGChild函數(shù)表

  • sil_vtable LGChild中,由子類聲明的函數(shù),被追加到父類函數(shù)下面。
  • 被子類重寫的父類函數(shù),位置不變,但被記錄為子類函數(shù)。
  • 未被子類重寫的父類函數(shù),位置不變,依舊記錄為父類函數(shù)。
  • extension中的函數(shù),并不是通過V-table函數(shù)表進(jìn)行調(diào)度,也不能被子類重寫,只能被子類調(diào)用。

extension中的函數(shù),不通過V-table函數(shù)表調(diào)度而是直接地址調(diào)用,其原因在于編譯時(shí)無法將extension中的函數(shù)插入到該類函數(shù)表的正確位置。

例如子類將父類的函數(shù)表繼承后,如果存在子類聲明的函數(shù),會(huì)繼續(xù)在連續(xù)地址中插入,也就是剛才看到的子類聲明的函數(shù)被追加到父類函數(shù)的下面。而聲明extension在代碼中的位置無法確定,很有可能在子類編譯后才被讀取到。這時(shí)子類中并沒有指針記錄來區(qū)分哪些函數(shù)屬于子類、哪些函數(shù)屬于父類,故此extension中的函數(shù)無法正確插入到指定位置。這也是extension中的函數(shù)不能被子類重寫,只能被子類調(diào)用的原因。

final

使用final修飾的方法,并不是通過V-table函數(shù)表進(jìn)行調(diào)度,而是直接地址調(diào)用。不能被子類重寫,只能被子類調(diào)用。

class LGTeacher {
    final func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    init() {}
}

將上述代碼生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

LGTeacher函數(shù)表
final修飾的test1方法,在函數(shù)表里不見了。修飾后的test1方法不再通過V-table進(jìn)?調(diào)度,變成直接地址調(diào)用。

我們可以通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:

匯編代碼驗(yàn)證
final修飾的test1方法是直接地址調(diào)用。test2、test3方法首地址+偏移,是通過V-table函數(shù)表進(jìn)行調(diào)度。

@objc

使用@objc修飾可以將swift方法暴露給oc使用。

class LGTeacher {
    @objc func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    init() {}
}

將上述代碼生成SIL文件:swiftc -emit-sil main.swift | xcrun swift-demangle

LGTeacher函數(shù)表
函數(shù)表沒有發(fā)生任何變化,被@objc修飾的test1方法,依然通過V-table函數(shù)表進(jìn)行調(diào)度。

@objc修飾的方法,雖然調(diào)度方式?jīng)]有改變,但方法的聲明變成了兩個(gè)。

方法的聲明
分別出現(xiàn)了swifttest1方法和octest1方法,而octest1方法內(nèi)部調(diào)用的還是swifttest1方法。

演示一下oc如何訪問swift的方法:

class LGTeacher : NSObject {
    @objc func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    override init() {}
}

方法只通過@objc修飾方法,oc并不能訪問到,還要將Class繼承NSObject

main.swift里寫入上述代碼,編譯后找到橋接文件

找到橋接文件

打開橋接文件,可以看到被@objc修飾的方法和屬性都生成了oc代碼

demo-Swift.h

ocTest.m中導(dǎo)入頭文件,可以直接使用swift的類和方法

ocTest.m
dynamic

使用dynamic修飾的方法具有動(dòng)態(tài)特性,可動(dòng)態(tài)修改。調(diào)度方式?jīng)]有改變,依然通過V-table函數(shù)表進(jìn)行調(diào)度。

  • 使用dynamic修飾方法,如果Class繼承NSObject,可以使用method-swizzling
  • swift中的方法交換:使用dynamic修飾方法,使用@_dynamicReplacement交換方法

演示一下swift中的方法交換:

class LGTeacher {
    dynamic func test1() {
        print("test1")
    }
}

extension LGTeacher{
    @_dynamicReplacement(for:test1)
    func test2() {
        print("test2")
    }
}

var t = LGTeacher()
t.test1()

//輸出以下內(nèi)容:
//test2

方法未使用dynamic修飾,使用@_dynamicReplacement交換方法時(shí),編譯報(bào)錯(cuò)

未使用`dynamic`修飾方法

方法不存在,使用@_dynamicReplacement交換方法時(shí),編譯報(bào)錯(cuò)

方法不存在
@objc + dynamic

使用@objc + dynamic修飾方法,會(huì)改變方法的調(diào)度方式。

class LGTeacher {
    @objc dynamic func test1() {}
    func test2() {}
    func test3() {}
    @objc deinit{}
    init() {}
}

我們可以通過斷點(diǎn),查看匯編代碼進(jìn)行驗(yàn)證:

匯編代碼驗(yàn)證
test1方法的調(diào)用方式,變?yōu)橄⒄{(diào)度,使用objc_msgSend動(dòng)態(tài)消息轉(zhuǎn)發(fā)

總結(jié):
  • 值類型的函數(shù)調(diào)用方式是靜態(tài)調(diào)度
  • 引用類型通過V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度
  • extension中的函數(shù)調(diào)用方式是靜態(tài)調(diào)度
  • final修飾的函數(shù)調(diào)用方式是靜態(tài)調(diào)度
  • @objc修飾的函數(shù)通過V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度
  • dynamic修飾的函數(shù)通過V-table函數(shù)表進(jìn)行調(diào)度,是動(dòng)態(tài)調(diào)度
  • @objc + dynamic修飾的函數(shù)調(diào)用方式是消息調(diào)度,使用objc_msgSend動(dòng)態(tài)消息轉(zhuǎn)發(fā)
最后編輯于
?著作權(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)容