1、类和结构体
1.1 基础认知
类和结构体十分相似,如:
class/struct Person{
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
相同点:
- 定义存储值的属性、方法、初始化器、以及下标以使用下标语法提供对其的访问
- 使用extension来扩展功能
- 遵循协议来提供功能
不同点:
- 类能继承,结构体不行
- 类型转换使你能够在运行时检查和解释类实例的类型
- 类有析构函数用来释放其分配的资源
- 引用计数允许对一个类实例有多个引用
1.2 引用类型和值类型
引用类型:即一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用
值类型:相比较类类型的变量中存储的是地址,那么值类型存储的就是具体的实例
- 要区分的第一件事就是:类是引用类型、结构体是值类型,可以借助lldb的po和x/8g指令来查看当前的变量的内存结构
po:p和po的区别在于使用po只会输出对应的值,而p则会返回值的类型以及命令结果的引用名。
x/8g:读取内存中的值(8g:8字节格式的输出)
我们可以通过运行以下代码来查看下内存结构:
class/struct Person{
var age: Int
var name: String
init(age: Int, name: String) {
self.age = age
self.name = name
}
}
var person = Person(age: 10, name: "小明")
var person_1 = person
结果1:person和person_1存放了相同的内存地址
结果2:person和person_1存放了相同的结构体实例
- 要区分的第二件事:引用类型和值类型的存储位置不同:一般情况下,值类型存储在栈上,引用类型存储在堆上
内存区域的一些基本认知
1、内存的排列是从高到低
2、一般只有从0x00000001到0x000007F之间的内存才是我们可以操作的区域
3、一般我们把操作区域分为Stack(栈 - 存放的是局部变量和函数运行过程中的上下文),Heap(堆 - 存储所有对象),Global(全局区 + 常量区 + 指令区 - 存储全局变量 + 常量 + 代码区)
4、当栈和堆拉伸后导致内存地址重合,即溢出
5、内存分配栈空间比堆空间快,因为堆空间需要查找合适的区域再拷贝值,完成实例对象分配,离开作用域后销毁查找并把内存块插入,而栈空间每次会直接分配新的空间,离开作用域直接回滚销毁。简单来说就是堆始终需要寻找,而栈不用
所以鉴于堆在时间和效率上略差,应将class尽量用struct替换,也就是尽量用值类型替换引用类型
1.3 class和struct的内存分布
已知class是在堆,struct在栈,那么具体内存分布又会是如何,借助frame variable命令进行查看
frame varibale -L xxx 查看变量内存地址
-
class:
通过打印结果可以知道类的内存分布是比较复杂的,首先8字节存的是指向类型信息,第二个8字节是引用计数,然后才是属性信息,p变量仍在堆上,只是存的栈地址
内存存值 | 说明 |
---|---|
0x0000000100008230 | 指向类型信息(Metadata) |
3 | 引用计数(RefCount) |
10 | person.age |
小明 | person.name |
0x0000000100617338 | person.p |
-
struct:
通过打印结果可知结构体的内存分布是比较简单的,是直接分布在栈上,即使结构体内部含有引用类型,p变量仍然在栈上,只是存的是person这个对象的堆空间地址,也就是说
内存存值 | 说明 |
---|---|
10 | person.age |
小明 | person.name |
0x0000000109c04110 | person.p |
2、类的初始化器
class Person {
var name: String
var age: Int
// 指定初始化器 (Designated Initializer) + 可失败 ?
init?(name: String, age: Int, minAge: Int) {
if minAge > age {
return nil
}
self.name = name
self.age = age
}
// 指定初始化器 (Designated Initializer) + 必需
required init(name: String, age: Int) {
self.name = name
self.age = age
}
// 便捷初始化器 (Convenience Initializer) + 必需 + 可失败
convenience required init?(name: String) {
let defaultAge: Int = 8
self.init(name: name, age: defaultAge)
}
// 便捷初始化器 (Convenience Initializer)
convenience init(age: Int) {
let defaultName: String = "小明"
self.init(name: defaultName, age: age)
}
}
class People : Person{
var address : String
override init?(name: String, age: Int, minAge: Int) {
//指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成
self.address = "北京"
super.init(name: name, age: age, minAge: minAge)
//指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值
self.name = "小红"
}
convenience required init?(name: String) {
//required 必需继承,便捷初始化器必须先委托同类中的其它初始化器
self.init(name: name, age: 8, minAge: 1)
}
required init(name: String, age: Int) {
self.address = "北京"
super.init(name: name, age: age)
}
}
- 当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译 器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!
- 创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值!
1、指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
2、指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
3、便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
4、初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
- 可失败初始化器:就意味着当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句(init?())
- 必要初始化器:在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须 实现该初始化器(requeired init())
3、类的生命周期和数据结构
- OC和Swift均是通过LLVM进行编译的
OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器码)
Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。
- Swift 对象内存分配:
__allocating_init -> swift_allocObject -> swift_allocObject -> swift_slowAlloc -> Malloc - Swift 对象的内存结构 HeapObject (OC objc_object) ,有两个属性: 一个是Metadata ,一个是 RefCount ,默认占用 16 字节大小。
- 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
}