Swift底層進階--004:內存分區 & 方法調度

內存分區
內存五大區
  • 內存分區按地址從高到低排列: 棧區->堆區->全局靜態區->常量區-> 代碼區
  • 棧區的地址比堆區的地址大很多
  • 棧區從高地址往低地址分配空間,堆區全局靜態區常量區代碼區都是從低地址往高地址分配空間
  • 棧區堆區邊界碰撞,就會出現開發中的溢出。
棧區

棧區
Stack棧區

  • 從高地址往低地址分配空間,向下延伸,是連續的內存空間
  • 棧區存放局部變量、函數調用上下文,由系統自動管理,使用完由系統回收
堆區

堆區
Heap堆區

  • 從低地址往高地址分配空間,向上延伸,堆空間是不連續的,結構類似鏈表
  • 通過newmalloc在堆區分配內存空間,由開發者手動管理,使用完手動釋放
全局靜態區

使用c語言測試

全局靜態區
abc都在全局靜態區

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

swiftc的差異

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

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

使用斷點查看匯編代碼尋找age2的地址

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

常量區

使用c語言測試

常量區
ab都在常量區

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

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

char *p="Zang";

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

硬編碼的字符串存放位置
通過查看Mach-O文件,"Zang"存儲在__TEXT.__cstring段,內存分區中的常量區

代碼區

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

使用static const修飾的變量

使用c語言測試

使用static const修飾的變量

  • a處于全局區,存儲在__DATA.__data
  • b處于常量區,存儲在__DATA.__data
  • c提示找不到地址,因為使用static const修飾的變量,Mach-O沒有記錄。c實際只是一個別名,沒有獨立內存空間
方法調度
靜態調度

值類型的函數調用方式是靜態調度。
例如結構體中的?法調度就是靜態調度,通過地址直接調用。在編譯、鏈接完成之后,當前的函數地址就已經確定,存放在代碼段__TEXT.__text,結構體內并不存儲函數地址。

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

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

通過斷點查看匯編代碼:

函數地址
函數地址在編譯、鏈接后已經確定,通過callq指令的跳轉,直接地址調用。

打開Mach-O文件:

Mach-O
函數地址存儲在代碼段__TEXT.__text,而結構體內并不存儲函數地址。

函數地址后面的符號,又是如何存儲的?

符號

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

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

String Table
符號字符串實際存儲在String Table字符串表里面
String Table:字符串表,里面存儲了所有變量名和函數名,它們都以字符串形式進行存儲。符號字符串也在其內
通過首地址+偏移地址可以找到相應符號

Dynamic Symbol Table:動態庫函數位于符號表的偏移信息

Dynamic Symbol Table

通過命令操作符號表
  • 查看符號表:nmMach-O路徑】

    查看符號表

  • 搜索符號:nmMach-O路徑】| grep【地址】

    搜索符號

  • 還原符號名稱:xcrun swift-demangle【符號】

    還原符號名稱

還原符號表

Release模式編譯項目,Mach-O中的符號表只保留不能確定地址的符號。同時在可執行文件目錄下,多出一個.dSYM文件。因為靜態鏈接的函數,實際上是不需要符號的。一旦編譯完成,其地址確定后,當前符號表會刪除當前函數對應的符號。這樣可以減小Mach-O文件的大小。

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

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

Lazy Symbol
Lazy Symbol:懶加載符號表,里面存儲不能確定地址的符號。它們是在運行時才能確定,即函數第一次調用時。

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

print

函數的命名重整規則

c語言:_函數名

c語言
原函數cFunc,重整后函數符號:_cFunc。簡單的在函數名前面加_。所以c語言不允許函數重載,因為重整規則過于簡單,函數重載在編譯后根本無法區分。

oc-[類名 函數名]

oc
原函數ocFunc,重整后函數符號:-[ocTest ocFunc]。對于oc來說,同樣不支持函數重載。

swift:包含工程名類名函數名參數名參數類型等信息

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

ASLR

ASLR:隨機地址偏移(address space layout randomizes
每次APP啟動,都會隨機生成一個地址偏移值。造成編譯后Mach-O文件中的地址與App運行時的地址產生偏差。

test方法上設置斷點,使用真機運行,可以看到運行時test函數地址:0x100ab2cf8

運行時函數地址

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

編譯時函數地址
可以看到test函數地址,在運行時和編譯時有明顯的差異

公式:

  • ASLR隨機偏移值 = 運行時基地址 - 編譯時基地址
  • 運行時函數地址 = 編譯時函數地址 + ASLR隨機偏移值

首先找到App運行時基地址,使用image list打印鏡像文件的地址。第一個鏡像文件地址就是App運行時的基地址:0x100aac000

運行時基地址

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

編譯時的基地址

通過剛才的公式進行驗證:
ASLR隨機偏移值:0x100aac000 - 0x100000000 = 0x000aac000
運行時函數地址:0x0100006CF8 + 0x000aac000 = 0x100ab2cf8

通過公式進行驗證

通過公式計算出的結果,和斷點里輸出的運行時函數地址完全一致

動態調度

結構體中的?法都是靜態調度,而類中的方法通過V-table函數表進行調度,是動態調度。

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

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

通過?個簡單的源?件進行演示:

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

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

LGTeacher函數表

  • 首先sil_vtable是關鍵字,后面LGTeacher表明當前是LGTeacher Class的函數表
  • 其次就是當前?法聲明對應著?法名稱
  • 函數表本質可以理解為數組,聲明在Class內部的方法在不加任何關鍵字修飾的過程中,會連續存放在我們當前的地址空間中

我們可以通過斷點,查看匯編代碼進行驗證:

匯編驗證
很明顯test1test2test3這三個函數,是連續存放在當前的地址空間中

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:跳轉到某地址

我們還可以通過源碼進行驗證,搜索initClassVTable,設置斷點并調試:

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

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

extension LGTeacher{
    func test4() {}
}

通過斷點,查看匯編代碼進行驗證:

extension中的函數調用
extension中的函數,并不是通過V-table函數表進行調度,而是直接地址調用

子類繼承父類,函數表會變成什么樣?

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函數表

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

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

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

final

使用final修飾的方法,并不是通過V-table函數表進行調度,而是直接地址調用。不能被子類重寫,只能被子類調用。

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

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

LGTeacher函數表
final修飾的test1方法,在函數表里不見了。修飾后的test1方法不再通過V-table進?調度,變成直接地址調用。

我們可以通過斷點,查看匯編代碼進行驗證:

匯編代碼驗證
final修飾的test1方法是直接地址調用。test2test3方法首地址+偏移,是通過V-table函數表進行調度。

@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函數表
函數表沒有發生任何變化,被@objc修飾的test1方法,依然通過V-table函數表進行調度。

@objc修飾的方法,雖然調度方式沒有改變,但方法的聲明變成了兩個。

方法的聲明
分別出現了swifttest1方法和octest1方法,而octest1方法內部調用的還是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中導入頭文件,可以直接使用swift的類和方法

ocTest.m
dynamic

使用dynamic修飾的方法具有動態特性,可動態修改。調度方式沒有改變,依然通過V-table函數表進行調度。

  • 使用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()

//輸出以下內容:
//test2

方法未使用dynamic修飾,使用@_dynamicReplacement交換方法時,編譯報錯

未使用`dynamic`修飾方法

方法不存在,使用@_dynamicReplacement交換方法時,編譯報錯

方法不存在
@objc + dynamic

使用@objc + dynamic修飾方法,會改變方法的調度方式。

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

我們可以通過斷點,查看匯編代碼進行驗證:

匯編代碼驗證
test1方法的調用方式,變為消息調度,使用objc_msgSend動態消息轉發

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

推薦閱讀更多精彩內容