Swift分析 -- 类与结构体(上)

3,201 阅读6分钟

「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。

本篇重点:
类与结构体在类型及初始化方面的相同与不同点。

一、Class & Struct

1.1 类与结构体的相同/不同点

  • 相同点
    1. 定义存储值的属性
    2. 定义方法
    3. 定义初始化器
    4. 使用下标语发访问其值
    5. 使用extension来扩展功能
    6. 遵循协议提供某种功能
struct/class UIPerson{
    var age: Int                    //age:存储值属性
    var name: String
    init(age: Int, name: String) { // 定义方法 定义初始化器
      self.age = age
      self.name = name
    }
}
extension UIPerson{		    //extension 扩展方法
    func shrink(){
        print("extention")
    }
}
var u = UIPerson.init(age: 19, name: "io")	// 下标访问其值
u.shrink()	                               //invoke extension 
  • 不同点
    1. Class 可以有继承,而Struct没有
    2. Class 有析构函数 -> deinit()
    3. Class 有引用计数
    4. 类型转换能够在运行时检查和解释类实例的类型
    5. 内存中存储位置不同
    6. Classs 是引用类型,Struct是值类型

1.2值类型与引用类型

  • 值类型:变量存储的就是具体的实例
  • 引用类型:变量不存储具体的实例对象,而是对具体实例对象的内存地址的引用
  • 关于存储位置:值类型存储在 Stack区,运行速度要比Heap快
// 同样一段代码,TestClass分别时class、struct时,t、p的内存存储的差别如下图所示。
var t = TestClass(name: a)
var p = t
  • 值类型 png.png

  • 引用类型

png.png

1.3 内存储存区域分析

关于内存布局目前还只懂些皮毛,就简单画一下图。

png.png

Stack区域存储局部变量运行上下文,所以创建下面一段代码来分析一下对象的存储位置

struct UIPerson{ 
    var age = 18
    var name = "Luck" 
}
func test(){
    //t 就是局部变量
    var t = UIPerson() 
    print("end")
} 
test()

根据上述的内存图来分析,其中 var t 作为局部变量是在Stack中开辟了8字节进行存储,然后它所指向了UIPerson是在Stack上开辟的内存空间。

可以使用以下命令来验证上述分析是否正确:frame varibale -L xxx ⬇️

HDftmqmIOPe1O-ZGpqfDqnERIwbMApKOVPwad-v2tlk.png

同样还是将UIperson改为引用类型的Class,再进行测试。

1.4 Stack与Heap速度比较

分析完了 值类型 、 引用类型以及其他代码在Stack、heap上的空间分配,接下来对刚才提到的堆栈速度进行一下比较。直接使用 Git上StructVsClassPerformance的例子对速度进行测试。

uoCjtTYbtkx2InR8KVBYwC9tXfEYgUw0qHAqgxfBktI.jpg

二、初始化器

2.1 声明初始化器

如图示的方式为类或结构体定义属性,编译器在类上报了3个错误,而结构图报了2个,来说明一下这2种错误的原因。

Eau_SoaypUD4AejtdK5O9mL-J0G9h1s2sftJVhetvb4.png

  1. Swift是类型安全语言,所以定义存储属性时,至少是要有类型的。
  2. 在未指定初始化器时,编译器会为Struct生成默认的成员初始化器,所以initialize的错误只在Class中出现。

2.2 初始化器种类

1)初始化器4原则

Swift 中创建类或结构体实例时,须为其所有存储属性设置⼀个合适的初始值,所以也需提供对应的的指定初始化器。同时,也可以提供多个**便携初始化器,为了访问安全,**所有便携初始化器必须先调用当前类中的指定初始化器,且要加上convenience修饰。

uKvhMYWN_aVie6X-9l_35dc4aXtTlTaHYDZPdh4sCVg.png

UIPerson派生出一个子类UIMan 且指定了初始化器,那UIMan的初始化器要在执行Super.init之前,将自己所有属性完成初始化。

GvIov4rzcXN_wxzAyIgv9SDXVbomb5hwnie2XFZPEv4.jpg

类似这样的原则共4条,与之前的一并总结一下,并且按照对应原则,在图中配了相对应的代码示例。

  1. 指定初始化器须保证在调用⽗类初始化器之前,将其类引⼊的所有属性初始化完成
  2. 指定初始化器须先调用⽗类初始化器之后,才能为继承自父类的属性设置新值。否则,赋予的新值会被⽗类中的初始化器所覆盖**(编译器报错)**
  3. 便捷初始化器必须先执行同类中的其它初始化器,然后才可对属性赋新值(包括同类⾥定义的属性)。否则,便捷构初始化器赋予的新值将被⾃⼰类中其它指定初始化器所覆盖**(编译器报错)**
  4. 初始化器在第⼀阶段初始化完成之前,不能调⽤任何实例⽅法、不能读取任何实例属性的值,也不能引⽤ self 作为值。

2)可失败 初始化器

如果当前因为参数的不合法或外部条件不满⾜,可能会初始化失败。Swift 中可失败初始化器写return nil 来表明在何种情况下会触发初始化失败。

class UIPersonC{
    var age: Int
    var name: String
    init?(_ age:Int, _ name: String ){
        if age < 18 {return nil};
        self.age = age
        self.name = name
    }
}

3)必要 初始化器

在初始化器前面添加required修饰,标示该父类所有子类必须实现该初始化器。否则编译器报错,'required' initializer 'init(_:_:)'

class UIPersonC{
    var age: Int
    var name: String
    required init(_ age:Int, _ name: String ){
    	self.age = age
    	self.name = name
    }
}
class UIMan:UIPersonC{
    var hight: Int
    required init(_ age: Int, _ name: String) {    //子类必须实现
    	self.hight = 190
    	super.init(16, "ll")
    }
}

三、浅探Swift中类的结构

3.1 下载Swift 源码

Apple-Swift官网对Swift有简单的介绍说明,也可查看到Swift是开源的:Swift is developed in the open at``Swift.org,网站中包含了Swift有关的各种资源,也包括Swift源码-Release,下载后就可以对Swift方法进行底层的分析了。

3.2 分析Class创建

  1. 通过以下简单的代码进行查看汇编(Debug——> Always Show Disassembly),然后一步步Setp Into可以追到Swift类创建时调用的符号名(swift_allocObject),再根据符号名在到Swift 源码中进行查看分析。

EkNm7k_tmNBN5M5Ez0XqatozNhEBbgH8CVz9_A0tiSA.png

  1. 在源码中全局搜索swift_allocObject,最终是在Heap.cpp中找到了方法的实现,然后对代码的简单分析整理出类的alloc调用流程类在底层的数据结构
  • 调用路径:__allocating_init ---> swift_allocObject ---> _swift_allocObject_ ---> swift_slowAlloc ---> Malloc
  • 数据结构:返回值HeapObject* ,包含两个属性:Metadata、RefCount,默认占用16字节的大小

wOpUbgdCUZbgmTzOyvGC0nQwm_zrI4tPXkzLqad-unY.png

  1. 然后对于HeapObject的属性metadata继续深入,metadata的类型是HeapMetadata const,搜索筛选后HeapMetadata =TargetHeapMetadata,进入后就可以得到父类TargetMetadata,并且发现有对Objc的兼容,那这个metadata就相当于OC类中isa。

lUx--AKktx0EFDHP8OBBlbpTqey7wvkcUwYVyaAaSUk.png

  • 进入TargetMetadata似乎是到瓶颈了,因为它已经没有父类,是最终类了。不过冷静分析一下TargetMetadata的实现逻辑,其中出现了很多MetadataKind的数据类型,应该是在什么位置根据这个Kind进行分发分发方法。然后对MetadataKind进行了搜索,几经分析发现一些端倪,在方法getTypeContextDescriptor对有对kind操作的switch。并且在case中有对应于Class类型TargetClassMetadata

3juOHy2DUQG193Z1vTRm5fPSKBjiPk_cupdv-4YB580.png

  1. 一步一步搜索找到其父类TargetAnyClassMetadata,然后是 TargetHeapMetadata:TargetMetadata。闭环了。

fMCD9cgyyEJk4YKqUOYf1OpBqeSJX4S03w66CB1EzDk.png

  1. 一路搜索分析下来,基本把Class底层的结构体的继承链分析完成了,汇总做一张继承图

png.png

3.3 数据结构汇总

前面的链路梳理,加上过程中找的的结构体上的注释,可以得出Swift中的类的最终结构体是在TargetClassMetadata 中的,将其中的属性摘取出来那么就可以汇总如下了

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 
}