一、初识类与结构体
首先,我们来看一下类的定义:
class Person {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
以及结构体的定义:
struct Person {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
从代码上看,两者的区别只是关键字的不同。
那为什么苹果官方建议我们在 Swift 中尽量使用结构体而不是类呢?
那就需要深入探究一下他们的异同点:
1.相同点:
- 定义存储值的属性
- 定义方法
- 定义下标以使用下标语法提供对其值的访问
- 定义初始化器
- 使用 extension 来拓展功能
- 遵循协议来提供某种功能
2.不同点:
- 类有继承的特性,而结构体没有
- 类型转换使您能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源(deinit)
- 引用计数允许对一个类实例有多个引用
总之,我们需要清楚的了解一点:
类是引用类型,而结构体是值类型。
引用类型,意味着一个类类型的变量并不直接存储具体的实例对象。而是对当前存储具体实例内存地址的引用,即指针。
比如我们使用 Person 的类实例化一个对象:
class Person {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var person1 = Person(age: 18, name: "ZZM")
var person2 = person1
在控制台p一下person1和person2:
0x00000001007343f0
我们再使用 x/8g
这里输出的内容就是实例的值。
有人会问,那 person1 和 person2 的内存地址是一样的吗?
我们可以继续探索:
可以看出 person1 和 person2 本身的内存地址是不一样的,毕竟他们是不同的变量。
总结一下,当我们实例化一个变量 Person(age: 18, name: "ZZM") 赋值给 person1 时,
内存中开辟了一块空间用来存储这个实例的值,而把这个值所在地址的值赋值给了 person1。
person1存储的值即为 0x00000001007343f0
而 person1 本身的地址为 0x0000000100008358
这就是类类型作为引用类型在内存中的直观表现
我们再来看结构体,作为值类型在内存中是什么样的?
struct Person {
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var person1 = Person(age: 18, name: "ZZM")
var person2 = person1
在控制台 p/po 一下person1和person2:
可以看出,这里直接打印了两个变量的值,而不是内存地址。
即使这里我修改了 person1 的 age 的值,也不会影响 person2:
person1.age = 20
而如果是引用类型,这里的 person2 的 age 值也会随之改变。
此外,引用类型和值类型还有一个最直观的区别就是存储的位置不同:
一般情况,值类型存储的在栈上,引用类型存储在堆上。
(iOS中,内存主要分为栈区、堆区、全局区、常量区、代码区五大区域)
栈区(stack):栈的地址空间在iOS中是以0X7开头,存储局部变量和函数运行过程中的上下文。
堆区(Heap):存储所有对象
全局区(Global):存储全局变量,常量,代码区
Segment & Section:Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每 个段又分为很多小的 Section
TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明
struct Person {
var age = 18
var name = "ZZM"
}
func test(){
var person = Person()
print("\(person)")
}
test()
以上代码,在 print 处打断点,使用 frame varibale -L person 打印 person 以及属性的地址。
可以看到 age、name 的地址相差 8 个字节。
再使用 cat address 命令查看这个内存地址属于哪个区。
可以看到这里的实例以及属性都属于 栈区。
如果结构体的对象中包含引用类型的属性呢?该属性的内存地址存放在哪?比如:
class Person {
var age = 18
var name = "ZZM"
}
struct Teacher {
var age = 18
var name = "ZZM"
var person = Person()
}
func test(){
var teacher = Teacher()
print("\(teacher)")
}
test()
此时打印 teacher 实例对象以及属性的内存地址:
可以看到,实例对象存储在栈中,而它的引用类型的属性,存储在堆中。
通过内存的读取速度,我们可以知道,结构体的读取比类的读取读取速度要更快一些。
所以在实际编码过程中,要尽可能的使用结构体。
二、类的初始化器
类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译器会提供默认的初始化方法(前提是我们自己没有指定初始化器)
1.指定初始化器
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
2.便捷初始化器
- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
3.可失败初始化器
- 意味着当前因为参数的不合法或者外部条件 的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句, 来表明可失败初始化器在何种情况下会触发初始化失败。
4.必要初始化器
- 在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须实现该初始化器
注意事项:
初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例
属性的值,也不能引用 self 作为值。
三、类的生命周期
iOS开发的语言不管是OC还是Swift,后端都是通过LLVM进行编译的。
OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
Swift 则是通过 Swift 编译器编译成 IR,然后再生成可执行文件。
图中命令解释:
// 分析输出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
对于 OC 的代码:
int8_t x = 100;
int8_t y = x + 100;
NSLog(@"%d", y);
这段代码在编译过程中是不会出错的,但是打印的结果是 -56,很显然是错误的。
而对于 Swift 代码:
let x = Int8(100) + 100
在编译就会报错 Arithmetic operation '100 + 100' (on type 'Int8') results in an overflow
这就是 SIL(Swift Intermediate Language)的作用。
通过对SIL 文件以及 Swift 源码的探索,最终会发现 Swift 对象内存分配的过程如下:
且 Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小。