函數方法調度
-
結構體的方法調度
如下結構體
struct YYTeacher {
func teach() {
print("teach")
}
}
var t = YYTeacher()
# 此處添加斷點
t.teach()
在匯編模式
下,可知結構體
的函數
調用方式是靜態調用
(直接調用):
通過在MachOView
中打開可執行文件
:
-
__Text,__text
:代碼段
編譯
時,每一個swift
文件都會經過編譯
、匯編
形成.o
文件(目標
文件),所有的.o
文件,最終會合成一個文件,當前代碼會根據鏈接
順序依次在.o中排列好統一放在text
字段里。
通過上圖可知:在調用函數
時,不用再去其他地方查找teach的函數地址,編譯鏈接
完成之后,地址就已經確定
放在text
字段里;所以說結構體
的函數調度方式是靜態
調度,意味著結構體
是不
會存儲
其中的函數
,執行效率
非常高
。
-
Symbol Table
:符號表,用于調試
過程
存儲的是符號
位于(String Table
)字符串表中的位置
,不
直接存儲符號
。
-
String Table
:字符串表
所有的變量名
和函數名
以字符串
的形式存放在字符串表
中。
符號
經過swift
命令重整(nm
)變成了符號表中存放的內容。
所以也可以通過以下命令在終端
拿到符號表
:
nm path:拿到符號表
nm path | grep addr:在符號表中通過指定函數地址搜索指定符號
其中:
path
-->可執行文件
的地址
addr
-->指定函數地址
如下圖:
在Release
模式下,會多生成一個.dsYM
文件用于捕獲崩潰
、查找debug
信息,在線上使用
該文件。 符號表
中不
再保留
那些靜態鏈接的函數符號(在字符串表中的位置信息),因為一旦編譯完成就能確定地址,這時符號表精簡很多
,不占用macho文件大小,保留的是那些不能確定地址的符號
(在字符串表中的位置)。
總結:靜態調度的函數一旦編譯完成
就能確定地址
,再通過地址
調用函數,只是在debug
模式下為了方便調試
才將該地址的符號信息
以字符串形式存儲
在字符串表
中,在字符串表中的位置信息
又存儲
在符號表
中,并不
是通過符號表
中去查找到函數地址
再進行調度,要注意先后順序
。
Dynamic Symbol Table
:動態庫
函數位于符號表
的偏移信息
命令重整規則:
對于
C
的函數來說, 其命令重整
就是直接在函數前加“_
”,所以如果在C里面定義兩個同名
的函數,即使參數和返回值不同
,也是不被允許
的。對于
OC
的函數來說,其命令重整則是-[YYTeacher test:]
,所以定義兩個
相同名稱
、相同參數個數
的函數,即使返回值
不一樣,也是不被允許
的。因為調用函數是通過class
的selector
去查找的,它只根據函數名
和參數個數
去查找,如果函數名和參數個數都一樣,查找出來多個就不知道調用哪個函數。對于
Swift
的函數,命令重整就比較復雜
,確保符號的唯一性
。這樣就使得Swift
中可以定義多個名稱相同
、參數類型不同
的函數。疑問:
每一次運行靜態調用的函數地址都是一樣的嗎
?
答:每一次運行函數地址不
是絕對一樣
的,因為它取決于偏移地址
(ASLR
地址隨機
化)。
首先需了解:
程序的靜態基地址
:在Load Commands
中__TEXT
字段里,VM Address
就是靜態基地址。
程序運行首地址
:在lldb
中通過image list
命令來查看
首地址。
隨機偏移地址
:在可執行程序隨機裝載到內存中時的隨機地址,就是我們當前這application偏移的地址??赏ㄟ^程序運行首地址 - 程序的靜態基地址
得到。
最終:靜態函數的地址 = 符號表中函數地址 + 隨機偏移地址
通過上圖可知:
偏移地址
= 程序運行首地址 - 程序的靜態基地址即0x5a47000
計算一下:靜態函數的地址 = 符號表中函數地址 + 隨機偏移地址 即
0x105a48db0 = 0x100001DB0 + 0x5a47000
-
類的方法調度
- 一般情況下,類中的方法是通過
V-Table
來進行調度。
首先了解V-Table
在SIL
中怎樣表示的,如下圖:
這張表的本質其實就類似我們理解的數組
,聲明在class
內部的方法在不
加任何關鍵字
修飾的過程中,連續
存放在我們當前的地址空間
中。
首先了解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:跳轉到某地址
通過以下例子在匯編模式下:
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()
}
}
可以看出上面的函數都是按順序
放在函數表
中。
接下來通過SIL
中查源碼斷點來看一下:
可看出V-Table
就是一個數組
結構。
-
extension
聲明的方法
如果更改方法聲明的位置
,將方法放在extension
中聲明:
extension YYTeacher {
func teach4() {
print("teach4")
}
}
匯編模式下可看出:如果方法聲明放在extension
中,則是直接地址調用
。為什么呢?舉個例子:在Swift中,一個類有子類
,有extension
,extension可以寫在任意Swift文件中,如果子類
所在文件優先
于extension
所在文件加載
,子類的函數表會首先繼承
父類的函數表,其次是自己的函數列表,當加載到extension時發現有函數,這時子類中沒有指針記錄哪些是父類方法哪些是自己的方法,就沒法將extension中的方法按順序
的插入自己的函數表
中。
擴展:OC中分類方法的調用
-
final
關鍵字:意味著當前方法不
能被子類繼承
只能調用
,該方法也不會加入V-Table
中,聲明之后直接調用
。
class YYTeacher {
final func teach() { print("teach") }
func teach1() { print("teach1") }
}
var t = YYTeacher()
t.teach()
t.teach1()
匯編模式下直接地址調用
:
SIL
的V-Table
中也沒有加入final
修飾的teach
函數:
-
@objc
關鍵字:暴露頭文件
和當前方法
給OC
調用,在匯編模式下可知其方法還是通過V-Table
進行調度。
在SIL
中可看出:編譯后生成了兩個函數YYTeacher.teach()
和@objc YYTeacher.teach()
,而在@objc YYTeacher.teach()
函數內部又調用
了YYTeacher.teach()
函數。
OC-Swift
橋接演示:
在OC
項目中新建Swift
文件并選擇Create Bridging Header
,Swift中:
class YYTeacher: NSObject {
@objc func teach() {
print("teach")
}
func teach1() {
print("teach1")
}
}
要在OC
中使用Swift
文件,就需要導入頭文件
,頭文件查看方式如下:
如果YYTeacher
不繼承NSObject
,該頭文件中則沒有與YYTeacher
相關的類信息,就不能訪問到YYTeacher
這個類。
繼承NSObject
后頭文件中才有下列信息:
接下來在OC
文件中:
在OC
中,只能訪問到有@objc
修飾的teach
函數,而沒有@objc
修飾的teach1
則不能被訪問到。
-
dynamic
關鍵字
匯編模式下可看出:dynamic
修飾的函數依然是通過V-Table
函數表調用,表示可以動態
修改。
Swift
中的函數可以是靜態
調用,靜態調用會更快。Swift的代碼直接被編譯優化成靜態調用的時候,就不能從Objective-C
中的SEL
字符串來查找到對應的IMP
了。這樣就需要在Swift
中添加一個關鍵字dynamic
,告訴編譯器這個方法是可能被動態調用
的,需要將其添加到查找表中。繼承自
NSObject
的Swift
類,其繼承自父類
的方法具有動態性
,其他自定義方法
、屬性
需要加dynamic
修飾才可以獲得動態性
。如果方法的
參數
、屬性
類型為Swift
特有、無法映射到Objective-C
的類型(如Character
、Tuple
),則此方法、屬性無法添加dynamic
修飾, 一旦添加就會編譯報錯
。通過
dynamic
修飾的方法可以被動態替換
class YYTeacher {
dynamic func teach() {print("teach")}
}
extension YYTeacher {
@_dynamicReplacement(for: teach)
func teach1() {
print("teach1")
}
}
var t = YYTeacher()
t.teach()
這時調用t.teach()
打印的則是teach1
,@_dynamicReplacement(for:teach)
在extension
中將teach()
動態替換成teach1()
。
-
@objc + dynamic
在匯編模式下可知,被@objc + dynamic
修飾的方法變成了動態消息轉發:
內存分區
內存分區
模型如下圖:
-
棧區(Stack):存放的是
函數內部
聲明的局部變量
和函數運行過程中的上下文
。
func test() {
var age : Int = 10
print(age)
}
上面例子中的age
就存放在棧
內存中。
-
堆區(Heap):存放的是通過
new & malloc
關鍵字來申請的內存空間,不連續
,類似鏈表
的結構,最直觀就是對象
。
class YYTeacher {
var age : Int = 10
}
var t = YYTeacher()
上面例子中的t里面存放的地址
就是在堆區
地址。
- 全局區
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的地址,是獲取不到的,因為不使用則不記錄。
return 0;
}
在上面例子中,
注意:SEGMENT
和SECTION
是Macho
文件對格式
的劃分,而內存分區是人為對內存布局
的分區,所以對于上面例子中a
存放在全局區
和在Macho
文件中存放__DATA.__data
里面互不沖突。
從上面圖片中可以看出,全局已初始化變量
a和age2的地址比較接近
,而且比全局未初始化變量
的地址低
,可以更詳細的對全局區進行分區:
如果例子中加入全局已初始化靜態常量
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;
}
因為age3
是靜態不可修改
的,macho文件直接不
會記錄age3
的符號
信息,賦值
過程中對于編譯器來說age3
這個符號
根本不存在
,就是一個值30
,這里的int b = age3
就相當于int b = 30
。
對于Swift來說,let age = 10
這種情況下,因為age是不可變的,所以不允許通過po withUnsafePointer(to: &age){print($0)}
這種方式來獲取age的地址。
可以通過以下方式在匯編模式
下來獲取age
的地址為0x100008028
:
可知age
的符號信息在macho
文件中存放在__DATA.__common
里面.
綜上可知:和C/OC
相比,Swift
對于全局
變量在Macho
文件中的劃分規則
是不一樣
的.
-
常量區
例子中p
的符號信息在Macho
文件中位于__TEXT.__cstring
(常量字符串)里,內存分區中位于常量區
。