函數(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ù)是通過class的selector去查找的,它只根據(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-Table在SIL中怎樣表示的,如下圖:

這張表的本質(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)用:

SIL的V-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)用的,需要將其添加到查找表中。繼承自
NSObject的Swift類,其繼承自父類的方法具有動(dòng)態(tài)性,其他自定義方法、屬性需要加dynamic修飾才可以獲得動(dòng)態(tài)性。如果方法的
參數(shù)、屬性類型為Swift特有、無法映射到Objective-C的類型(如Character、Tuple),則此方法、屬性無法添加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;
}
在上面例子中,

注意:SEGMENT和SECTION是Macho文件對(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ū)。