Swift-类与结构体(上)

3,454 阅读10分钟

苹果自2014年推出Swift后,已经经过了多个版本的迭代,Swift运行更快,效率更高,集成了很多高级语言语法特性,本篇通过案例的方式首先来了解一下类和结构体的异同点以及类的初始化,通过源码来分析一下类的生命周期。

初识类与结构体

先看一下类和结构体的定义:

# 结构体的定义只需把`class`关键字换成`struct`即可
class ATTeacher {
  var age: Int
  var name: String
  
  init(age: Int, name: String) {
    self.age = age
    self.name = name
  }
}

相同点

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

不同点

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

图例分析

类(class)

类是引用类型。也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用 01.png 给变量t赋值给t1后,如下图: 02.png 以上是当把类实例t赋值给t1时,他们是引用的同一个内存地址。

结构体(struct)

结构体struct是值类型,结构体的定义也非常简单,相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)

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

var s = ATStudent(age: 18, name: Atom)
var s1 = s

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

代码调试

类(class)

通过上面的定义分析,得出class是引用类型,而struct是值类型,下面通过实际代码来验证一下分析的正确性。定义的class代码和上面一致,然后断点运行: 04.png 初始化ATTeacher赋值给t,然后把t赋值给t1,通过打印输出得出tt1的内存地址是一样的,再改变实例tage的属性值,输出t1age的值,看看结果。 05.png 结果发现t1.age也等于20,这就验证了类是引用类型,他们指向了同一片内存地址。

结构体(struct)

同样,把上面的类改成结构体,再次运行: 06.png 发现输出的都是值,在修改s1.age之前,ss1的值都一致,当修改了s1.age后,s1的值更新了,但s的值还是之前的,这也验证了struct是值类型。

内存区域概念图

引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储的在栈上,引用类型存储在堆上。首先看一下常见的内存区域分布图: 07.png 还是通过代码来验证一下,定义一个函数,代码如下:

func test() {
  var age: Int = 10
  print(age)
}

test()

print打个断点,然后输出age的地址,这里用到了工具去查看变量age的在内存中的存储区域。 08.png

内存区域的存储信息
  • 栈区(stack):局部变量和函数运行过程中的上下文
  • Heap: 存储所有对象
  • Global: 存储全局变量;常量;代码区
  • Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每个段又分为很多小的 Section
  • TEXT.text : 机器码
  • TEXT.cstring : 硬编码的字符串
  • TEXT.const: 初始化过的常量
  • DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
  • DATA.bss: 没有初始化的(静态/全局)变量
  • DATA.common: 没有初始化过的符号声明
类和结构体在内存中的分布

通过frame variable命令查看结构体中变量在内存中的分布情况,还是利用之前的代码

# 可以通过help查看具体的命令文档
help frame variable

09.png 断点调试输出age就是结构体的首地址,nameage增加8个字节。因此当前结构体的在内存中的示意图如下所示: 10.png 当我们初始化ATTeacher的时候就会在栈上申请24字节大小的内存空间,agename在内存中的地址是连续的,当函数执行完成后系统会销毁之前申请的栈内存空间,栈指针还原指向位置。如果其他条件不变,我们在struct中增加一个class实例,这样会不会影响结构体内存分布呢? 11.png 可以看到增加了类是不会对原有的struct内存造成影响的,ATPerson会先在堆上申请内存空间存放在栈上的变量p中。同样我们把struct改成class再运行。 12.png 可以看出class申请的堆内存空间。

类的初始化

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

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

指定初始化器&便捷初始化器

Swift中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。所以类ATPerson必须要提供对应的指定初始化器,同时我们也可以为当前的类提供便捷初始化器(注意:便捷初始化器必须从相同的类里调用另一个初始化器。)

class ATPerson{
  var age: Int
  var name: String
  
  # 指定初始化器
  init(_ age: Int, _ name: String) {
    self.age = age
    self.name = name
  }
  
  # 便捷初始化器
  convenience init() {
    self.init(18, "Atom")
  } 
}
  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
  • 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用self作为值。
示例

下面定义一个子类的初始化器,结合了上面的第1和第2条。不然编译会报错 class.gif

可失败初始化器

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

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

必要初始化器

在类的初始化器前添加require修饰符来表明所有该类的子类都必须实现该初始化器 13.png 如果子类没有提供就编译报错。

类的生命周期

编译流程图

iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示: 14.png OC通过clang编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机器码),Swift则是通过Swift编译器编译成IR,然后在生成可执行文件。 15.png Swift编译过程:

  • Swift代码通过dump parse进行语法分析,解析成抽象语法树
  • 通过语义分析生成Swift中间代码SIL(SIL包括原生的和优化过的)
  • LLVM生成IR
  • 再由后端代码编译成机器码(x86、arm64...)

Swift编译命令

# 分析输出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

SIL代码分析

首先定义一个简单的Swift类,然后根据上面的命令来生成中间代码SILSwift代码如下:

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
}
var t = ATTeacher()

生成中间代码SIL命令:

# 生成SIL并保存在main.sil文件中
swiftc -emit-sil main.swift > ./main.sil

打开生成的main.sil文件,截取部分代码

class ATTeacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @_hasStorage @_hasInitialValue var name: String { get set }
  @objc deinit
  init()
}

// main 入口函数
// 0%~9%:可以理解为SIL的寄存器(虚拟的),最终跑在设备上会映射到真实的寄存器上
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  
  // alloc_global: 分配一个全局变量 
  // s4main1tAA9ATTeacherCvp:混写后的名称,可以通过xcrun swift-demangle s4main1tAA9ATTeacherCvp还原
  alloc_global @$s4main1tAA9ATTeacherCvp // id: %2
  
  // 拿到全局变量的地址给到3%
  %3 = global_addr @$s4main1tAA9ATTeacherCvp : $*ATTeacher // user: %7
  %4 = metatype $@thick ATTeacher.Type // user: %6
  
  // function_ref ATTeacher.__allocating_init()
  // 拿到__allocating_init()函数的指针地址
  %5 = function_ref @$s4main9ATTeacherCACycfC : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher // user: %6
  // 使用5%的函数指针,参数是metatype,把结果返回给6%
  %6 = apply %5(%4) : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher // user: %7
  
  // 把6%存储在3%(也就是将实例变量的地址存储在全局变量里)
  store %6 to %3 : $*ATTeacher // id: %7
  
  // 创建一个Int32为0的值然后返回,就是main函数的return 0
  %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'

有关Swift SIL详细说明可以参考官方文档

从上面的main函数里有个函数指针function_ref对应的s4main9ATTeacherCACycfC,在SIL文件搜索一下可以定位到以下函数:

// ATTeacher.__allocating_init()
// ATTeacher.Type:可以理解为isa
sil hidden [exact_self_class] @$s4main9ATTeacherCACycfC : $@convention(method) (@thick ATTeacher.Type) -> @owned ATTeacher {
// %0 "$metatype"
bb0(%0 : $@thick ATTeacher.Type):
  %1 = alloc_ref $ATTeacher // user: %3
  // function_ref ATTeacher.init()
  %2 = function_ref @$s4main9ATTeacherCACycfc : $@convention(method) (@owned ATTeacher) -> @owned ATTeacher // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned ATTeacher) -> @owned ATTeacher // user: %4
  return %3 : $ATTeacher // id: %4
} // end sil function '$s4main9ATTeacherCACycfC'

查看alloc_ref官方的文档的解释 16.png alloc_ref其实就是从堆区申请内存空间,如果标识为objc的就会以Objective-C的初始化方法。

示例分析

在初始化ATTeacher的时候打个断点,debug以汇编的模式打开,可以看到初始化是swift_allocObject 17.png 再把ATTeacher集成自NSObject再次运行,可以看到初始化方式通过Objective-C的方式。

class ATTeacher: NSObject {
    var age: Int = 18
    var name: String = "Atom"
}
var t = ATTeacher()

18.png

Swift源码分析

通过上面的SIL代码以及结合实例断点运行,在Swift初始化函数的过程中我们看到了swift_allocObject,接下来下载Swift的源码,通过全局搜索可以定位到HeapObject.cpp文件中有下面的方法: 19.png swift_allocObject调用了_swift_allocObject__swift_allocObject_又调用了swift_slowAlloc,再全局搜索swift_slowAlloc可以看到它的函数定义: 20.png 在这里就可以看到调用了malloc,基本上和Objective-C的初始化类似,最终也会调用malloc来开辟内存空间。

从上面的分析可以做以下几点:

  • _allocating_init ~> swift_allocObject ~> _swift_allocObject_ ~> swift_slowAlloc ~> malloc
  • Swift对象的内存结构HeapObject(OC objc_object),有两个属性: 一个是Metadata,一个是RefCount,默认占用16字节大小。 通过源码定位到了HeapObjectHeapObject结构体中找到了HeapMetadata,根据命名空间又找到TargetHeapMetadata
using HeapMetadata = TargetHeapMetadata<InProcess>;

TargetHeapMetadata找到下面的定义:

template <typename Runtime>
struct TargetHeapMetadata : TargetMetadata<Runtime> {
  using HeaderType = TargetHeapMetadataHeader<Runtime>;

  TargetHeapMetadata() = default;
  constexpr TargetHeapMetadata(MetadataKind kind)
    : TargetMetadata<Runtime>(kind) {}
#if SWIFT_OBJC_INTEROP
  constexpr TargetHeapMetadata(TargetAnyClassMetadata<Runtime> *isa)
    : TargetMetadata<Runtime>(isa) {}
#endif
};

根据函数的定义,如果是纯Swift类就是Metadata,如果是和objc交互,那它就是isa,这就是纯Swift代码以及存在objc代码生成不同的数据类型。关于源码中的kind种类的定义,如下表:

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

根据kind我们找到了getTypeContextDescriptor的函数定义: 21.png 如果getKind类型是Class,就强转成TargetClassMetadata类型,根据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
}

以上是通过源码得出的结果,下面通过示例来验证一下是不是这样的

示例分析

我们可以把类的指针绑定为自定义的HeapObject结构的数据类型,代码如下:

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
}

var t = ATTeacher()
// 获取原生指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)

然后运行: 22.png 通过打印可以看出类的指针还原了HeapObject类型,输出了metadata的指针地址信息,根据这步操作,看能不能再把metadata的指针还原成Metadata的数据呢,再改一下代码:

struct HeapObject {
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}

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
}

class ATTeacher {
    var age: Int = 18
    var name: String = "Atom"
}

var t = ATTeacher()
// 获取原生指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
// 把objcRawPtr指针绑定为HeapObject结构体类型
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self, capacity: 1)
print(objcPtr.pointee)

// 把objcPtr.pointee.metadata的指针绑定为Metadata的结构体类型
// MemoryLayout测量数据大小
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee

print(metadata)

再次运行 23.png 通过代码示例也验证了Swift类的数据结构就是上面源码分析得出的Metadata。

总结

通过本篇了解了Swift中类和结构体、类的初始化和类的生命周期。

  • 类和结构体中的异同点
  • 类的各种初始化器的概念和用法
  • 通过SIL分析了类的编译过程
  • 通过源码分析了类的结构及通过示例去验证