Swift 类与结构体的异同

232 阅读6分钟

1 Swift类与结构体

相同

  1. 定义属性用于存储值
  2. 定义方法用于提供功能
  3. 定义下标操作用于通过下标语法访问它们的值
  4. 定义构造器用于设置初始值
  5. 通过扩展以增加默认实现之外的功能
  6. 遵循协议以提供某种标准功能

区分

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

类与结构体本质区分:

类是引用类型。也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用。结构体是值类型,相比较引用类型的变量中存储的是地址,那么值类型存储的就是具体的实例。

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

另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在栈上,引用类型存储在堆上。

实际项目中运用类和结构体需要注意的事项:

由于二者在内存位置上的存储位置的不同,决定了二者运行速度的不同,栈(Stack)速度 > 堆(Heap)速度。进一步讲就是我们在优化或是设计程序时尽量用结构体而非类。

初始化器

初始化器是为了可以完整地初始化实例,所以在初始化器中必须完成对存储属性的赋值

默认初始化器
  • 编译器不会自动提供类的初始化器,需要自己提供一个指定初始化器

16407023969609.jpg

  • 结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!

16407023513938.jpg

可失败初始化器

  • 当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。

16407037824293.jpg

必要初始化器

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

2 类的生命周期

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

16407056407730.jpg

  • OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
  • Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件 如下图:

ade10d78046d45e883869cc5626b86ed.png 我们接下来看下前端编译器swift的命令,类似我们oc中clang的命令语句

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文件分析

根据上面的指令我们生成main的sil文件,我们新建一个工程,在main定义一个Person类 包含一个name属性和一个age属性

在终端输入下面的代码会生成sil文件,并在终端打印生成的main.sil文件

swiftc main.swift -emit-sil

image.png 上面的%0,%1等是虚拟的寄存器。在SIL文件中,你会看到很多不懂的关键词,你可以查看GitHub上的官方文档查阅

  • @main代表入口函数
  • %0 1 2...: 寄存器,这里是虚拟的,跑到设备上会使用真的寄存器
  • alloc_global @$s4main1tAA9LGTeacherCvp:分配一个全局变量,该全局变量的名称是混写的,我们可以通过终端指令xcrun swift-demangle将其还原:
  • %3 = global_addr @$s4main1tAA9LGTeacherCvp:拿到该全局变量的地址给%3寄存器
  • %4 = metatype $@thick LGTeacher.Type:获取LGTeacher.Type的元类型给%4寄存器
  • %5 = function_ref @$s4main9LGTeacherCACycfC : %5是LGTeacher.__allocating_init()函数的引用,也就是拿到LGTeacher.__allocating_init()函数的指针地址
  • %6 = apply %5(%4):使用%5函数并且传入参数%4,把结果返回值(也就是实例变量)给到%6
  • store %6 to %3:将%6也就是实例变量的内存地址,存放到%3这个全局变量中
  • Int在Swift底层中t就是一个Struct类型,%8和%9是在构建一个Int32的整数类型0,所以也就是return 0,类似与OC中main函数最终的return 0

3 类的初始化流程

  • @main代表入口函数首先调用__allocating_init()函数,它是用来创建实例对象的,所以需要看到函数执行的过程,那么在SIL文件中搜索s4main9LGTeacherCACycfC,看到下面的代码。这个函数需要一个元数据类型,这里是LGTeacher.Type,你可以直接把这个元数据类型先理解成isa指针

09a30496e9b84bd2a8efdf9bb755cc35.png

  • 看这个alloc_ref是啥呢,alloc_ref表示创建一个T的实例对象,并且引用计数加1,所以alloc_ref实际上是去堆区申请内存空间

995d847de1334a0b9a43fc671398bfc7.png

  • 汇编模式查看下__allocating_init方法,它在具体做什么呢,我们点击跳进去,里面有两个方法,swift_allocObject在堆区找合适的内存空间,init()在初始化所有的成员变量。

1640955920344.jpg

  • 那么如果让LGTeacher继承自NSObject,又会有什么变化呢,运行后就可以看到LGTeacher.__allocating_init里面调用的就是OC的初始化方法objc_allocWithZone 和 init 两个方法了。
class LGTeacher:NSObject {
    var age: Int = 18
    var name: String = "asd"
}

1640956552799.jpg

swift_allocObject 接下来我们看下这个函数到底干了什么

  • 打开swift源码然后找到HeapObjec.cpp文件,在里面找到了_swift_allocObject_ 方法并且看到这里有三个参数,没错是三个参数 一个metadata(元数据类型),requiredSize(所需要的大小),requireAlignmentMask(所需要对齐的掩码 它的值为7).

1.jpg

  • 然后调用swift_slowAlloc方法,并将requiredSize和requiredAlignmentMask作为参数传了进去,然后swift_slowAlloc方法返回了一个HeapObject类型的指针,malloc函数 也就是分配内存空间的。

1640957373246.jpg

总结下Swift对象进行内存分配的流程

3421.jpg

分析HeapObject结构体

  • 里面包含HeapMetadata,占用8字节内存大小

  • HeapMetadata是TargetHeapMetadata这个类型的别名定义,所以接下去需要看TargetHeapMetadata。 在这里插入图片描述

  • 我们知道在objc中类的结构是objc_class 它的结构如下

image.png 那么swift中类的结构是什么呢 如下

image.png 从上面不难看出 superClass cacheData这些关键词都是一样的

4 总结

  1. swift中的类和struct最主要的区别在于类是一个引用类型,而结构体是值类型。当不涉及继承关系时,我们用来存贮值的时候我们可以优先考虑结构体来存储。
  2. swift中的类本质是heapObject类型的结构体包含metadata和引用计数,我么oc中的类本质是objc_class 类型的结构体。
  3. swift中实例对象存贮的是metadata和引用计数,成员变量。oc中实例变量存储的是isa和成员变量
  4. swift中metadata中包含类的信息通过getTypeContextDescriptor获取描述的类型把数据存在对应的结构体中。