Swift进阶(一) —— 类与结构体

8,621 阅读7分钟

一、结构体

在swift中,结构体是非常重要的数据结构,swift的标准库中,绝大多数的公开类型都是结构体,例如BoolIntDouble、 StringArrayDictionary 等常见类型都是结构体。

结构体的定义方式如下:

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

1.结构体的初始化

所有的结构体都有一个有编译器自动生成的一个初始化器,以保证结构体中所有的成员都有初始值。

截屏2021-12-29 上午10.21.15.png

如果你自定义了一个初始化器,那么编译器就不会给你生成一个初始化器。

截屏2021-12-29 下午8.33.50.png

我们在定义结构体的初始化时,一定要保证结构体中的所有成员都有初始值,如果我们对结构体的某个成员变量赋初始值,那么生成的初始化器可以不用给该成员变量作为参数传值。

二、类

类和结构体基本类似,主要有以下不同点:

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

1.类的初始化

和结构体不同,编译器默认不会为类自动提供成员初始化器。所以,如果你的类里面没有为成员变量提供初始值,那么你必须为类提供一个指定的初始化器,保证你创建的类中所有的成员都有初始值

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

我们也可以为类创建一个便捷初始化器,便捷初始化器必须从相同的类里调用另一个初始化器。

class FYPerson {
    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")
    }
}

在swift中,构造指定初始化器和便捷初始化器有一系列的规则:

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

可失败初始化器: 我们在对类进行初始化时,有可能会遇到外部参数输入不合法,或者没有满足可以初始化的条件,最终导致初始化失败的情况出现。因此,我们可以使用可失败初始化器,当出现初始化失败的情况时,我们允许类返回nil值,用来表示类初始化失败。可失败初始化器的定义如下:

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

var p = FYPerson(age: 16, name: "FY") //将会返回一个nil

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

截屏2021-12-31 下午10.25.45.png 如上代码所示,继承了FYPerson类的FYMan类,如果没有实现init(age:, name:)初始化器,那么编译器就会报错。

三、值类型和引用类型

Swift里面的类型分为两种:值类型和引用类型。

  • 值类型:每个实例都保留了一分独有的数据拷贝,一般以结构体(struct)、枚举(enum)或者 元组(tuple)的形式出现。
  • 引用类型:每个实例共享同一份数据来源,一般以类(class)的形式出现。

1.值类型和引用类型的区别

值类型的变量在赋值时是深拷贝,当一个实例变量都会有一个数据副本。如果把一个struct类型的实例变量赋值给另外一个变量时,当修改这个实例变量的属性值时,不会影响另外一个实例变量的值。

截屏2022-01-02 下午9.27.07.png

截屏2022-01-02 下午9.38.01.png

通过查看变量p的内存结构,我们发现struct类型的实例变量存储的是值。当p赋值给p1,变量p1得到的是变量p的拷贝,修改p1变量的age属性,p变量的age属性值保持不变。

引用类型的变量不会直接存储具体的实例对象。

截屏2022-01-02 下午10.16.33.png

截屏2022-01-02 下午10.17.16.png

我们查看p和p1的内存结构,可以看见变量p和p1存储的是通一个具体实例的内存地址。

截屏2022-01-02 下午11.07.31.png 我们再次查看p和p1变量对应的地址的值,发现当p1的age值改变以后,p对应的age值也发生了改变。

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

四、类和结构体的存储位置

引用类型和值类型中还有一个不同的地方就是存储位置不同,值类型一般存储在栈(stack)上,而引用类型一般存储在堆(heap)上。我们通过class和struct的存储位置来做一个对比。 截屏2022-01-23 下午7.42.35.png 使用frame varibale -L xxx获取内存地址,然后通过cat address命令可以得到实例对象在内存的位置。

  • 对于类的内存分配

    1. 在栈上开辟 8 字节内存空间存储t变量,t变量中存储LGTeacher的地址
    2. 在堆上,会寻找合适的内存区域,开辟内存,存储LGTeacher的实例对象
    3. 函数结束时,会销毁栈上的指针,查找并回收堆上的示例变量。 截屏2022-01-23 下午7.45.17.png
  • 对于结构体的内存分配

    在栈上直接分配结构体内存大小的空间存储结构体的值 截屏2022-01-23 下午7.48.56.png 截屏2022-01-23 下午7.50.52.png

四、如何使用结构体和类

结构体和类的存储位置不同,结构体存储在栈(stack)上,而类存储在堆(heap)上,因此在创建和销毁上,类消耗的时间和内存会比较多,同时因为类是引用类型,在赋值时会有引用计数,这样也会造成消耗成本。因此,创建结构体会比创建类所消耗的时间少,使用的内存也会比较少。

当我们想要建立一个新的类型的时候,怎么决定用值类型还是引用类型呢?当你使用 Cocoa 框架的时候,很多 API 都要通过 NSObject 的子类使用,所以这时候必须要用到引用类型 class。在其他情况下,有下面几个准则:

  • 什么时候该用值类型:
    • 要用==运算符来比较实例的数据时
    • 你希望那个实例的拷贝能保持独立的状态时
    • 数据会被多个线程使用时
  • 什么时候该用引用类型(class):
    • 要用==运算符来比较实例身份的时候
    • 你希望有创建一个共享的、可变对象的时候

五、类的生命周期

Swift的编译过程

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

  • OC通过clang编译器,编译成IR,然后再生成可执行文件.o(这里也就是我们的机器码) 
  • Swift则是通过Swift编译器编译成IR,然后在生成可执行文件。 16407056873309.jpeg 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文件分析

生成SIL文件

我们先在工程里面输入实例代码

class ClassTeacher{
    var age: Int = 10
    var name: String = "FY"
    
}
var t = ClassTeacher()

然后我们在工程里面输入生成SIL文件命令

 swiftc -emit-sil  ${SRCROOT}/SwiftTest/main.swift > ./main.sil && open main.sil

在工程里面执行就会生成SIL文件 截屏2022-01-28 下午11.11.32.png 生成的SIL文件如下: 截屏2022-01-28 下午11.15.43.png

解析SIL文件

现在我们根据上面的SIL文件具体分析一下SIL中间语言。因为代码比较多,我这边针对main函数来进行解析。

首先是

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

其中@_hasStorage@_hasInitialValue这两个是标识符,说明LGTeacher类里面的agename是一个拥有了初始值的存储属性。LGTeacher类里面还有deinitinit方法。

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4main1tAA9LGTeacherCvp          // id: %2
  %3 = global_addr @$s4main1tAA9LGTeacherCvp : $*LGTeacher // user: %7
  %4 = metatype $@thick LGTeacher.Type            // user: %6
  // function_ref LGTeacher.__allocating_init()
  %5 = function_ref @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher // user: %7
  store %6 to %3 : $*LGTeacher                    // id: %7
  %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'

接下来我们看@main函数,这个函数是一个入口函数。

像%0、 %1、%2是虚拟寄存器,一旦赋值就不可以更改,所以值会一直累加。

我们接下来看alloc_global @$s4main1tAA9LGTeacherCvp,分配一个全局变量。而这个变量名s4main1tAA9LGTeacherCvp是经过混写的变量名。我们可以通过xcrun swift-demangle命令可以得到实际名称main.t: main. LGTeacher 截屏2022-01-28 下午11.35.54.png %3 = global_addr @$s4main1tAA9LGTeacherCvp : $*LGTeacher 这个命令是把这个全局变量的内 存地址给保存下来。

%4 = metatype $@thick LGTeacher.Type这个命令是获取LGTeacher.Type的元类型。

%5 = function_ref @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher这个命令是获取LGTeacher.__allocating_init()这个函数的指针地址。

%6 = apply %5(%4) : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher 这个命令是调用了LGTeacher.__allocating_init()函数,以LGTeacher.Type的元类型作为参数,然后把函数返回值存储到%6的寄存器中,也就是创建了LGTeacher类的实例对象。

store %6 to %3 : $*LGTeacher这个命令是把实例对象的内存地址存到了全局变量的地址中。

%8 = integer_literal $Builtin.Int32, 0          // user: %9
%9 = struct $Int32 (%8 : $Builtin.Int32)        // user: %10

这两个命令是在构建一个Int32类型的变量,并赋值0。在Swift中Int类型是一个结构体。

return %9 : $Int32 这个是return 0,也就是在OC中main函数里面经常看到的结束标志。

接下来我们分析一下LGTeacher类的构建函数__allocating_init()

// LGTeacher.__allocating_init()
sil hidden [exact_self_class] @$s4main9LGTeacherCACycfC : $@convention(method) (@thick LGTeacher.Type) -> @owned LGTeacher {
// %0 "$metatype"
bb0(%0 : $@thick LGTeacher.Type):
  %1 = alloc_ref $LGTeacher                       // user: %3
  // function_ref LGTeacher.init()
  %2 = function_ref @$s4main9LGTeacherCACycfc : $@convention(method) (@owned LGTeacher) -> @owned LGTeacher // user: %3
  %3 = apply %2(%1) : $@convention(method) (@owned LGTeacher) -> @owned LGTeacher // user: %4
  return %3 : $LGTeacher                          // id: %4
} // end sil function '$s4main9LGTeacherCACycfC'

首先,我们要先获取LGTeacher的元类型metatype,这个元类型可以理解为OC中的isa指针。 接下来我们要alloc_ref $LGTeacher,这个alloc_ref我们可以通过查看官方文档可知这个函数用来申请内存空间。

Allocates an object of reference type T. The object will be initialized with retain count 1;
its state will be otherwise uninitialized. 
The optional objc attribute indicates that the object should be allocated using Objective-C's allocation methods (+allocWithZone:).

这个函数在Swift中通过调用swift_allocObject来申请内存,而在OC中则使用+allocWithZone:

最后通过调用LGTeacher.init()方法,生成一个实例对象,并作为返回值返回出来。

分析类实例变量创建过程。

首先我们先创建一个纯Swift类

class LGTeacher {
    var age: Int = 18
    var name: String = "FUYU"
}
var t = LGTeacher()

运行起来后我们先把断点打在__allocating_init函数上 截屏2022-01-29 下午9.54.23.png 深入到__allocating_init函数里面我们会发现调用了swift_allocObjectinit两个方法。 截屏2022-01-29 下午10.04.25.png 我们再把这个LGTeacher类修改成继承NSObject

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

进入到__allocating_init函数里面我们会发现这次调用了OC里面的objc_allocWithZoneobjc_msgSend方法。 截屏2022-01-29 下午10.09.34.png 接下来我们从Swift源码里面去寻找实例变量是怎么创建的。 首先,我们先去从HeapObject.cpp 文件中搜索swift_allocObject 函数,可以看到内部调用了 swift_slowAlloc 函数 截屏2022-01-29 下午10.17.11.png 进入swift_slowAlloc函数我们可以看到调用了malloc 截屏2022-01-29 下午10.19.55.png 由此可以得出swift对象创建时经过如下的过程分配内存的

__allocating_init -> swift_allocObject -> _swift_allocObject_ -> swift_slowAlloc -> malloc

从刚才的对象创建分析过程中,我们可以看到Swift的对象内存结构是HeapObject。而在HeapObject中,我们可以看到有两个属性,一个是metadata,一个是refCounts。默认占了16个字节大小。 截屏2022-01-29 下午10.27.23.png

Swift类结构分析。

在OC中类的只有一个isa指针,但是在swift中有 metadatarefCounts两个属性

  • metadata结构分析 通过查看HeapObject源码,我们知道metadataHeapMetadata类型,而这是TargetHeapMetadata定义的别名。 截屏2022-01-29 下午10.36.30.png 我们接下来查看TargetHeapMetadata的源码,通过TargetHeapMetadata的初始化代码我们可以知道,如果是一个纯Swift类,那么会传入一个MetadataKind类型的kind参数。而如果是OC交互的类,则传入的是isa截屏2022-01-29 下午10.37.52.png 我们继续看一下MetadataKind类型的代码,发现它是一个uint32_t类型 截屏2022-01-29 下午10.47.07.png 在Swift中,MetadataKind类型有多个定义,如下所示 16407858875605.jpeg
  • 类的metadata结构猜想 我们从TargetHeapMetadata类的源码中可以知道,TargetHeapMetadata继承自TargetMetadata,因此我们可以知道MetaData最终继承自TargetMetadata。我们从TargetMetadata源码中去寻找TargetMetadata的创建函数。函数如下: 截屏2022-01-29 下午10.57.32.png 通过这个函数我们可以看到,通过Kind属性的不同类型,TargetMetadata会转成其它类型的Metadata。因此TargetMetadata是所有类型的元类型的基类。

通过以上函数方法,我们可以知道,当kind是Class类型时,TargetMetadata被转换成TargetClassMetadata类,因此,我们可以通过TargetClassMetadata类去寻找类的MetaData结构。

查看TargetClassMetadata类的源码,我们看到这个类继承了TargetAnyClassMetadata类,而TargetAnyClassMetadata又继承了TargetHeapMetadata类。 截屏2022-01-29 下午11.06.54.png 截屏2022-01-29 下午11.11.58.png 截屏2022-01-29 下午11.09.13.png 截屏2022-01-29 下午11.09.26.png 通过源码分析,我们可以得到类的MetaData属性的继承链。即TargetClassMetadata -> TargetAnyClassMetadata -> TargetHeapMetadata -> TargetMetadata 同时,我们通过查看这些类的属性我们可以得到Swift类的MetaData的数据结构

struct Metadatavar kind: Int 
    var superClass: Any.Type 
    var cacheData: (Int, Intvar 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

所以我们自定义一个结构体用来将我们的实例对象强转为该类型,如果转换成功则说明分析正确。代码如下:

截屏2022-01-29 下午11.28.49.png 截屏2022-01-29 下午11.29.11.png 转换结果成功,说明我们的分析正确。

接下来我们验证metadata的数据结构是否正确 截屏2022-01-29 下午11.34.50.png 截屏2022-01-29 下午11.35.04.png 从以上结果显示,类的数据结构正确。