Swift 类与结构体--类和结构体的本质

345 阅读8分钟

对于swift而言,类和结构体都是最基础的结构,那么今天我们研究一下他们, 首先看一下他们的区别

类与结构体的相同点

  • 都可以实现方法,都可以定义计算属性和存储属性,都支持属性监听。
  • 都支持扩展(extension)。
  • 都可以遵守协议。
  • 都可以定义下标subscript以使用下标语法提供对其值的访问。
  • 都要定义初始化器,指定初始化和快捷初始化器。

类与结构体的不同点

  • 结构体属于值类型,类属于引用类型;
  • 结构体不可以继承,可以继承;
  • 值类型赋值给let var 或者函数传参的时候完,全是深拷贝;
  • 结构体的方法修改属性的时候需要用@mutating修饰(枚举也需要);
  • required关键字只支持Class, Class可以用static和Class 关键字修饰静态方法;Struct 只能用Static 修饰;
  • 值类型在代码中使用要比引用类型使用占用内存少,读写少一步,也没有引用计数的内存占用;
  • 类有析构函数用来释放其分配的资源;
  • 引用计数允许对一个类实例有多个引用;
  • 类型转换使您能够在运行时检查和解释类实例的类型;

类是引用类型,一个类类型的变量存储在栈中,这个变量的值是当前类实例内存地址的引用,而实例的内存地址则在堆中存储。 结构体是值类型,一个结构体类型的变量则直接存储在栈中

首先我们对内存区域来一个基本概念的认知,大家看下面这张图

image.png

栈区(stack): 局部变量和函数运行过程中的上下文, 0x00000FF 堆区(Heap): 存储所有对象 Global: 存储全局变量;常量;代码区

例如:

func test(){
    //我们在函数内部声明的age变量是不是就是一个局部变量
    var age: Int = 10
    print(age)
}
test()

image.png

打印内存发现,address:0x00007ffeefbff418, stack address age变量是处于栈区。

Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每个段又分为很多小的 Section 例如如下section TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量 DATA.common: 没有初始化过的符号声明

结构体中的属性,如果有引用类型属性,类的实例仍然存储到内存的堆中,栈中的该属性变量,仍然是实例内存的引用

值类型的内存分配,以及释放效率都比较高,并且处于栈中,所以比较安全,引用类型的内存堆内存分配和释放步骤较多,所以效率相比较值类型较低,所以苹果建议尽量使用结构体。

初始化器

初始化器是类和结构体分配内存后,要使用的必经之路。这里有指定初始化器和便捷初始化器两个概念。

指定初始化器和便捷初始化器(convenince)

为了让我们类的初始化时候,必须按照指定的方式,避免一些必要初始化逻辑的丢失,所以一个类最好要有一个指定初始化器,其余可作为便利初始化器,并且便利初始化器的实现里面,必须从相同类里调用一个其他的初始化器。

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

    convenience init() { 
        self.init(age: 18, name:"Kody")
    } 
}

这里我们记住: 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。 说白了,super.init()之前,自己的属性都要初始化完成。

指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖

便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。

初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。

可失败初始化器

这是为了满足,提供的参数可能存在初始化失败的情况下,来使用可失败初始化器,例如:

必要初始化器

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

类的生命周期

前面只是叙述了类和结构体的一些语法和特性的区别以及相同点,那么想分析本质,就得有一些渠道和方法,下面我们说说swift底层分析的三板斧,SIL,Mach-O和源码

SIL

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

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

下面是swift编译过程: image.png 编译器首先对代码进行语法分析,解析成AST抽象语法树,然后语义分析,类型检查是否正确,是否安全,降级生成SIL( Swift Intermediate Language)SIL原生(SLL),优化(SIL)两种,最后降级成LLVM IR ,最终编译成为Machine Code。 swift编译一个很大的特点就是生成SIL,下面看一段简单的SIL WeChat02091712f6d49754f1c1a1236b869aa5.png

从字面意思就能看懂。 然后,SIL中以@作为标识符,文件中有一个@main,这个是入口函数的标识符。 image.png 我们可以看到,本main程序运行了一个LGTeacher()的实例创建,关键看__allocating_init()这个方法,经过查找这个方法的实现为: image.png 这个方法的参数thick LGTeacher.Type源类型,我们猜测是不是可以理解为OC的isa,继续跟踪,发现主要是alloc_ref $LGTeacher这个方法,然后去查SIL的官方文档,找到alloc_ref的描述: image.png 红框中文字说明,这个方法就是创建一个T的实例对象,并且这个对象引用计数会初始化为1,也就是我们先前的去堆申请内存空间,特殊的,如果这类标识是OC的,会走OC的初始化方式。 例如: image.png 上面代码debug调试,观看汇编,发现__allocating_init方法,该方法执行如下俩个方法,swift_allocObject 和 LGTeacher.init(), 就是先分配内存,然后实例初始化 WeChat8ae151e73c751ebcfc4b20d7d6f2eab3.png WeChat6727d35e21d6cd7cef57f6f57ae9c764.png

如果LGTeacher继承的是NSObject,那么会怎么样呢? image.png image.png image.png 看到了吧,很经典的OC创建和初始化方式(objc_allocwithZone)。和SIL的说明是一致的。 这里不研究OC的流程,看纯swift的swift_allocObjec会做什么? 这里呢就是swift分析的第二把板斧-源码。

源码

查看swift c++源码。我们找到swift_allocObjec方法 image.png 它主要调用的是_swift_allocObjec image.png 先执行swift_slowAlloc,malloc(size)开辟内存, image.png 最后转为HeapObject结构体,返回 image.png

我们总结一下: Swift 对象内存分配:

  1. __allocating_init 
  2. swift_allocObject 
  3. swift_allocObject
  4. swift_slowAlloc 调 Malloc
  5. 返回HeapObject结构

现在我们知道,实例对象是不是就是一个HeapObject的结构体,它里面是什么呢? image.png 我们得出一个结论:

Swift实例对象的内存结构就是HeapObject(OC就是objc_object),

HeapObject有两个属性: 一个是HeapMetadata 8字节 ,一个是RefCount 64位位域信息,默认占用16字节大小(2个8字节)。

接下来分析,HeapMetadata的内容。 通过如下源码swift源码发现,HeapMetadata就是TargetHeapMetadata的别名,继承自Metadata, image.png

那么我们看看TargetHeapMetadata的初始化方法, 如果是OBJC-swift交互,初始化时候TargetHeapMetadata需要一个isa, 如果是纯swift,初始化时候则需要一个MetadataKind kind。 MetadataKind是一个uint32类型,所以我们可以推测,MetadataKind也是一个类似isa的东东。类型呗,kind 再看看TargetHeapMetadata的父类TargetMetadata。 我们在里面发现一个getKind(), kind判断了值类型和class类型 所有class类型都转成TargetClassMetadata 所有value类型都转成TargetValueMetadata image.png 进入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
}

那么我们结论是什么呢 Swift类和结构体的本质就是Metadata(TargetClassMetadata或TargetValueMetadata)