Swift底层原理学习笔记
笔记主要记录Swift和OC底层原理差异的地方,OC的底层原理之前的笔记有详细记录。
课程是逻辑教育的,视频基本只看了总结部分,然后结合网上已有笔记进行的重点梳理。
Swift 进阶一:类,对象,属性
类、对象
- Swift对象的内存结构
HeapObject
,有两个属性:一个是Metadata
,一个是Refcount
,默认占用16
字节大小,就是对象中没有任何东西也是16
字节。 - OC中实例对象的本质是结构体,是以objc_object为模板继承的,其中有一个isa指针,占
8
字节。 - Swift比OC中多了一个
refCounted
引用计数大小,也就是多了8字节。
-
getClassObject
函数是根据kind
获取object
类型 - 如果
kind
(理解为isa指针)是Class
类型,则将当前的Metadata
强转成ClassMetadata
,而ClassMetadata
是TargetClassMetadata
根据类型的别名,其中TargetClassMetadata
结构: -
TargetClassMetadata
继承自TargetAnyClassMetadata
- 结构与
OC
中的objc_class
的结构一样,有isa
,有父类,有cacheData
,Data
类似于objc_class
中的bits
- 根据上面分析我们可以得到结论:当
metadata
的kind
为Class
时,有如下的继承关系:
- 总结,
swift
的类内存结构可以理解为:
struct Metadata {
void *kind; // 类型标识(如类、结构体、枚举)
void *superClass; // 父类的 Metadata 指针
void *cacheData; // 方法缓存(类似 OC 的 cache_t)
void *data; // 指向额外数据的指针
// 实例布局信息
uint32_t flags; // 类型标志位
uint32_t instanceAddressOffset; // 实例变量的起始偏移量
uint32_t instanceSize; // 实例对象占用的内存大小
uint16_t instanceAlignMask; // 实例的对齐掩码
uint16_t reserved; // 保留字段
// 类布局信息
uint32_t classSize; // 类对象占用的内存大小
uint32_t classAddressOffset; // 类变量的起始偏移量
void *description; // 类型描述信息
// 类对象与元类对象的关键区别
uint32_t flags; // 包含类型标志位(如是否为元类)
void *vtable; // 类对象的 vtable 指向实例方法表
void *classVtable; // 元类对象的 vtable 指向类方法表
// 方法签名表(所有方法),存放在类对象中
MethodDescriptor* methodDescriptors;
uint32_t methodCount;
}
// 例子:
class MyClass {
func instanceMethod() {} // 实例方法
static func classMethod() {} // 类方法
}
// 实例方法调用(通过类对象的 vtable)
let obj = MyClass() obj.instanceMethod() // 类对象 → Metadata → vtable → 方法实现
// 类方法调用(通过元类对象的 classVtable)
MyClass.classMethod() // 类对象 → 元类对象 → Metadata → classVtable → 方法实现
属性
-
- 存储属性:有常量存储属性和变量存储属性两种,都占用内存
-
- 计算属性:不占用内存,本质为函数。
-
- 属性观察者:
-
- 属性观察可以添加在
类的存储属性
、继承的存储属性
、继承的计算属性
中
- 属性观察可以添加在
-
- 父类在调用
init
中改变属性值不会触发
属性观察,子类调用父类的init
会触发
属性观察
- 父类在调用
-
- 统一属性在父类和子类都添加观察,在触发观察时:
-
willSet
方法,先子类后父类 -
didSet
方法,先父类后子类
-
-
延迟属性(lazy) :延迟属性必须有初始(可以为nil),只有在
访问后
内存中才有值,延迟属性对内存有影响,不能保证线程安全
-
延迟属性(lazy) :延迟属性必须有初始(可以为nil),只有在
-
-
类型属性:类型属性必须有初始值,内存只分配一次,通过
swift_once
函数创建,类似dispatch_once
,是线程安全的。
- 可以用于单例:
class XXX { static let share: XXX = XXX() private init(){} }
-
Swift 进阶二:值类型、引用类型、结构体
结构体,值类型
struct WSPerson {
var age: Int = 18
}
struct WSTeacher {
var age: Int
}
- 结构体会自动创建为所有参数赋值的构造函数。
- 结构体开辟的内存在
栈区
。 - 结构体的赋值是
深拷贝
,并且有写时复制
的机制。
结构体的属性修改问题
- 结构体对象
self
类型为let
,即不可以被修改。 - 结构体中
函数修改属性
, 需要在函数前添加mutating
关键字,本质是给函数的默认参数self
添加了inout
关键字,将self
从let
常量改成了var
变量。 -
mutating
方法修改结构体属性时,采用的是 "in-place" 的方式,也就是直接在当前实例的内存空间里修改属性值,并没有重新创建一个新的实例来替换原来的实例。这一特性和赋值操作有着本质的区别。
结构体的函数调用
-
值类型对象的函数的调用方式是
静态调用
,即直接地址调用
,调用函数指针,这个函数指针在编译、链接完成后就已经确定了
,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用
这个符号哪里来的?
-
是从
Mach-O
文件中的符号表Symbol Tables
,但是符号表中并不存储字符串
,字符串存储在String Table
(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名
方法重载问题
- 在
Objective-C
里,方法重载是不被支持的,不过Swift
却支持,这主要是由它们不同的函数签名机制和语言设计理念造成的。
- 函数签名机制
-
Objective - C:它的函数签名
只依据方法名
,和参数类型没有关系。 比如下面这两个方法,在OC看来是一样的,所以无法共存:
- (void)doSomethingWithInt:(int)value;
- (void)doSomethingWithInt:(NSString *)value;
-
Swift:它的函数签名是
由方法名和参数类型
共同组成的。 下面这样的重载在Swift中是被允许的:
func doSomething(value: Int)
func doSomething(value: String)
2. 消息传递机制
-
OC:采用的是运行时消息传递机制,方法调用是通过字符串(SEL)来实现的。 像
[obj doSomethingWithInt:1]
这样的调用,在运行时会被解析为SEL @selector(doSomethingWithInt:)
,要是有多个同名方法,就会引发冲突。 - Swift:使用的是静态 dispatch 机制,在编译时就会确定具体要调用哪个方法。
- 补充说明
- Swift 的重载:除了参数类型不同可以重载外,参数数量不同或者参数标签不同也能实现重载。
-
OC 的替代方案:在OC中,如果要实现类似功能,通常会采用命名约定,例如
doSomethingWithInt:
和doSomethingWithString:
。
总结来说,Swift支持方法重载是其类型系统和编译时检查机制的自然结果,而OC不支持则是受限于其动态特性和历史设计。
Swift 进阶三:内存分区、方法调度、指针
方法调度
- Swift 类的方法(非
final
、非static
、非@objc
修饰的)会被存放在一个名为 vtable 的表中。 - 只有类能够使用 vtable,结构体和枚举由于不支持继承,所以没有 vtable
内存布局示例:
[实例对象内存]
├ isa 指针 ───→ [类对象]
├ Metadata 指针 ───→ [Metadata]
│ └ vtable 指针 ───→ [vtable 内存区域]
│ ├ 0: init()
│ ├ 1: method1()
│ └ 2: method2()
└ 其他类数据...
-
方法调用时的流程,当调用一个类的实例方法时,Swift 运行时会:
- 通过实例的
isa
指针找到类对象。 - 从类对象中获取
Metadata
指针。 - 从 Metadata 中读取
vtable
指针。 - 根据方法在
vtable
中的索引,调用对应的函数实现。
- 通过实例的
- vtable 仅存储
可重写的方法
,而类的所有方法(包括不可重写的)仍通过元数据(Metadata)管理。 - 元类对象(Metaclass Object)的 Metadata 主要存储类方法(
static
/class
方法)的实现信息。 - 协议方法的签名和实现由
Witness Table
管理,与类对象 / 元类对象的Metadata
是分离的。
方法调用总结
-
struct
是值类型
,它的函数调度是直接调用
,即静态调度
- 值类型在函数中如果要修改实例变量的值,则函数前面需要添加
Mutating
修饰
- 值类型在函数中如果要修改实例变量的值,则函数前面需要添加
-
class
是引用类型
,它的函数调度是通过vtable函数
,即动态调度
-
extension
中的函数是直接调用
,即静态调度
-
final
修饰的函数是直接调用
,即静态调度
-
@objc
修饰的函数是methodList函数表调度
,如果方法需要在OC
中使用,则类需要继承NSObject
-
dynamic
修饰的函数调度方式是methodList函数表调度
,它是动态可以修改的,可以进行method-swizzling
-
@objc+dynami
修饰的函数是通过objc_msgSend
来调用的
-
- 如果函数中的
参数想要被更改
,则需要在参数的类型前面增加inout
关键字,调用时需要传入参数的地址
Swift 进阶四:弱引用、闭包、元类型
Swift 内存管理
- swift实例对象的内存中,存在一个
Metadata
,一个Refcount
。后者记录引用计数。 -
Refcount
最终可以获得64位整型数组bits
,其结构:
// 简化的 Refcount 结构(实际实现可能更复杂)
struct Refcount {
// 64 位中的高 32 位:强引用计数
uint32_t strongRefCount: 32;
// 64 位中的低 32 位:
uint32_t hasWeakRefs: 1; // 是否有弱引用
uint32_t hasUnownedRefs: 1; // 是否有 unowned 引用
uint32_t isDeiniting: 1; // 是否正在析构
uint32_t sideTableMask: 1; // 是否使用 Side Table
uint32_t weakRefCount: 28; // 弱引用计数
};
- 当引用计数超出直接存储范围时,通过
sideTableMask
标志切换到全局 Side Table 存储。 -
Swift
在创建实例对象时的默认引用计数是1
,而OC
在alloc
创建对象时是没有引用计数的。
弱引用
- 为对象增加弱引用时,实际是调用
refCounts.formWeakReference
,即去操作sideTable表,添加对象的弱引用关系,这里和OC处理是一致的。
swift中的runtime
- 对于纯swift类来说,没有动态特性
dynamic
(因为swift是静态语言),方法和属性不加任何修饰符的情况下,已经不具备runtime特性,此时的方法调度,依旧是函数表调度即V_Table调度。 - 对于纯swift类,方法和属性添加
@objc
标识的情况下,可以通过runtime API获取到,但是在OC中是无法进行调度的,原因是因为swift.h文件中没有swift类的声明。 - 对于继承自
NSObject
类来说,如果想要动态的获取当前属性+方法,必须在其声明前添加@objc
关键字,如果想要使用方法交换,还必须在属性+方法前添加dynamic关键字,否则当前属性+方法只是暴露给OC使用,而不具备任何动态特性。
补充
- Any:任意类型,包括
function
类型、optional
类型 - AnyObject:任意类的
instance
、类的类型、仅类遵守的协议,可以看作是Any的子类 - AnyClass:任意实例类型,类型是
AnyObject.Type
- T.self:如果T是实例对象,则表示它本身,如果是类,则表示
metadata.T.self
的类型是T.Type