Swift进阶:Runtime机制
Swift类的对象创建和销毁
Swift中可以定义两种类:
- 从
NSObject
或者其派生类派生的类(NSObject体系的子类) - 从Swift基类
SwiftObject
派生的类(在定义类时没有指定基类时,默认会从基类SwiftObject派生)
Swift中类的实例对象都是在堆内存中创建的。系统会为类提供一个默认的init
构造函数,如果想自定义构造函数,需要重写重载init
函数。
类的实例对象的构建
不像OC那样将
alloc
和init
分开调用;直接使用
类型(初始化参数)
来完成实例对象的创建:let lily = Person("Lily")
在编译时,系统为每个类的初始化方法生成一个:
模块名.类名.__allocating_init(类名, 初始化参数)
的函数伪代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16/** 定义了一个类 */
class Person {
init(_ name: String){}
}
/** 编译生成的对象内存分配创建和初始化函数 */
Person * XXX.Person.__allocating_init(swift_class classPerson, String name){
Person *obj = swift_allocObject(classPerson); // 分配内存
obj->init(name); // 调用初始化函数
}
/** 编译时还会生成对象的析构和内存销毁函数 */
XXX.Person__deallocating_deinit(Person *obj) {
obj->deinit(); // 调用析构函数
swift_deallocClassInstance(obj); // 销毁对象分配的内存
}其中的
swift_class
就是从objc_class
派生出来的,是用于描述类信息的结构体。
Swift对象的生命周期
在Swift中,实例对象的生命周期也是通过引用计数来控制的。
- 当对象创建时,引用计数为1;
- 对对象赋值操作,会调用
swift_retain
函数来增加引用计数; - 对象不再被访问,会调用
swift_release
函数来减少引用计数; - 当对象引用计数变成0时,最终通过
模块名.类名__deallocating_deinit
函数对对象析构和销毁。
Swift类中的对象方法调用的区分
Swift中的类定义的方法分为三类:
- OC类的派生类并重写了基类的方法
extension
中定义的方法- 类中定义的常规方法
方法的调用需要区分为两种情况:
- 关闭编译链接优化
- 开启编译链接优化:release模式下默认开启
关闭编译链接优化
OC类的派生类并重写了基类的方法
在Swift中使用了OC类,并重写了基类的方法,这些重写的方法定义信息还是会保存在类的Class结构体中,在调用上还是采用OC语言的Runtime
机制来实现,也就是通过objc_msgSend
来调用。比如Swift中使用UIViewController
并重写了viewDidLoad
方法。
在Swift中使用了OC类,但在这个类中定义了一个新方法,那就是在类中定义的常规方法这一类了。
extension中定义的方法
- 在Swift类的
extension
中定义的方法(重写OC基类的方法除外)的调用是在编译时就决定; - 在调用这种对象方法时,方法调用指令中的函数地址将会以硬编码的形式存在;
- 这些方法的符号信息都不会保存到类的描述信息中——这也是Swift中派生类无法重写基类的extension中定义的方法的原因——
extension
中的方法调用是硬编码完成的,无法支持多态! - 在
extension
中定义的方法无法在运行时做任何的替换和改变; extension
中可以重写OC基类的方法,但不能重写Swift类中定义的方法。
类中定义的常规方法
在Swift中的定义的常规方法的调用机制和C++中的虚函数调用机制相似。
swift为每个类都建立了一个被称为续表的数据结构,这个数据保存着类中所有的常规的成员方法函数的地址;
每个Swift类的实例对象的内存布局中的第一个成员都是
isa
(跟OC相似);isa
中保存着Swift类的描述信息;类的虚函数表保存在类描述结构的第
0x50
个字节的偏移处;每个虚表条目中保存着一个常规方法的函数地址指针;
每个对象方法调用的源代码在编译时就会转化为从虚表中取对应偏移位置的函数地址来实现间接的函数调用。
伪代码:
1
2
3
4
5
6
7CB *objB = CB.__allocating_init(classCB);
objB->isa = &classCB;
asm("mov x20, objB");
objB->isa->vtable[0](10);
objB->isa->vtable[1](10,20);
objB->isa->vtable[2]();
objB->isa->vtable[3]();
Swift函数调用的一些变化
Swift类的常规方法中没有两个隐藏的参数
self
和_cmd
了。那么方法调用时对象如何被引用和传递呢?
其他语言中一般情况下对象总是作为方法的第一个参数,在编译阶段,将对象放到
x0
这个寄存器中(arm64);Swift中则是在编译阶段,将对象存放在
x20
这个寄存器中(arm64)。每个方法调用都是通过读取方法在虚表中的索引获取到函数的真实地址进行调用。
这个虚表索引的值在编译时期就已经确定,因此不需要再通过方法名在运行时动态地区查找真实的函数地址来实现调用。
基类和派生类虚表中索引处的函数的地址可以不一致:当派生类重写了父类的某个方法时,会分别生成两个类的虚表,在相同索引位置保存不同的函数地址来实现多态的能力。
每个方法函数名字都和源代码中不一样了,原因在于编译链接时,系统对所有的方法名进行了重命名处理,这个过程成为命名修饰。
进行命名修饰的原因:
- 解决方法重载和运算符重载的问题:因为源代码中重载的方法函数名都一样,只是参数和返回值类型不一样,无法简单通过名字进行区分;
- Swift提供了命名空间的概念,可以支持不同模块之间存在同名的方法或函数。
命名修饰会带上模块名:
$s<模块名长度><模块名><类名长度><类名>C<方法名长度><方法名>yy<参数类型1>\*<参数类型2>_<参数类型N>F
Swift的类中成员变量的访问
- Swift中,每个实例对象开始部分是一个
isa
成员,指向类的描述信息; - 类中定义的属性或变量会根据定义的顺序一次排列在
isa
后面; - 系统会对每个成员变量生成
set
/get
两个函数并保存到虚函数表中,所有对对象成员变量的方法的调用都会转化为通过虚函数表来执行set
/get
对应的方法。
Swift结构体中的方法
- Swift的结构体的内存结构中并没有地方保存结构体的信息(不存在
isa
成员),所以结构体中的方法是不支持多态的; - 结构体中的所有方法调用都是在编译时硬编码来实现的,所以结构体不支持派生,也不支持
override
类方法及全局函数
- Swift中的类方法和全局函数一样,因为不存在对象作为参数,因此在调用类方法时不存在将对象保存到
x20
寄存器;源代码定义的函数的参数在编译时也不会插入附加的参数。 - 类方法和全局函数也会被重命名修饰,所以全局函数和类方法支持同名但参数不同的函数定义;
- 类方法和全局函数的调用都是在编译链接时刻硬编码为函数地址来调用的。
开启编译链接优化
主要变化:弱化了通过虚函数表来进行间接方法调用的实现,而是大量使用内联的方式来处理方法函数的调用。
函数实现换成内联函数模式:对象方法的调用不再通过虚函数表来间接调用,而是将函数的调用改为直接将内联函数生成的机器码进行拷贝处理。
优点:
- 由于没有函数调用的跳转指令,而是直接执行方法中定义的指令,极大地提高了程序的运行速度;
- 使程序更加安全:函数的实现逻辑散布各处,除非恶意修改所有指令,否则只会影响局部程序的运行
缺点:内联会使程序的体积增大很多
对多态的支持,不是通过虚函数来处理,而是通过类型判断,采用条件语句来实现方法的调用。
主要是考虑性能和包大小的优化:
通过间接调用的方式可能需要增加更多的指令以及进行间接的寻址处理和指令跳转;而简单的类型判断则只需要更少的指令就可以解决多态调用的问题。
对比:
- 采用编译链接优化:链接时如果发现一个函数没有被任何地方调用或者引用,链接器就会把这个函数的实现代码整个体删除;
- 采用虚函数表:需要把一个类的所有方法的函数都存放到类的虚函数表中,而不管类中的函数是否又被调用。通过虚函数表的形式间接调用时无法在编译连接时明确那个函数是否会被调用。
所以,采用虚函数表的形式可能会增加程序的体积。
关于系统编译器如何平衡将虚函数表间接调用转换为内联模式带来的程序体积增大,则不得而知。
OC和Swift中函数的混编调用
Swift权限修饰符
open
:可以在任何地方被访问、继承、重写public
:可以在任何地方被访问,在其他模块中不能被继承和重写internal
:默认权限,在整个模块内都可以被访问。fileprivate
:其修饰的属性可以在同一个文件被访问、继承和重写private
:其修饰的属性和方法只能在本类被访问和使用。
@objc , @objc(Type)和 @objcMembers
@objc
:用于当前类可以在OC中使用@objc(Type)
:可以给Swift类重命名, 可以在OC中通过Runtime
获取类@objcMembers
:用于当前类、子类、类扩展和子类扩展的所有属性和方法都加上@objc
Swift调用OC
- 需要为工程创建一个桥接文件:
项目名称-Bridging-Header.h
; - Swift中要调用的OC方法都需要在这个头文件中声明
OC调用Swift
- 由于Swift和OC的函数调用ABI规则不同,所以OC只能创建Swift中从NSObject类中派生的类;
- 方法的调用只能调用NSObject及其派生类中的所有方法,以及被声明为
@objc
关键字的Swift对象方法; - OC和Swift代码在不同模块中时,Swift中需要开放函数权限为
open
或public
; - 在OC中调用Swift的类和方法时,需要在OC中添加
#import “项目名-Swift.h”
;
Swift方法添加@objc
关键字时,在编译时会生成两个函数:
一个是本体函数:供Swift内部调用
一个是跳板函数(
trampoline
):供OC调用该函数信息会记录在OC的运行时类结构体中,跳板函数的实现会对参数的传递规则进行转换——把
x0
寄存器的值(self
)赋值给x20
寄存器,然后把其他参数按照Swift函数参数规则转化,最后再执行本体函数调用。源代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20/** Swift源代码 */
//Swift类定义
class MyUIView:UIView {
@objc
open func foo(){}
}
func main() {
let obj = MyUIView()
obj.foo()
}
/** OC源代码 */
#import "工程-Swift.h"
void main() {
MyUIView *obj = [MyUIView new];
[obj foo];
}伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47/** Swift类描述 */
struct swift_class {
...
struct method_t methods[1];
...
IMP vtable[1]; // 虚函数表
};
/** 类定义 */
struct MyUIView {
struct swift_class *isa;
}
/** 本体函数foo的实现 */
void foo(){}
/** 跳板函数的实现 */
void trampoline_foo(id self, SEL _cmd){
asm("mov x20, x0");
self->isa->vtable[0](); //这里调用本体函数foo
}
/** 类的描述信息构建,这些都是在编译代码时就明确了并且保存在数据段中。 */
struct swift_class classMyUIView;
classMyUIView.methods[0] = {"foo", &trampoline_foo};
classMyUIView.vtable[0] = {&foo};
/** Swift代码部分 */
void main()
{
MyUIView *obj = MyUIView.__allocating_init(classMyUIView);
obj->isa = &classMyUIView;
asm("mov x20, obj");
/** Swift中foo的调用是通过虚函数表间接调用 */
obj->isa->vtable[0]();
}
/** OC代码部分 */
void main()
{
MyUIView *obj = objc_msgSend(objc_msgSend(classMyUIView, "alloc"), "init");
obj->isa = &classMyUIView;
/** OC中对foo的调用还是用objc_msgSend来执行调用。 */
/** 因为objc_msgSend最终会找到methods中的方法结构并调用trampoline_foo, trampoline_foo内部会调用foo的本体 */
objc_msgSend(obj, (foo));
}