最近学习了Swift底层原理相关的视频和文章,收获颇丰,趁热打铁,记录和总结对Swift的理解,对于Swift性能优化主要从下面三个方面入手
- 内存分配
- 引用计数
- 方法派发方式
内存分配
在程序运行过程中,我们控制的内存主要有两个下面两个区域(DATA段也能修改,但对性能影响不大)
- 栈(Stack):由操作系统管理,通常用来执行函数,存放局部变量和临时变量
- 对于在栈上分配内存和释放只是堆栈指针的移动(入栈和出栈),并且不需要增加额外的数据
- 堆(Heap): 由开发者自行管理内存的生命周期,通常用于存放类对象
- 对于在堆上分配内存,需要更
高级的数据结构
- 申请内存的时候需要
搜索堆空间
,寻找合适的闲置内存块 - 需要添加额外的数据用于管理内存(如
引用计数
) - 对于引用计数的操作需要具备
原子性
(线程安全) - 堆上的内存还存在
线程安全
的问题
- 对于在堆上分配内存,需要更
Swift 中的数据类型可以分成两种:值类型
(Struct, Enum)、引用类型
(Class)。两者的内存分配区域是不同的,值类型默认分配在栈区,引用类型默认分配在堆区
栈分配
堆分配
优化
在值类型和引用类型的选择上,应该更多使用值类型,对于调用频繁的方法,应该减少在堆创建对象,如下
1 | enum Color { case blue, green, gray } |
上面key
由于是动态创建的,会被分配到堆上,考虑用结构体包装,可以避免频繁在堆创建对象
1 | struct Attribute: Hashable { |
小结
对于需要频繁分配内存的需求,应尽量使用 Struct
/Enum
代替 Class
。因为栈区的内存分配速度更快,更安全。
引用计数
上面例子可以看到,class Point在堆分配时候,会额外分配两个字段,第一个是函数表,用来实现多态,另一个就是引用计数
,用于内存管理,上面的Point类可以看成下面代码
1 | class Point { |
- 引用计数是间接的管理内存,当引用计数为0时,Swift会将对应的内存释放
- 引用计数的操作是高频率的
- 引用计数的操作具备原子性(考虑线程安全),会带来一定的开销
虽然栈上的内存分配会比堆上块,但是有时候,使用栈会增加引用计数的操作(栈上的结构体使用了类对象,类对象在堆上分配),从而影响性能,如下
1 | struct Label { |
上面可以看到,每次label拷贝的时候,都会带来所有引用变量retain(上面例子是2个,如果多的话影响会更大),可以考虑改成class
1 | class Label { |
再看下面一个例子
1 | struct Attachment { |
同样是struct包含多个class,优化代码如下,把引用类型改成值类型,提高性能同时,语义更明确
1 | struct Attachment { |
小结
如果结构体中包含多个引用对象,在结构体传递的过程中,会对引用对象进行retain/release
- 可以考虑把
引用类型
转换成值类型
(枚举/结构体),减少struct中的class数量 - 也可以考虑把struct改成class来提高性能
当然也要根据具体场景判断是否要进行优化
派发方式
Swift的函数派发有
直接派发
- 全局方法
- 使用
static
和final
修饰的类和方法 - 使用
private
修饰的属性和方法会隐式添加final
值类型
(struct, enum)的方法extension
里面没有用@objc
修饰的方法
函数表派发
- 使用protocol调用的方法
- class的实例方法
- 使用
消息派发
- class中使用
dynamic
修饰的方法 - 继承自OC对象的方法
- class中使用
性能:直接派发 > 函数表派发 > 消息派发
除了上面派发方式,Swift会根据情况对小函数进行Inline
优化
1 | func drawAPoint(_ param: Point) { |
会被优化成
1 | let point = Point(x: 0, y: 0) |
Witness Table
我们知道,结构体也能实现协议,对于实现相同协议的不同的结构体,放到同一个数组中,内存是怎么分布的
1 | protocol Drawable { func draw() } |
数组的内存是连续的,而结构体又存放在栈中,并且结构体的大小可能不一样,这不是矛盾了吗
显然不可能像上面一样存储,在Swift中提供了一个叫The Existential Container
的容器用来包装Protocol类型,该容器有5个字节
,该容器结构如下
- valueBuffer: 占用3个字节
- vwt: 占用1个字节,存放The Value Witness Table (VWT),用于访问vwt,例如释放内存
Swfit会为每个实现了Protocol的结构体,实现下面方法,用于把包装并管理数据的声明周期allocate:
: 由于valueBuffer只有3个字节,当结构体数据超过3个字节时,就需要在堆上申请内存,allocate
用于申请内存copy:
: 把结构体的数据拷贝到valueBuffer中,或把堆空间的地址复制到valueBuffer第一个字节destruct:
: 用于销毁数据deallocate:
: 回收内存
- pwt: 占用1个字节,存放The Protocol Witness Table(PWT),方法表
protocol方法表,每一个实现protocol的结构体都有一个pwt表,在运行时通过pwt找到实例的方法
The Existential Container
对应的结构体如下
1 | struct ExistContDrawable { |
我们看下一下面代码
1 | func drawACopy(_ local: Drawable) { |
Swift会把drawACopy
方法改成下面形式
1 | func drawACopy(_ val: ExistContDrawable) { |
总结:
vwt
: 解决结构体内存空间不一致问题pwt
: 解决动态派发的问题(多态)
Protocol属性
1 | struct Pair { |
pair的内存布局如下
在Swift使用Protocol的时候,很多时候都是使用The Existential Container
Protocol 泛型
小结
出于性能的考虑,我们尽量
- 使用
final
来修饰不会被重载的方法,如果class不会被重载,可以设置为final - 使用
private
来修饰不会被外部访问到的属性和方法 - 从而提高函数的派发性能
引用
未完待续~