Concept brief
data structure
Swift's data structure can be roughly divided into: Class
, Struct
, Enum
.
Memory Allocation
Each process has an independent process space. The areas in the process space that can be used for memory allocation are mainly divided into two types:
- Stack area (Stack)
- Heap _
- object memory allocation
SIL
Here we need to use the intermediate language ( Swift Intermediate Language ) generated during the swift compilation process
The Swift programming language is built on LLVM and uses LLVM IR and the LLVM backend to generate code. But the Swift compiler also includes a new high-level intermediate language called
SIL
.SIL
Higher-level semantic analysis and optimizations are performed on Swift.
//main.swift
class Person {
var name: String?
func doSometing() { }
}
let person: Person = Person()
person.doSometing()
let type: Person.Type = Person.self
复制代码
Orderswiftc -emit-sil
class_method indicates that the dynamic dispatch of class methods is implemented through sil_vtable.
viewController.swift
class Person {
var name: String?
func doSometing() { }
}
class ViewController: UIViewController {
var person: Person? //在堆区分配内存
override func viewDidLoad() {
super.viewDidLoad()
person = Person()
person?.doSometing()
}
}
复制代码
Type Metadata
The Swift runtime keeps a metadata record for every type used in a program, including every instantiation of generic types.
The Swift runtime maintains a metadata record for every type used in a program, including every instantiation of a generic type.
In-memory storage structure
The first 8 bytes of the object point to the type information, that is, to the metatype. So these 8 bytes are a metatype pointer.
method dispatch method
一个方法会在运行时被调用或者是一个方法被唤起,是因为编译器有一个计算机制,用来选择正确的方法,然后通过传递参数来唤起它,这个机制通常被成为分派(dispatch
),分派就是处理方法调用的过程。方法从书写完成到调用完成,概括上会经历编译期和运行期两个阶段,确定哪个方法被执行,也是在这两个时期进行的。故选择正确方法的阶段,可以分为编译期和运行期,而分派机制通过这两个不同的时期分为两种:
- 静态分派(
static dispatch
) - 动态分派(
dynamic dispatch
)
能够在编译期确定执行方法的方式叫做静态分派,无法在编译期确定,只能在运行时去确定执行方法的分派方式叫做动态分派。
Static dispatch
更快,而且静态分派可以进行内联等进一步的优化,使得执行更快速,性能更高。
但是对于多态的情况,我们不能在编译期确定最终的类型,这里就用到了Dynamic dispatch
动态分派。动态分派的实现是,每种类型都会创建一张表,表内是一个包含了方法指针的数组。动态分派更灵活,但是因为有查表和跳转的操作,并且因为很多特点对于编译器来说并不明确,所以相当于block了编译器的一些后期优化。所以速度慢于Static dispatch
。
组件关系
在swift中组件关系可以分为inheritance
,protocols
,generics
。
- inheritance 继承
- protocols 协议
- generics 泛型
多态(特性)
指允许不同对象对同一消息做出响应。即同一消息可以根据发送对象的不同而采用多种不同的行为方式。
Swift 协议与泛型的实现
问题:
- Protocol Type 和 Generic Type 如何实现存储?
- Protocol Type 和 Generic Type 如何拷贝变量?
- Protocol Type 和 Generic Type 如何进行方法派发?
协议类型 Protocol Type
例子,这里是通过Protocol Type
实现多态,几个类之间没有继承关系
protocol Person {
func doSometing()
}
struct Person1: Person {
var x, y: Double
func doSometing() {}
}
struct Person2: Person {
var x1, y1, x2, y2: Double
func doSometing() {}
}
//遵守了Person协议的类型集合,可能是Person1或者Person2
var persons: [Person] = [Person1(x: 1.0, y: 1.0), Person2(x1: 1.0, y1: 1.0, x2: 1.0, y2: 1.0)]
for p in persons {
p.doSometing()
}
复制代码
init_existential_addr %x 初始化由%x 引用的内存为Existential Container。
可以发现在这种情况下,变量 persons 中存储的元素是一种特殊的数据类型:Existential Container。
Existential Container
Existential Container 是编译器生成的一种特殊的数据类型,用于管理遵守了相同协议的协议类型。因为这些数据类型的内存空间尺寸不同,使用 Extential Container 进行管理可以实现存储一致性。
在这里我们可以使用Memlayout这个API去验证一下(本机内存对齐是 8 字节)
可见 Existential Container 类型占用了 5 个内存单元(也称 词,Word)。其结构如下图所示:
- 三个词作为
Value Buffer
。 - 一个词作为
Value Witness Table
的索引。 - 一个词作为
Protocol Witness Table
的索引。
Value Buffer
Value Buffer 占据 3 个词,存储的可能是值,也可能是指针。对于 Small Value(存储空间小于等于 Value Buffer),可以直接内联存储在 Value Buffer 中。对于 Large Value(存储空间大于 Value Buffer),则会在堆区分配内存进行存储,Value Buffer 只存储对应的指针。
Value Witness Table
由于协议类型的具体类型不同,其内存布局也不同,Value Witness Table 则是对协议类型的生命周期进行专项管理,从而处理具体类型的初始化、拷贝、销毁。
Protocol Witness Table
Value Witness Table 管理协议类型的生命周期,Protocol Witness Table 则管理协议类型的方法调用。
在 OOP 中,基于继承关系的多态是通过 Virtual Table 实现的;在 POP 中,没有继承关系,因为无法使用 Virtual Table 实现基于协议的多态,取而代之的是 Protocol Witness Table。
拷贝变量
上面的描述中我们可以了解到
- 对于 Small Value,直接内联存储在 Existential Container 的 Value Buffer 中。
- 对于 Large Value,通过堆区分配进行存储,使用 Existential Containter 的 Value Buffer 进行索引。
Indirect Storage With Copy-On-Write
原理:优先使用内存指针,拷贝时仅仅拷贝 Existential Container,当修改值时,先检测引用计数,如果引用计数大于 1,则开辟新的堆区内存。
泛型类型 Generic Type
由Protocol Type实现的多态是动态的多态(Dynamic Polymorphism),那什么是静态多态呢
例子
protocol Person {
func doSometing()
}
struct Person1: Person {
var x, y: Double
func doSometing() {}
}
struct Person2: Person {
var x1, y1, x2, y2: Double
func doSometing() {}
}
func todo(person: Person) {
person.doSometing()
}
let person1 = Person1(x: 1.0, y: 1.0)
todo(person: person1)
复制代码
这个时候通过SIL文件我们可以看到这里还是使用Existential Container数据结构,但是上面我们说得使用Existential Container是为了管理遵守了相同协议的协议类型以及内存对齐,从上面的代码上看这个时候编译器是知道传进去的是Person1类型的对象,故这个时候我们可以使用泛型Generic code
来实现。
func todo<T: Person>(person: T) {
person.doSometing()
}
复制代码
故我们可以根据方法调用时数据类型是否确定可以将多态分为:静态多态(Static Polymorphism)和 动态多态(Dynamic Polymorphism)。
泛型和Protocol Type
的区别在于:
- 泛型支持的是静态多态。
- 每个调用上下文只有一种类型。
- 在调用链中会通过类型降级进行类型取代。
Value Buffer
Value Buffer 占据 3 个词,存储的可能是值,也可能是指针。对于 Small Value(存储空间小于等于 Value Buffer),可以直接内联存储在 Value Buffer 中。对于 Large Value(存储空间大于 Value Buffer),则会在堆区分配内存进行存储,Value Buffer 只存储对应的指针。
Value Witness Table
由于协议类型的具体类型不同,其内存布局也不同,Value Witness Table 则是对协议类型的生命周期进行专项管理,从而处理具体类型的初始化、拷贝、销毁。
Protocol Witness Table
Value Witness Table 管理协议类型的生命周期,Protocol Witness Table 则管理协议类型的方法调用。
在 OOP 中,基于继承关系的多态是通过 Virtual Table 实现的;在 POP 中,没有继承关系,因为无法使用 Virtual Table 实现基于协议的多态,取而代之的是 Protocol Witness Table。
拷贝变量
上面的描述中我们可以了解到
- 对于 Small Value,直接内联存储在 Existential Container 的 Value Buffer 中。
- 对于 Large Value,通过堆区分配进行存储,使用 Existential Containter 的 Value Buffer 进行索引。
Indirect Storage With Copy-On-Write
原理:优先使用内存指针,拷贝时仅仅拷贝 Existential Container,当修改值时,先检测引用计数,如果引用计数大于 1,则开辟新的堆区内存。
泛型类型 Generic Type
由Protocol Type实现的多态是动态的多态(Dynamic Polymorphism),那什么是静态多态呢
例子
protocol Person {
func doSometing()
}
struct Person1: Person {
var x, y: Double
func doSometing() {}
}
struct Person2: Person {
var x1, y1, x2, y2: Double
func doSometing() {}
}
func todo(person: Person) {
person.doSometing()
}
let person1 = Person1(x: 1.0, y: 1.0)
todo(person: person1)
复制代码
这个时候通过SIL文件我们可以看到这里还是使用Existential Container数据结构,但是上面我们说得使用Existential Container是为了管理遵守了相同协议的协议类型以及内存对齐,从上面的代码上看这个时候编译器是知道传进去的是Person1类型的对象,故这个时候我们可以使用泛型Generic code
来实现。
func todo<T: Person>(person: T) {
person.doSometing()
}
复制代码
故我们可以根据方法调用时数据类型是否确定可以将多态分为:静态多态(Static Polymorphism)和 动态多态(Dynamic Polymorphism)。
泛型和Protocol Type
的区别在于:
- 泛型支持的是静态多态。
- 每个调用上下文只有一种类型。
- 在调用链中会通过类型降级进行类型取代。
方法调用
在方法执行时,Swift将泛型T绑定为调用方使用的具体类型
todo(person: person1) -->todo<T = Person1>(person: person1)
泛型方法调用的具体实现为:
- 同一种类型的任何实例,都共享同样的实现,即使用同一个Protocol Witness Table。
使用Protocol/Value Witness Table。
- 每个调用上下文只有一种类型:这里没有使用
Existential Container
, 而是将Protocol/Value Witness Table
作为调用方的额外参数进行传递。 - Variable initialization and method invocation are both performed using the passed in
VWT
sumPWT
.
generic specialization
After the type is downgraded, a method of a specific type is generated, and a corresponding method is created for each type of the generic type to perform specific optimization under static polymorphism . Virtual function calls are replaced with calling function maps.
E.g
func todo<T: Person>(person: T) {
person.doSometing()
}
todo(person: person1)
转换成
func todo<Person1>(person: Person1) {
person.doSometing()
}
复制代码
Because it is static polymorphism. So powerful optimizations can be made, such as inlining the implementation, and further optimizations can be made by getting the context. Thereby reducing the number of methods. It can be more precise and specific after optimization.
References
www.rightpoint.com/rplabs/swit…
airspeedvelocity.net/2015/03/26/…
developer.apple.com/documentati…
We are the Byte Shenzhen Feishu team, dedicated to creating high-performance, excellent experience enterprise collaborative office software. Interested children's shoes can submit their resumes to [email protected] . iOS Jobs: https://job.toutiao.com/s/Nj3oTKV Android Jobs: https://job.toutiao.com/s/NjTyGkf