OC的Runtime机制使得它可以被认为是一种动态语言。
Swift则取消了Runtime这个能力,让Swift称为一门静态语言。Swift语言的对象方法调用基本在编译期间就被确定,可以看做是一种硬编码形式的调用实现。这种机制加快了程序的运行速度、减少程序包体积,但在编译连接优化功能开启时反而又会出现增大包提及的情况。Swift在编译连接期间采用的是空间换时间的优化策略,以提高运算行速度为主要优化考虑点。
OC类对象方法调用
OC对象调用方法都通过消息发送的objc_msgSend
完成,并至少传入调用对象和对象方法名称作为参数,方法根据对象找到类结构信息,通过方法名来找到最终调用的方法函数地址,并最终完成函数的调用。这是OC Runtime的实现机制,同时也是OC对多态的实现。
Swift类对象
Swift类对象按其基类可分为:
- 从NSObject及其子类派生的类。
- SwiftObject派生的类(SwiftObject是隐藏的类,不会在源代码中体现)。
Swift类对象内存布局和OC类内存布局相似。都是在最开始部分有一个isa指针,指向类的描述信息。Swift类的描述信息结构继承自OC类的描述信息,但没有完全使用其中的属性,对于方法的调用主要是使用其中扩展的序函数表的区域。
Swift对象实例都是在堆内存中创建,与OC一致。Swift类在实例化时,会生成堆内存分配和初始化函数,形式为:
1 | 模块名.类名.__allocating_init(类名,初始化参数) |
与OC一致,Swift类实例也是通过引用计数管理生命周期,所以也会在编译时插入swift_retain
、swift_release
函数,当前引用计数为0后,就调用生成的西沟和销毁函数,形式为:
1 | 模块名.类名.__deallocating_deinit(对象) |
Swift方法调用
Swift类定义的方法可分为:
- OC类派生类并重写的方法
- 扩展中定义的方法
- 类中定义的一般方法
对于这三种方法,系统采用的处理和调用机制是完全不一样的。
OC类派生类并重写的方法
这些方法还是与OC类原本方法的调用机制一致,即也是使用objc_msgSend
调用。
扩展中定义的方法
这里的扩展中定义的方法是不包含重写OC基类的方法。这种方法调用时在编译期就决定的,即在调用方法时,直接使用硬编码的函数地址。这也决定了扩展中的方法无法在运行时做替换和改变。
同时这类方法的符号信息不保存到类的描述信息中,也决定了派生类中不能重写扩展中的定义的方法,即不支持多态。
类中定义的一般方法
Swift在未开启编译链接优化时,对象的方法调用实现机制和C++的虚函数调用机制类似。Swift为每个类都建立一个虚函数表的数组结构,保存着所以定义的常规成员方法的地址。
在方法调用时,不再想使用objc_msgSend
调用传入调用对象和方法名,而是直接调用函数地址,而调用对象则存在x20寄存器中,让代码更加安全。
虽然类中方法在虚函数表中的索引值是在编译期确定的,但基类和派生类虚函数表中相同索引处的函数地址可以不一样,即当子类重写了父类某个方法时,会分别生成两个类的虚函数表,在相同的索引位置保存不同的函数地址实现多态。
另外,为了实现方法重载和运算符重载,函数名字会进行修饰重命名,规则如下:
1 | _$s<模块名长度><模块名><类名长度><类名>C<方法名长度><方法名>yy<参数类型1>_<参数类型2>_<参数类型N>F |
Swift的成员变量
OC类的成员变量根据定义顺序排在isa后面,另外还会生成一张变量偏移表,通过偏移来访问成员变量。
Swift简化了对成员变量的访问。直接在编译链接时确定成员变量在对象的偏移位置。且不生成变量编译信息表。
结构体
初始化器与属性默认值在汇编上是一样的。
Swift结构体与C结构体的内存结构是一样的,成员变量内存都是紧挨在一起。
Swift结构体内存结构中没有保存isa,即没有保存结构体信息,因此不支持多态与派生,同时结构体中的所有方法都是通过在编译期硬编码实现的。
类方法和全局函数
Swift类方法和全局函数不存在对象作为参数,即不需要把对象保存到x20寄存器中,所以它就是个简单的C语言普通函数,只是方法名需要进行修饰重命名,所有对类方法和全局函数的调用都是在编译期硬编码为函数地址来调用的。
开启编译链接优化后
开启编译链接优化后,Swift对象方法的调用机制做了很大的优化,最主要是弱化了通过虚函数表来间接调用方法的实现,而是大量改用一些内联的方式来处理方法函数的调用。
对于多态的支持,可能不是通过虚函数来处理,而是通过类型判断,然后用条件语句分支执行方法。
Swift其他特性底层
引用类型与值类型
值类型存在栈中,引用类型存在堆中。若值类型中包含引用类型,则存储的只是引用类型的指针。
延迟存储属性
多线程同时第一次访问lazy属性,是无法保证属性只被初始化一次。
当结构体包含一个延迟存储属性时,只有var
才能访问延迟存储属性。因为延迟属性初始化时要改变结构体的内存,而let修饰的结构体要求里面的所以成员都完成初始化,两者相悖。
延迟属性也不能添加属性观察器。
类存储属性
类存储属性默认就是lazy
修饰,且能有let
修饰成常量,会在第一次使用的时候才初始化。类存储属性时多线程安全的(系统底层会有加锁处理)。这其中调用了swift_once
函数,内部调用了dispatch_once
函数。
类存储属性实际上是全局变量。
输入输出参数
inout参数本质是地址传递(引用传递)。在汇编使用leaq
传递地址,而一般参数则是使用movq
传递值。
限制:
- 不能有默认值
- 需要时左值。
对于有属性观察器以及计算属性,在get/set输入输出参数时,也会触发相关的方法调用。
所以,如果实参有物理内存地址,且有设置属性观察器,则直接把实参的内存地址传入参数(实参进行引用传递)。
如果实参是计算属性或设置了属性观察器,则采取Copy In Copy Out的做法:在调用该函数时,先复制实参的值,产生副本(可以理解成getter操作),将副本的内存地址传入参数(副本进行引用传递),在函数内部修改副本的值,函数返回后,再将副本的值覆盖实参的值(可以理解为setter操作)。
函数重载
返回值不参与函数重载。
内联函数
开启编译器优化后,编译器会将某些函数转为内联函数,即把函数展开成函数体。但以下情况不会被自动内联:
- 函数体较长
- 包含递归调用的函数
- 包含动态派发的函数
闭包
闭包的定义:一个函数和它所捕获的变量/常量环境组合起来。
闭包函数会把捕获的变量存储到堆中动态申请的内存空间上。存储的信号包含类型描述信息、引用计数信息、具体值。每次闭包函数调用,都会动态申请一段新的堆内存在存放新的捕获变量。若没有捕获变量,则不回分配堆空间。
内存布局如下:
所以闭包的内存布局与实例对象很相似:共享方法、各自管理自己的成员变量。
空合并运算符??
默认值是个自动闭包,这可以在可以解包的情况下不调用默认值的逻辑。
1 | public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T? |