1、Swift类与结构体(上)

505 阅读9分钟
大纲:
  1. 类与结构体
  2. 类的初始化器
  3. 类的生命周期

一、类与结构体

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{
        
    }
}

乍一看它俩一样,实际还是有些区别:

  1. 类有继承的特性,而结构体没有
  2. 类型转换使您能够在运行时检查和解释类实例的类型
  3. 类有析构函数用来释放其分配的资源
  4. 引用计数允许对个类实例有多个引用
  5. 类是引用类型,struct是类型

相同点:

  1. 可定义存储值的属性
  2. 可定义方法
  3. 定义下标以使用下标语法提供对其值的访问
  4. 定义初始化器【struct会默认生成一个init初始化器,class需要手动写】
  5. 可使用 extension 来拓展功能
  6. 遵循协议来提供某种功能

类是引用类型,struct是类型

引用类型和值类型:

  1. 引用类型可以理解为在线的Excel ,不管怎么修改,分享给多少人修改,永远在改同一个源文件

  2. 值类型则是完整拷贝,你我修改的不是同一个文件。

  3. 储存位置不同:引用类型存在**堆(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 实例

打印结果: image-20211228150414018.png

根据打印得出第一个特征:

➤ 1.结构体实例的首地址就是它第一个属性的地址

结构体内存探究_测试代码二:

var person = CTPerson(10, "是我")
var person2 = person

打印结果: image-20211228150900133.png

根据打印得出第二个特征:

➤ 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)

打印结果:

image-20211228154045285.png

根据打印得出第三个特征:

➤ 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实例")

打印结果:

image-20211228151336528.png

根据打印得出第一个特征:

➤ 1.类实例的首地址与第一个成员属性的首地址不同,实例地址与第一个成员变量之间相差0x10

类内存探究_测试代码二:

var person = CTPerson(10, "我是class实例")
var person2 = person
print("------   修改age后   -------")
person2.age = 90;

打印结果:

image-20211228151950370.png 根据打印得出第二,第三个特征:

➤ 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()

使用便捷初始化器的时候有几个注意的点,就是当这个类有派生类的时候,那么在派生类中需要注意:

  1. 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
  2. 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。
  3. 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
  4. 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 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进行编译的

iShot2021-12-30 10.39.08.png

➤ OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o。

➤ Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。

iShot2021-12-30 10.40.45.png

在swift工程中添加对应脚本,可以生成对应的目标代码

以下列出编译过程的每个步骤脚本,分别可生成上图对应的每个步骤的目标文件

目标脚本
分析输出ASTswiftc main.swift -dump-parse
分析并且检查类型输出ASTswiftc 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()

来个断点: iShot2021-12-30 15.43.24.png 进入汇编 iShot2021-12-30 15.31.27.png

能看到它调用了 __allocation_init() 这个玩意儿,继续step into进入看看

iShot2021-12-30 15.30.59.png 又调用了 swift_allocObjectinit() 函数

那如果类继承自NSObject又是个啥情况?

class CTTeahcer:{
    
    var age:Int = 18
    var name:String = "是我"
}

var t = CTTeahcer()

iShot2021-12-30 15.40.58.png __allocation_init() 里边儿调用的是熟悉的objc_allocWithZone,走的是消息发送

汇编能看的东西有限,这个初始化的调用流程【从汇编调用的角度】也只能看到这儿,详细还是得看源码: github百度云提取码: 9g7g

我们来看源码,swift_allocObject 到底干了啥事儿

直接来到 HeapObject.cpp 文件,看到 _swift_allocObject_ 这个方法

iShot2022-01-09 15.12.27.png 这个方法内部,又调用了 swift_slowAlloc方法,进去看一下:

iShot2022-01-09 15.14.03.png 他的内部又调用了 malloc 来分配堆区内存,ok,到这里就很清晰了。

总结一下,Swift对象内存分配的流程:

__allocating_init --> swift_allocObject --> _swift_allocObject_-->swift_slowAlloc-->Malloc

Swift对象的内存结构是:HeapObject

iShot2022-01-09 15.21.13.png 有两个属性,一个是 metadata 一个是 refCounts 各占用8字节

从上面截图的代码可以看出来,metadate是一个HeapMetadata的别名类型,对应的类是TargetHeapMetadata,那这又是个啥?进去看一下:

iShot2022-01-09 15.40.31.png 通过判断是不是swift与oc混编,如果不是,那么这个数据结构就是kind类型,如果是swift与oc混编则转成isa类型,那这个kind是个什么玩意儿? 进去看:

iShot2022-01-09 15.43.56.png 这里面定义了许多类型:具体都在 『MetadataKind.def』这个文件里面

namevalue
Class0x0
Struct0x200
Enum0x201
Optional0x202
ForeignClass0x203
Opaque0x300
Tuple0x301
Function0x302
Existential0x303
Metatype0x304
ObjCClassWrapper0x305
ExistentialMetatype0x306
HeapLocalVariable0x400
HeapGenericLocalVariable0x500
ErrorObject0x501
LastEnumerated0x7FF

好了,看到这里,我们还是不能弄明白这个matedata的数据结构是什么样的,那就进去父类看看,来到TargetMetadata

iShot2022-01-09 15.56.48.png 这里有一个 StoredPointer kind 指明我们当前的类型,它上面有一句注释:getKind() must be used to get the kind value. 说getKind方法会得到kind的类型,好,我们找一下调用 getkind 方法的地方:

iShot2022-01-09 15.58.19.png 好家伙,一顿swiftch,通过调用 getKind方法 的到kind值,来匹配类型!

好,接着分析,我们看到匹配Class的case:就是匹配到class的话,会把 this 强转,转为 <const TargetClassMetadata<Runtime> *>这么个玩意儿,那么,TargetClassMetadata 是不是我们要找的class的数据结构?进去看:

iShot2022-01-09 16.11.55.png 好家伙,有没有似曾相识的感觉

进去它父类再看:

iShot2022-01-09 16.12.18.png 直呼好家伙! 这不就跟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)

iShot2022-01-09 16.46.37.png 有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)

iShot2022-01-09 16.52.08.png 嘿嘿,成了。ok,通过代码的方式验证了刚才对swift的Class的数据结构的猜想。