大纲:
- 类与结构体
- 类的初始化器
- 类的生命周期
一、类与结构体
Struct和Class实在太像了,用起来没啥大差别,以至于开发的时候习惯性忽略其差异,先来两段代码:
Class:
class CTTeacher{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
deinit{
}
}
Struct:
struct CTTeacher{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
deinit{
}
}
乍一看它俩一样,实际还是有些区别:
- 类有继承的特性,而结构体没有
- 类型转换使您能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源
- 引用计数允许对个类实例有多个引用
- 类是引用类型,struct是值类型
相同点:
- 可定义存储值的属性
- 可定义方法
- 定义下标以使用下标语法提供对其值的访问
- 定义初始化器【struct会默认生成一个init初始化器,class需要手动写】
- 可使用 extension 来拓展功能
- 遵循协议来提供某种功能
类是引用类型,struct是值类型
引用类型和值类型:
-
引用类型可以理解为在线的Excel ,不管怎么修改,分享给多少人修改,永远在改同一个源文件
-
值类型则是完整拷贝,你我修改的不是同一个文件。
-
储存位置不同:引用类型存在**堆(Heap)**上,值类型存在栈(Stack)上,这俩地方有一个读取速度的快慢:栈(Stack)速度 > 堆(Heap)速度,换句话说:值类型的创建与读取速度 > 引用类型的创建与读取速度
【根据这一点,得出一个平时开发可注意的点:开发过程一些 (不需要继承特性的)、(不需要在运行时检查和解释类实例的),都尽量用struct】
附件:官网提供的测试struct与class初始化速度对比demo:StructVsClassPerformance
从内存的角度来看看类与结构体的不同:
结构体内存探究_测试代码一:
struct CTPerson{
var age:Int = 0
var name:String = ""
init(_ age:Int, _ name:String){
self.age = age
self.name = name
}
}
var person = CTPerson(10, "是我")
lldb命令: 查看实例与属性的内存地址
frame variable -L 实例
打印结果:
根据打印得出第一个特征:
➤ 1.结构体实例的首地址就是它第一个属性的地址
结构体内存探究_测试代码二:
var person = CTPerson(10, "是我")
var person2 = person
打印结果:
根据打印得出第二个特征:
➤ 2.复制实例后,新实例的地址和每个成员属性的地址均发生改变
结构体内存探究_测试代码三:
struct CTPerson{
var age:Int = 0
var name:String = ""
var teaccher:CTTeacher
init(_ age:Int, _ name:String, _ teacher:CTTeacher) {
self.age = age;
self.name = name;
self.teaccher = teacher;
}
}
class CTTeacher{
var age:Int = 0
var name:String = ""
init(_ age:Int, _ name:String){
self.age = age
self.name = name
}
}
let tearcher = CTTeacher(25, "老师")
var person = CTPerson(10, "我是class实例", tearcher)
打印结果:
根据打印得出第三个特征:
➤ 3.当结构体内部有class类型的成员属性时,实例中存储的是该class的指针,该成员属性仍然是分配在堆中。
类内存探究_测试代码一:
class CTPerson{
var age:Int = 0
var name:String = ""
init(_ age:Int, _ name:String){
self.age = age
self.name = name
}
}
var person = CTPerson(10, "我是class实例")
打印结果:
根据打印得出第一个特征:
➤ 1.类实例的首地址与第一个成员属性的首地址不同,实例地址与第一个成员变量之间相差0x10
类内存探究_测试代码二:
var person = CTPerson(10, "我是class实例")
var person2 = person
print("------ 修改age后 -------")
person2.age = 90;
打印结果:
根据打印得出第二,第三个特征:
➤ 2.复制出来的新实例的地址和成员属性的地址均没有发生改变。
➤ 3.修改新实例的成员属性之后,旧实例的成员属性也跟着变化。
二、类的初始化器
类编译器默认不会自动提供成员初始化器(init方法),但结构体会。
结构体光这么写属性不写init方法是ok的:
struct CTPerson{
var age:Int
var name:String
}
但是class不行,你一定要写个init方法,ok,这个init方法,还要几种细分:
1.来个最简单的:init()
- 这个init有个叫法:指定初始化器
class CTPerson{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
}
2.还有一种:便捷初始化器 convenience init()
class CTPerson{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
convenience init(){
self.init(age: 8, name: "void")
}
}
这个便捷初始化器存在的意义在于:调用方便,不用传参 ,例如:
let person = CTPerson()
使用便捷初始化器的时候有几个注意的点,就是当这个类有派生类的时候,那么在派生类中需要注意:
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。
- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
说了这么多来一段代码:
class CTPerson{
var age:Int
var name:String
init(age:Int, name:String) {
self.age = age
self.name = name
}
convenience init(){
self.init(age: 8, name: "void")
}
}
class CTStudent:CTPerson{
var subjectName:String
init(subjectName:String){
self.subjectName = subjectName
super.init(age: 24, name: "Helios")
}
}
3.可失败初始化器 init?()
中间有个问号,别问,问就是得这么写
解释:当前因为参数不合法或者外部条件的不满⾜,初始化失败,可以写return nil语句,来表明初始化失败
class CTPerson{
var age:Int
var name:String
init?(age:Int, name:String) {
if age < 18 {
return nil
}
self.age = age
self.name = name
}
}
4.必要初始化器 required init()
该类的⼦类都必须实现该初始化器
class CTPerson{
var age:Int
var name:String
required init(age:Int, name:String) {
self.age = age
self.name = name
}
}
class CTStudent:CTPerson{
var subjectName:String
required init(age: Int, name: String) {
self.subjectName = "IOS"
super.init(age: 24, name: "Helios")
}
}
三、类的生命周期
3.1 swift编译
iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的
➤ OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o。
➤ Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。
在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 |
3.2 sil文件分析
我们来跑一段:
swiftc -emit-sil ${SRCROOT}/macSwift/main.swift > ./main.sil && open main.sil
测试代码:
class CTTeahcer{
var age:Int = 18
var name:String = "是我"
}
var t = CTTeahcer()
生成的sil文件:
class CTTeahcer {
@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue var name: String { get set }
@objc deinit
init()
}
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
alloc_global @$s4main1tAA9CTTeahcerCvp // id: %2
%3 = global_addr @$s4main1tAA9CTTeahcerCvp : $*CTTeahcer // user: %7
%4 = metatype $@thick CTTeahcer.Type // user: %6
// function_ref CTTeahcer.__allocating_init()
%5 = function_ref @$s4main9CTTeahcerCACycfC : $@convention(method) (@thick CTTeahcer.Type) -> @owned CTTeahcer // user: %6
%6 = apply %5(%4) : $@convention(method) (@thick CTTeahcer.Type) -> @owned CTTeahcer // user: %7
store %6 to %3 : $*CTTeahcer // id: %7
%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'
// CTTeahcer.__allocating_init()
sil hidden [exact_self_class] @$s4main9CTTeahcerCACycfC : $@convention(method) (@thick CTTeahcer.Type) -> @owned CTTeahcer {
// %0 "$metatype"
bb0(%0 : $@thick CTTeahcer.Type):
%1 = alloc_ref $CTTeahcer // user: %3
// function_ref CTTeahcer.init()
%2 = function_ref @$s4main9CTTeahcerCACycfc : $@convention(method) (@owned CTTeahcer) -> @owned CTTeahcer // user: %3
%3 = apply %2(%1) : $@convention(method) (@owned CTTeahcer) -> @owned CTTeahcer // user: %4
return %3 : $CTTeahcer // id: %4
} // end sil function '$s4main9CTTeahcerCACycfC'
• @main:入口函数
• %0:寄存器
• 这段代码大体流程就是解析CTTeahcer类,然后申请内存,调用__allocating_init()函数进行初始化。
3.3 汇编断点分析
还是刚才的代码
class CTTeahcer{
var age:Int = 18
var name:String = "是我"
}
var t = CTTeahcer()
来个断点:
进入汇编
能看到它调用了 __allocation_init() 这个玩意儿,继续step into进入看看
又调用了
swift_allocObject 和 init() 函数
那如果类继承自NSObject又是个啥情况?
class CTTeahcer:{
var age:Int = 18
var name:String = "是我"
}
var t = CTTeahcer()
__allocation_init() 里边儿调用的是熟悉的objc_allocWithZone,走的是消息发送
汇编能看的东西有限,这个初始化的调用流程【从汇编调用的角度】也只能看到这儿,详细还是得看源码: github, 百度云提取码: 9g7g
我们来看源码,swift_allocObject 到底干了啥事儿
直接来到 HeapObject.cpp 文件,看到 _swift_allocObject_ 这个方法
这个方法内部,又调用了
swift_slowAlloc方法,进去看一下:
他的内部又调用了
malloc 来分配堆区内存,ok,到这里就很清晰了。
总结一下,Swift对象内存分配的流程:
__allocating_init --> swift_allocObject --> _swift_allocObject_-->swift_slowAlloc-->Malloc
Swift对象的内存结构是:HeapObject
有两个属性,一个是 metadata 一个是 refCounts 各占用8字节
从上面截图的代码可以看出来,metadate是一个HeapMetadata的别名类型,对应的类是TargetHeapMetadata,那这又是个啥?进去看一下:
通过判断是不是swift与oc混编,如果不是,那么这个数据结构就是kind类型,如果是swift与oc混编则转成isa类型,那这个
kind是个什么玩意儿?
进去看:
这里面定义了许多类型:具体都在 『MetadataKind.def』这个文件里面
| 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 |
好了,看到这里,我们还是不能弄明白这个matedata的数据结构是什么样的,那就进去父类看看,来到TargetMetadata:
这里有一个 StoredPointer kind 指明我们当前的类型,它上面有一句注释:getKind() must be used to get the kind value. 说
getKind方法会得到kind的类型,好,我们找一下调用 getkind 方法的地方:
好家伙,一顿swiftch,通过调用
getKind方法 的到kind值,来匹配类型!
好,接着分析,我们看到匹配Class的case:就是匹配到class的话,会把 this 强转,转为 <const TargetClassMetadata<Runtime> *>这么个玩意儿,那么,TargetClassMetadata 是不是我们要找的class的数据结构?进去看:
好家伙,有没有似曾相识的感觉
进去它父类再看:
直呼好家伙! 这不就跟oc的objc_class一样吗,这个数据结构就是Swift的class的数据结构!
整理一下:
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
}
好,到目前为止,以上都是通过看源码得出的猜测,我们需要通过代码来验证。 测试代码:
struct HeapObject{
var metadata:UnsafeRawPointer
var refcounted1:UInt32
var refcounted2:UInt32
}
class CTPerson {
var age:Int = 8
var name:String = "CT"
}
var t = CTPerson()
//获取实例对象的指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
//将对象的指针重新绑定到HeapObject类型上
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)
有metadata,意味着绑定成功,接下来我们把metadata绑定到刚才我们整理的struct上
//接着刚才的代码,增加这个Metadata结构体
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
}
//获取实例对象的指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
//将对象的指针重新绑定到HeapObject类型上
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
//将对象的metadata重新绑定到我们自己的数据类型
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride)
print(metadata.pointee)
嘿嘿,成了。ok,通过代码的方式验证了刚才对swift的Class的数据结构的猜想。