Swift中结构体的方法调度&内存分区
函数方法调度
-
结构体的方法调度
如下结构体
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
(常量字符串)里,内存分区中位于常量区
。