Swift类与结构体(上)

1,055 阅读12分钟

1、初识类与结构体

类:Class

class ZLTeacher {
    var name: String
    var age: Int
    init(_ age:Int, _ name:String) {
        self.age = age
        self.name = name
    }
}

结构体:Struct

struct ZLTeacher {
    var name: String
    var age: Int
}

通过上面两段代码,让我们初步认识了类与结构体,接下来介绍一下类与结构体的相同点与不同点。

相同点:

  • 定义存储值的属性;
  • 定义方法;
  • 定义下标以使用下标语法提供对其值的访问;
  • 定义初始化器;
  • 使用extension来扩展功能;
  • 遵循协议来提供某种功能。

不同点:

  • 类有继承的特性,而结构体没有;
  • 类型转换使你能够在运行时检查和解释类实例的类型;
  • 类有析构函数用来释放其分配的资源;
  • 引用计数允许对一个类实例有多个引用。
  • 最主要的不同点是:类是引用类型,结构体是值类型

1.1、类是引用类型

类是引用类型,也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储的具体实例对象内存地址的引用。(通过修改类中属性的值,类的地址是不会发生变化的)

class ZLTeacher {
    var name: String
    var age: Int
    init(_ age:Int, _ name:String) {
        self.age = age
        self.name = name
    }
}
var t = ZLTeacher.init(18, "ZL")
var t1 = t
print("start")
t.age = 20
print("end")

WeChatb302e0f66179abed81f6c6c4173db89c.png 通过po指令,我们可以查看到t与t1存储的都是内存地址,并且t与t1都是指向同一块内存地址的。修改类t中的属性的值,t1里面的属性也发生变化,但是t与t1的地址并不发生变化。

下面介绍2个指令:
po: “po”与“p”的区别在于使用“po“只会输出对应的值,而”p“则会返回值的类型以及命令结果的引用名。
x/8g:读取内存中的值。(8g:8字节格式输出)

1.2、结构体是值类型

结构体是值类型,值类型存储的就是具体的实例,也就是具体的值。

struct ZLTeacher {
    var name: String
    var age: Int
}
var s = ZLTeacher(name: "ZL", age: 18)
var s1 = s
print("statr")
s.age = 20
print("end")

WeChat4a52670084c810eb8406915bc29f24ee.png

通过po指令,我们可以发现s与s1存储的都是对应结构体的值,并且修改了s的值以后,s1的值并不会发生变化。

综上所述
引用类型就相当于在线的Excel,当我们把这个链接共享给别人的时候,别人的修改我们是能够看到的;
值类型就相当于本地的Excel,当我们把本地的Excel传递给别人的时候,就相当于重新复制了一份给别人,至于他们对内容的修改,我们是无法感知的。

1.3、引用类型与值类型的存储区域

引用类型和值类型还有一个最直观的区别就是存储的位置不同,一般情况,值类型的存储在栈上,引用类型存储在堆上。
首先我对内存区域来一个基本概念的认知,大家看下面这张图 WeChatbad8eb9bbe73c500ecffde6655cc819c.png
栈区(Stack):有系统去管理,地址从高到低分配,先进后出。主要存储局部变量,函数运行过程中的上下文,非OC对象(基本数据类型)。
堆区(Heap):需要我们自己管理内存,地址从低到高分配,先进先出。alloc申请内存,release释放内存。通过alloc分配的对象,OC对象都是存储在堆区中的。
全局区/静态区(Staic):主要存储全局变量和静态变量。初始化的全局变量和静态变量在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后由系统释放。
常量区:主要存储常量字符串,const常量。
代码区:存放App代码,App程序也拷贝在这里。

Segment & Section: Mach-O文件有多个段(Segment),每个段有不同的功能,然后每个段又分为很多小的Section。

TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据
DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common:没有初始化过的符号声明
类的打印如下图 WeChat6bcbcf60fdad64a12e4b36cb41cbeb7e.png 结构体的打印如下图 image.png

这里我们也可以通过gitHub上StuctVsCkassPerformance这个案例来直观的测试当前结构体和类的时间分配。

frame variable -L 指令: 打印对象的内存布局
cat address 0x0000... 指令:打印对象的存储区域

cat address指令,需要配置libLGCatAddress.dylib
配置libLGCatAddress.dylib 在根目录下创建.lldbinit文件,然后在输入plugin load <libLGCatAddress.dylib path>

2.类的初始化器

需要注意的一点是:当前类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)。

2.1、指定初始化器

Swift中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以类在创建的时候必须要提供对应的指定初始化器。

class ZLPerson {
    var name: String
    var age: Int
    //指定初始化器
    init(_ age:Int, _ name:String) {
        self.age = age
        self.name = name
    }
}

当我们派生一个子类ZLTeacher,并指定一个初始化器之后会出现什么问题? image.png 从上图中的错误我们可以发现,指定初始化器必须先向上委托父类初始化器。
我们还要注意几点:
1.指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。 image.png 更改如下: image.png
2.指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖。 image.png
更改如下: image.png

2.2、便捷初始化器

同时我们也可以为当前的类提供便捷初始化器(注意:便捷初始化器必须从相同的类里调用另一个初始化器)

class ZLPerson {
    var name: String
    var age: Int
    //指定初始化器
    init(_ age:Int, _ name:String) {
        self.age = age
        self.name = name
    }
    //便捷初始化器
    convenience init(_ age:Int) {
        self.init(age, "ZL")
    }
}

便捷初始化器也要注意两点:

  • 便捷初始化器必须先委托同类中的其他初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷初始化器赋予的新值将被自己类中其他指定初始化器所覆盖。
  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。 image.png
    更改如下:

image.png

2.3、可失败初始化器

这个也非常好理解,也就意味着当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。这种Swift中可失败初始化器写return nil语句,来表明可失败初始化器在何种情况下会触发初始化失败。写法也非常简单:

失败初始化器,主要就是在指定初始化器init的后面添加一个“?”,并且在所有调用指定初始化器的初始化器的init的后面都需要添加一个“?”。 image.png

2.4、必要初始化器

在类的初始化器前添加required修饰符来表明所有该类的子类都必须实现该初始化器。

image.png
更改如下:

image.png

3、类的生命周期

iOS开发的语言中不管是OC还是Swift,后端都是通过LLVM进行编译的,如下图所示:

image.png

OC通过clang编译器,编译成IR,然后再生成可执行文件.O(这里也就是我们的机器码)。
Swift则是通过Swift编译器编译成IR,然后再生成可执行文件。

image.png
//分析输出AST

//分析输出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.1、SIL文件的分析

3.1.1创建SIL文件

image.png

image.png

image.png

image.png

根据上面流程,运行上面的文件,就可以生成SIL文件了,刚开始无法打开SIL文件,需要将SIL文件的打开方式设置成VSCode,并且启动VSCode,就可以直接打开SIL文件了。

3.1.2、解析SIL

image.png
通过上图可以看出,ZLPerson这个类中存在age,name属性,并且每个属性都存在get,set方法,同时还存在默认的初始化器init,以及析构函数deinit。@_hasInitialValue表示属性设置了默认值。

image.png 总结:

  • s4main1pAA8ZLPersonCvp:这就是全局变量p,只是经过swift混写之后的,可以使用xcrun命令进行还原

在终端中通过xcrun命令还原混写后的字符串
xcrun swift-demangle s4main1pAA8ZLPersonCvp
输出结果为:$s4main1pAA8ZLPersonCvp ---> main.p : main.ZLPerson
main.p指的是,当前字符串解析以后就是main函数里面的变量p

  • @main:入口函数; SIL中的标识符名称以@作为前缀。
  • %0,%1,%2:在 SIL也叫做寄存器,这⾥我们可以理解为我们⽇常开发中的常量,⼀旦赋值之后就不可 以再修改,如果SIL中还要继续使⽤,那么就不断的累加数字。同时这⾥所说的寄存器是虚拟的,最 终运⾏到我们的机器上,会使⽤真的寄存器。
  • %2:声明全局变量 s4main1pAA8ZLPersonCvp 也就是实例对象p
  • %3:获取全局变量 s4main1pAA8ZLPersonCvp的地址
  • %4:获取 ZLPerson的元类型
  • %5:获取 s4main8ZLPersonCACycfC 的函数引用,也就是获取__allocating_init的方法地址
  • %6:通过apply调用%5(也就是__allocating_init函数),并传入参数%4(ZLPerson的元类型),最后将返回值赋值给%6
  • %7:将返回的实例结果%6 存储到 %3(全局变量 s4main1pAA8ZLPersonCvp的地址)的地址中
  • %8:构建Int 0,并返回状态码

image.png

alloc_ref:创建了一个ZLPerson的实例对象,当前实例对象的默认引⽤计数为1调⽤init⽅法,申请内存 swift 调用 swift_allocObject, oc 会调用 allocWithZone。

3.2、汇编分析

image.png
我们在ZLPerson类初始化前打断点,然后在__allocating_init()处打断点

image.png
然后进入__allocating_init()内部,通过lldb指令 si 进入 __allocating_init 函数内部可以得出 __allocating_init 内部会调用 swift_allocObject 和 init 函数

image.png

3.3、Swift源码分析

Swift源码 用 VSCode 打开下载好的 swift 源码,搜索 HeapObject.cpp 文件,找到 swift_allocObject 函数可以看到内部调用了 swift_slowAlloc 函数

image.png
进入swift_slowAlloc 函数 可以看到 调用了 malloc

image.png
综上所述,可以得出结论, swift对象创建时,经过如下的过程进行分配内存
__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc

3.4、类的结构分析

Swift对象的内存结构 HeapObject,内部有两个属性:一个是Metadat,一个是RefCount,默认占用16字节大小; OC对象的内存结构是objc_object,内部只有一个属性就是isa指针。

image.png
上图可以看到HeapObject结构,主要有2个初始化的方法,都包含2个参数分别时HeapMetadata类型的metaData和InlineRefCounts类型的refcounts。
HeapObject中的metaData属性的类型是HeapMetadata 类型,我们进入HeapMetadata 类型中,可以看出HeapMetadata 类型是 TargetHeapMetadata类型定义的别名

image.png
进入 TargetHeapMetadata类型内部

image.png
如上图,可以发现 TargetHeapMetadata 继承自 TargetMetadata,同时也可以知道其初始化时如果是 纯swift类则传入 MetadataKind kind 如果是和 OC 交互的类传入 isa
接下来进入 TargetMetadata,我们可以查看到 kind 的成员变量 image.png
查看 MetadataKind 可得它是一个 uint32_t的类型 image.png

查看MetadataKind.def文件,可以发现Kind有以下种类:

image.png

通过上面查看TargetMetadata类型内部,我们发现TargetMetadata里面有很多方法属性,我们关注下getTypeContextDescriptor的方法

image.png
如上图可以发现,getTypeContextDescriptor的方法内部,当Kind的类型是Class类型的时候,会拿到一个名称为TargetClassMetadata的指针,我们看看TargetClassMetadata的实现:

image.png
如上图可以发现,TargetClassMetadata继承TargetAnyClassMetadata,我们再进入TargetAnyClassMetadata中查看:

image.png
通过上图我们可以发现,如果是OC类,这个TargetAnyClassMetadata这个结构和我们OC中类objc_class的结构类似包括的属性分别是isa,superClass,cache,bits(data)。

总结: 最终通过对对源码的追踪可以发现类的结构由如下继承关系 TargetClassMetadata : TargetAnyClassMetadata :TargetHeapMetadata : TargetMetadata 那么,对于类的最终结构,就是这些结构体所有的成员的集合,经过以上分析,可以大致得出 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 有两个属性: 一个是 Metadata ,一个是 RefCount
所以我们自定义一个结构体用来将我们的实例对象强转为该类型,如果转换成功则说明分析正确.

class ZLPerson {
    var name: String = "ZL"
    var age: Int = 18
}
struct HeapObject {
    var metaData: UnsafeRawPointer
    var refCount1: Int32
    var refCount2: Int32
}
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 p = ZLPerson()
//获取实例对象的指针
let objcRawPtr = Unmanaged.passUnretained(p as AnyObject).toOpaque()
//把HeapObject转换成我们自定义的MeatData结构
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)
//将HeapObject中的metadata绑定成MetaData类型
//MemoryLayout<MetaData>.stride 获取数据结构需要的大小
let metadata = objcPtr.pointee.metaData.bindMemory(to: MetaData.self, capacity: MemoryLayout<MetaData>.stride)
print(metadata.pointee)

image.png
根据蓝色框部分的输出值,可以确定,类的实例对象的结构为 HeapObject 是正确的。 根据红色框部分的输出值,可以确定,metadata实例对象可以转换成上述分析的Metadata类型,也就证明类的数据结构也是正确的。

Swift类和结构体(下)