苹果自2014年推出Swift
后,已经经过了多个版本的迭代,Swift
运行更快,效率更高,集成了很多高级语言语法特性,本篇通过案例的方式首先来了解一下类和结构体的异同点以及类的初始化,通过源码来分析一下类的生命周期。
初识类与结构体
先看一下类和结构体的定义:
# 结构体的定义只需把`class`关键字换成`struct`即可
class ATTeacher {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
相同点
- 定义存储值的属性
- 定义方法
- 定义下标以使用下标语法提供对其值的访问
- 定义初始化器
- 使用
extension
来拓展功能 - 遵循协议来提供某种功能
不同点
- 类有继承的特性,而结构体没有
- 类型转换使你能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源
- 引用计数允许对一个类实例有多个引用
图例分析
类(class)
类是引用类型。也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用
给变量t赋值给t1后,如下图:
以上是当把类实例t赋值给t1时,他们是引用的同一个内存地址。
结构体(struct)
结构体struct是值类型,结构体的定义也非常简单,相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)
struct ATStudent {
var age: Int
var name: String
}
var s = ATStudent(age: 18, name: Atom)
var s1 = s
其实引用类型就相当于在线的
Excel
,当我们把这个链接共享给别人的时候,别人的修改我们是能够看到的;值类型就相当于本地的Excel
,当我们把本地的Excel
传递给别人的时候,就相当于重新复制了一份给别人,至于他们对于内容的修改我们是无法感知的。
代码调试
类(class)
通过上面的定义分析,得出class
是引用类型,而struct
是值类型,下面通过实际代码来验证一下分析的正确性。定义的class
代码和上面一致,然后断点运行:
初始化
ATTeacher
赋值给t,然后把t
赋值给t1
,通过打印输出得出t
和t1
的内存地址是一样的,再改变实例t
中age
的属性值,输出t1
中age
的值,看看结果。
结果发现
t1.age
也等于20
,这就验证了类是引用类型,他们指向了同一片内存地址。
结构体(struct)
同样,把上面的类改成结构体,再次运行:
发现输出的都是值,在修改
s1.age
之前,s
和s1
的值都一致,当修改了s1.age
后,s1
的值更新了,但s
的值还是之前的,这也验证了struct
是值类型。
内存区域概念图
引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在栈上,引用类型存储在堆上。首先看一下常见的内存区域分布图:
还是通过代码来验证一下,定义一个函数,代码如下:
func test() {
var age: Int = 10
print(age)
}
test()
在print
打个断点,然后输出age
的地址,这里用到了工具去查看变量age
的在内存中的存储区域。
内存区域的存储信息
- 栈区(stack):局部变量和函数运行过程中的上下文
- Heap: 存储所有对象
- Global: 存储全局变量;常量;代码区
- Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每个段又分为很多小的 Section
- TEXT.text : 机器码
- TEXT.cstring : 硬编码的字符串
- TEXT.const: 初始化过的常量
- DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
- DATA.bss: 没有初始化的(静态/全局)变量
- DATA.common: 没有初始化过的符号声明
类和结构体在内存中的分布
通过frame variable
命令查看结构体中变量在内存中的分布情况,还是利用之前的代码
# 可以通过help查看具体的命令文档
help frame variable
断点调试输出
age
就是结构体的首地址,name
在age
增加8
个字节。因此当前结构体的在内存中的示意图如下所示:
当我们初始化
ATTeacher
的时候就会在栈上申请24字节大小的内存空间,age
和name
在内存中的地址是连续的,当函数执行完成后系统会销毁之前申请的栈内存空间,栈指针还原指向位置。如果其他条件不变,我们在struct
中增加一个class
实例,这样会不会影响结构体内存分布呢?
可以看到增加了类是不会对原有的
struct
内存造成影响的,ATPerson
会先在堆上申请内存空间存放在栈上的变量p
中。同样我们把struct
改成class
再运行。
可以看出
class
申请的堆内存空间。
类的初始化
当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)。
struct ATTeacher {
var age: Int
var name: String
}
指定初始化器&便捷初始化器
Swift
中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以类ATPerson
必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初始化器(注意:便捷初始化器必须从相同的类里调用另一个初始化器。)
class ATPerson{
var age: Int
var name: String
# 指定初始化器
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
# 便捷初始化器
convenience init() {
self.init(18, "Atom")
}
}
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。
示例
下面定义一个子类的初始化器,结合了上面的第1和第2条。不然编译会报错
可失败初始化器
这个也非常好理解,也就意味着当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。这种Swift
中可失败初始化器写return nil
语句,来表明可失败初始化器在何种情况下会触发初始化失败。写法也非常简单:
class ATAdult {
var age: Int
var name: String
init?(age: Int, name: String) {
if age < 18 return nil
self.age = age
self.name = name
}
}
必要初始化器
在类的初始化器前添加require
修饰符来表明所有该类的子类都必须实现该初始化器
如果子类没有提供就编译报错。
类的生命周期
编译流程图
iOS
开发的语言不管是OC
还是Swift
后端都是通过LLVM
进行编译的,如下图所示:
OC
通过clang
编译器,编译成IR
,然后再生成可执行文件.o
(这里也就是我们的机器码),Swift
则是通过Swift
编译器编译成IR
,然后在生成可执行文件。
Swift
编译过程:
Swift
代码通过dump parse
进行语法分析,解析成抽象语法树- 通过语义分析生成
Swift
中间代码SIL
(SIL包括原生的和优化过的) - 由
LLVM
生成IR
- 再由后端代码编译成机器码(x86、arm64...)
Swift编译命令
# 分析输出AST
swiftc main.swift -dump-parse
# 分析并且检查类型输出AST
swiftc main.swift -dump-ast
# 生成中间体语言(SIL),未优化
swiftc main.swift -emit-silgen
# 生成中间体语言(SIL),优化后的
swiftc main.swift -emit-sil
# 生成LLVM中间体语言 (.ll文件)
swiftc main.swift -emit-ir
# 生成LLVM中间体语言 (.bc文件)
swiftc main.swift -emit-bc
# 生成汇编
swiftc main.swift -emit-assembly
# 编译生成可执行.out文件
swiftc -o main.o main.swift
SIL代码分析
首先定义一个简单的Swift
类,然后根据上面的命令来生成中间代码SIL
,Swift
代码如下:
class ATTeacher {
var age: Int = 18
var name: String = "Atom"
}
var t = ATTeacher()
生成中间代码SIL
命令:
# 生成SIL并保存在main.sil文件中
swiftc -emit-sil main.swift > ./main.sil
打开生成的main.sil
文件,截取部分代码
class ATTeacher {
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
// main 入口函数
// 0%~9%:可以理解为SIL的寄存器(虚拟的),最终跑在设备上会映射到真实的寄存器上
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
// alloc_global: 分配一个全局变量
// s4main1tAA9ATTeacherCvp:混写后的名称,可以通过xcrun swift-demangle s4main1tAA9ATTeacherCvp还原
alloc_global @$s4main1tAA9ATTeacherCvp // id: %2
// 拿到全局变量的地址给到3%
%3 = global_addr @$s4main1tAA9ATTeacherCvp : $*ATTeacher // user: %7
%4 = metatype $@thick ATTeacher.Type // user: %6
// function_ref ATTeacher.__allocating_init()
// 拿到__allocating_init()函数的指针地址
%5 = function_ref @$s4main9ATTeacherCACycfC : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher // user: %6
// 使用5%的函数指针,参数是metatype,把结果返回给6%
%6 = apply %5(%4) : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher // user: %7
// 把6%存储在3%(也就是将实例变量的地址存储在全局变量里)
store %6 to %3 : $*ATTeacher // id: %7
// 创建一个Int32为0的值然后返回,就是main函数的return 0
%8 = integer_literal $Builtin.Int32, 0 // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32) // user: %10
return %9 : $Int32 // id: %10
} // end sil function 'main'
有关Swift SIL
详细说明可以参考官方文档。
从上面的main
函数里有个函数指针function_ref
对应的s4main9ATTeacherCACycfC
,在SIL
文件搜索一下可以定位到以下函数:
// ATTeacher.__allocating_init()
// ATTeacher.Type:可以理解为isa
sil hidden [exact_self_class] @$s4main9ATTeacherCACycfC : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher {
// %0 "$metatype"
bb0(%0 : $@thick ATTeacher.Type):
%1 = alloc_ref $ATTeacher // user: %3
// function_ref ATTeacher.init()
%2 = function_ref @$s4main9ATTeacherCACycfc : $@convention(method) (@owned ATTeacher) -> @owned ATTeacher // user: %3
%3 = apply %2(%1) : $@convention(method) (@owned ATTeacher) -> @owned ATTeacher // user: %4
return %3 : $ATTeacher // id: %4
} // end sil function '$s4main9ATTeacherCACycfC'
查看alloc_ref
官方的文档的解释
alloc_ref
其实就是从堆区申请内存空间,如果标识为objc
的就会以Objective-C
的初始化方法。
示例分析
在初始化ATTeacher
的时候打个断点,debug
以汇编的模式打开,可以看到初始化是swift_allocObject
再把
ATTeacher
集成自NSObject
再次运行,可以看到初始化方式通过Objective-C
的方式。
class ATTeacher: NSObject {
var age: Int = 18
var name: String = "Atom"
}
var t = ATTeacher()
Swift源码分析
通过上面的SIL
代码以及结合实例断点运行,在Swift
初始化函数的过程中我们看到了swift_allocObject
,接下来下载Swift
的源码,通过全局搜索可以定位到HeapObject.cpp
文件中有下面的方法:
swift_allocObject
调用了_swift_allocObject_
,_swift_allocObject_
又调用了swift_slowAlloc
,再全局搜索swift_slowAlloc
可以看到它的函数定义:
在这里就可以看到调用了
malloc
,基本上和Objective-C
的初始化类似,最终也会调用malloc
来开辟内存空间。
从上面的分析可以做以下几点:
_allocating_init
~>swift_allocObject
~>_swift_allocObject_
~>swift_slowAlloc
~>malloc
Swift
对象的内存结构HeapObject(OC objc_object)
,有两个属性: 一个是Metadata
,一个是RefCount
,默认占用16字节大小。 通过源码定位到了HeapObject
,HeapObject
结构体中找到了HeapMetadata
,根据命名空间又找到TargetHeapMetadata
;
using HeapMetadata = TargetHeapMetadata<InProcess>;
从TargetHeapMetadata
找到下面的定义:
template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
using HeaderType = TargetHeapMetadataHeader<Runtime>;
TargetHeapMetadata() = default;
constexpr TargetHeapMetadata(MetadataKind kind)
: TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
: TargetMetadata<Runtime>(isa) {}
#endif
};
根据函数的定义,如果是纯Swift
类就是Metadata
,如果是和objc
交互,那它就是isa
,这就是纯Swift
代码以及存在objc
代码生成不同的数据类型。关于源码中的kind
种类的定义,如下表:
Name | Value |
---|---|
Class | 0x0 |
Struct | 0x200 |
Enum | 0x201 |
Optional | 0x202 |
ForeignClass | 0x203 |
Opaque | 0x300 |
Tuple | 0x301 |
Function | 0x302 |
Existential | 0x303 |
Metatype | 0x304 |
ObjCClassWrapper | 0x305 |
ExistentialMetatype | 0x306 |
HeapLocalVariable | 0x400 |
HeapGenericLocalVariable | 0x500 |
ErrorObject | 0x501 |
LastEnumerated | 0x7FF |
根据kind
我们找到了getTypeContextDescriptor
的函数定义:
如果
getKind
类型是Class
,就强转成TargetClassMetadata
类型,根据TargetClassMetadata
的定义,我们可以猜测Swift
类的数据结构大概会是下面这样:
struct Metadata {
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
以上是通过源码得出的结果,下面通过示例来验证一下是不是这样的
示例分析
我们可以把类的指针绑定为自定义的HeapObject结构的数据类型,代码如下:
struct HeapObject {
var metadata: UnsafeRawPointer
var refcounted1: UInt32
var refcounted2: UInt32
}
class ATTeacher {
var age: Int = 18
var name: String = "Atom"
}
var t = ATTeacher()
// 获取原生指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)
然后运行:
通过打印可以看出类的指针还原了HeapObject类型,输出了metadata的指针地址信息,根据这步操作,看能不能再把metadata的指针还原成Metadata的数据呢,再改一下代码:
struct HeapObject {
var metadata: UnsafeRawPointer
var refcounted1: UInt32
var refcounted2: UInt32
}
struct Metadata{
var kind: Int
var superClass: Any.Type
var cacheData: (Int, Int)
var data: Int
var classFlags: Int32
var instanceAddressPoint: UInt32
var instanceSize: UInt32
var instanceAlignmentMask: UInt16
var reserved: UInt16
var classSize: UInt32
var classAddressPoint: UInt32
var typeDescriptor: UnsafeMutableRawPointer
var iVarDestroyer: UnsafeRawPointer
}
class ATTeacher {
var age: Int = 18
var name: String = "Atom"
}
var t = ATTeacher()
// 获取原生指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
// 把objcRawPtr指针绑定为HeapObject结构体类型
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)
// 把objcPtr.pointee.metadata的指针绑定为Metadata的结构体类型
// MemoryLayout测量数据大小
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)
再次运行
通过代码示例也验证了Swift类的数据结构就是上面源码分析得出的Metadata。
总结
通过本篇了解了Swift中类和结构体、类的初始化和类的生命周期。
- 类和结构体中的异同点
- 类的各种初始化器的概念和用法
- 通过SIL分析了类的编译过程
- 通过源码分析了类的结构及通过示例去验证