一.初始类与结构体
了解类与结构体的异同点
class GQTeacher {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
// 上面代码的class 换成struct , 那我们就定义了一个结构体
struct GQTeacher {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
结构体和类的主要共同点有:
定义存储值的属性
定义方法
定义下标以使用下标语法提供对其值的访问
定义初始化器
使用extension来拓展功能
遵循协议来提供某种功能
结构体和类的不同点有:
类有继承的特性,而结构体没有
类型转换使您能够在运行时检查和解释实例的类型
类有析构函数(deinit())用来释放分配的资源
引用计数允许对一个类实例有多个引用
对于类与结构体我们需要区分的第一件事就是:
类是引用类型.也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用.
var t1 = t // t1 和 t 都是对类对象内存地址的引用
体现在具体的应用程序当中,我们可以应用LLDB的命令查看:
po: p 和 po 的区别在于使用po只会输出对应的值,而 p 则会返回值的类型以及命令结果的引用名
x/8g : 读取内存中的值(8g: 8字节格式输出)
变量类型的地址也可以通过po的命令,查看 t , t1 的地址可以通过 po withUnsafePointer(to: &t){print($0)}
查看变量t1的地址就把 &t 换成 & t1 (指针后面会继续讲解,在此只做了解)
swift中有引用类型,就有值类型,最典型的就是Struct,结构体的定义也非常简单,相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值).
struct Teacher {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
用LLDB的命令po打印出来就是变量的值
其实引用类型就相当于在线的Excel,当我们把这个链接共享给别人的时候,别人的修改我们是能够看到的;
值类型就相当于本地的Excel,当我们把本地的Excel传递给别人的时候,就相当于重新复制了一份给别人,至于他们对于内容的修改我们是无法感知的
另外引用类型和值类型还有一个最直观的区别就是存储的位置不同:一般情况,值类型存储在栈上,引用类型存储在堆上.
对内存区域基本概念的认知,请看下图
栈区(stack): 局部变量和函数运行过程中的上下文
// test()是一个函数, 其实函数也是存放在内存中的栈区的
func test() {
// 在函数内部声明的age变量是一个局部变量
var age: Int = 10
print(age);
}
// 如果此时调用test()函数, 那么局部变量(age)一定栈区, 其实函数也是存储在栈区
test()
堆区(Heap): 存储所有对象
全局区(Global):存储全局变量;常量;代码区
Segment & Section: Mach-O 文件有多个段(Segment),每个段有不同的功能.然后每个段又分为很多小的Section.
TEXT.text : 机器码
TEXT.cstring: 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据
DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明
例子
struct Teacher {
var age: Int
var name: String
}
func test(){
var t = Teacher(age: 18, name: "lgq")
print("end")
}
test()
接下来使用命令
frame varibale -L xxx
结构体在内存中是存储在栈内存中的.
如果其他条件不变, 将struct修改成class,此时,在类实例的初始化过程中,编译器会在堆内存空间上寻找合适的内存区域,找到之后, 把内存区域的地址返回, 把value的值拷贝到该地址对应的堆区的内存当中,并且把当前栈上的内存地址指向该堆区.如此, 就完成了实例对象的内存分配. 当变量离开了作用域,那么堆内存销毁,它是查找并且把内存快重新插入到堆空间当中.那就意味着对于堆内存, 它始终要有一个查找的过程. 与此同时,在销毁栈上的指针.
栈和堆最主要的区别在分配内存的过程中会有时间和速度上的区别.
这里可以通过GitHub上StructVsClassPerformance这个案例直观测试当前结构体和类的时间分配.
二.类的初始化器
了解类初始化器的一些规则,以便于我们写出比较swift(高效)的代码
swift是类型安全的语言,在类的创建过程中,必须确保所有的成员属性都是有值的.
class Person {
var age: Int
var name: String
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
}
需要注意的是, 当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)
swift中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值.所以类Teacher必须要提供对应的指定初始化器,同时我们也可以为当前的类提供<u style="box-sizing: border-box;">便捷初始化器</u>(<u style="box-sizing: border-box;">注意:便捷初始化器必须从相同的类里调用另一个初始化器</u>)
class Person {
var age: Int
var name: String
init(_ age: Int, _ name: String) {
self.age = age
self.name = name
}
convenience init(_ age: Int) {
self.init(18, "lgq")
self.age = age
}
convenience init() {
self.init(18, "lgq")
}
}
注意: 在便捷初始化器中,在调用self.age等属性之前必须保证self极其属性已经初始化了!这些规则都是为了访问属性是安全的.
记住:
指定初始化器必须保证在向上委托给父类初始化器之前,其所在父类引入的所有属性都要初始化完成.
指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值.如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖.
便捷初始化器必须先委托同类中的其他初始化器,然后再为任意属性赋新值(包括同类里定义的属性).如果没这么做,便捷初始化器赋予的新值将被自己类中其他初始化器所覆盖.
初始化器在第一阶段初始化完成之前,不能调用任何实例方法,不能读取任何实例属性的值,也不能引用self作为值.
class Teacher : Person {
var subjectName: String
init(_ subjectName: String) {
self.subjectName = subjectName
super.init(18, "lgq")
}
}
可失败初始化器: 也就意味着当前因为参数不合法或者外部条件的不满足,存在初始化失败的情况.这种swift中可失败初始化器写return nil 语句来表明可失败初始化器在何种情况下会触发初始化失败.
class Person {
var age: Int
var name: String
init?(age: Int, name: String) {
if age < 18 {return nil}
self.age = age
self.name = name
}
convenience init?() {
self.init(age: 18, name: "lgq")
}
}
必要初始化器:在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器.
三.类的生命周期
主要是对当前类在内存结构以及数据结构的探索,在其中会看swift的源码结合当前的SIL去理解swift背后发生的故事.
iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:
OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。
SIL (Swift Intermediate Language) 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
切换项目路径,使用下面命令
swiftc -emit-sil main.swift > ./main.sil
此时就会把main.swift编译成main.sil的中间代码, 打开中间代码:
class Teacher {// 类 @_hasStorage @_hasInitialValue var age: Int { get set } // 初始化过的存储属性 @_hasStorage @_hasInitialValue var name: String { get set } //初始化过的存储属性 @objc deinit// 标识了@objc的deinit函数 init() // 默认的init函数 } // 以上代码就是Teacher在SIL的声明
查看源码相关:
@main: 入口函数, 一般的,SIL会以@作为标识
%0: 寄存器,可以理解为日常开发中的常量,一旦赋值之后不能修改,这个寄存器是虚拟的.
如果遇到混写后的名称,则使用如下命令可以还原 :
xcrun swift-demangle 名称
此源码需要分析的是类的初始化流程:
如果是swift类, 里面调用了swift_allocObject()函数
Swift对象内存分配:
- __alloccating_init --> swift_allocobject --> swift_allocObject --> swift_slowAlloc --> malloc
- Swift对象的内存结构 HeapObject(OC objc_object),有两个属性:一个是Metadata,一个是RefCount,默认占用16字节大小.
(如果是类继承了NSObject , 则是调用了objc_allocWithZone()函数)
经过swift源码分析,我们得出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 }