Swift 类与结构体
目录
一、初识类和结构体
// 类
class PersonClass : NSObject {
var name: string
var age: Int
init(age: Int, name: string) {}
// 析构函数
deinit {}
}
// 结构体
struct PersonStruct {
var name: string
var age: Int
init(age: Int, name: string) {}
}
1.1 Class(引用类型)
var p = PersonClass(age: 10, name: "张三")
var p1 = p
类不管在栈、堆、全局数据区等定义,所定义的类存储的数据就一定在堆空间,而指针变量的内存是存在相应的栈、堆、全局数据区
po: p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果
的引用名。
x/8g: 读取内存中的值(8g: 8字节格式输出)
1.2 Struct(值类型)
var p = PersonStruct(age: 10, name: "张三")
var p1 = p
结构体在栈、堆、全局数据区等定义,所定义的结构体存储的数据也是存在相应的栈、堆、全局数据区
1.3 内存分布
引用类型 与 值类型内存分布位置不同:一般情况,值类型分布在栈上,引用类型分布在堆上。
Stack(栈区):局部变量和函数运行过程中的上下文(函数)
Heap(堆区):存储所有对象
Global(全局区):存储全局变量
常量区:字符串 & 常量
代码区:存储指令
Mach-O 文件有多个段(Segment),每个段有不同的功能。然后每 个段又分为很多小的 Section
TEXT.text : 机器码
TEXT.cstring : 硬编码的字符串
TEXT.const: 初始化过的常量
DATA.data: 初始化过的可变的(静态/全局)数据
DATA.const: 没有初始化过的常量
DATA.bss: 没有初始化的(静态/全局)变量
DATA.common: 没有初始化过的符号声明
二、结构体 & 类初始化器
2.1 结构体(Struct)
结构体初始化器:编译器会根据情况,会为结构体生成多个初始化器,宗旨是保证所有成员都有初始值
// 结构体
struct Point {
var x: Int
var y: Int
}
var p1 = Point(x: 10, y: 10) ✅
var p2 = Point(x: 10) ❎
var p3 = Point(y: 10)❎
var p4 = Point()❎
struct Point {
var x: Int = 0
var y: Int
}
var p1 = Point(x: 10, y: 10) ✅
var p2 = Point(x: 10) ❎
var p3 = Point(y: 10) ✅
var p4 = Point()❎
struct Point {
var x: Int = 0
var y: Int = 0
}
var p1 = Point(x: 10, y: 10) ✅
var p2 = Point(x: 10) ✅
var p3 = Point(y: 10) ✅
var p4 = Point()✅
// 可选项有个默认的值nil
struct Point {
var x: Int?
var y: Int?
}
var p1 = Point(x: 10, y: 10) ✅
var p2 = Point(x: 10) ✅
var p3 = Point(y: 10) ✅
var p4 = Point()✅
结构体自定义初始化器:一旦在定义结构体时自定义初始化器,编译器就不会再帮它自动其他生成初始化器
struct Point {
var x: Int = 0
var y: Int = 0
init(x: Int, y: Int) {
self.x = x
self.y = y
}
}
var p1 = Point(x: 10, y: 10) ✅
var p2 = Point(x: 10) ❎ // 虽然有初始值,但是编译器不会帮你生成
var p3 = Point(y: 10) ❎ // 虽然有初始值,但是编译器不会帮你生成
var p4 = Point()❎ // 虽然有初始值,但是编译器不会帮你生成
2.2 类(Class)
类初始化器:类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器
(类的所有成员都在定义的时候指定了初始值,编译器会为类生成无参的初始化器,成员的初始化是在这个初始化器中完成的)
class Point {
var x: Int = 0
var y: Int = 0
}
let p1 = Point() ✅
var p2 = Point(x: 10) ❎
var p3 = Point(y: 10) ❎
var p4 = Point(x: 10, y: 10)❎
- 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性
都要初始化完成。- 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如
果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖- 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括
同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其
它指定初始化器所覆盖。- 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例
属性的值,也不能引用 self 作为值。
可失败初始化器:意味着当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。
// 示例
class Person {
let age: Int
let name: String
init?(age: Int, name: String){
if age < 6 {
return nil
}
self.age = age
self.name = name
}
}
必要初始化器:在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须 实现该初始化器
// 示例
class Person {
let age: Int
let name: String
// 必要初始化器
required init(age: Int, name: String){
self.age = age
self.name = name
}
}
// 示例
class Student: Person {
let subjectName: String
required init(age: Int, name: String){
super.init(age: age, name: name)
self.subjectName = "语文"
}
}
2.3 对象的堆空间申请过程
swift中,在创建类的实例对象,需要向堆空间申请内存(malloc,alloc指令代表分配内存在堆空间),流程如下:
- Class.__allocating_init()
- libswiftCore.dylib:swift_allocObject
- libswiftCore.dylib:swift_slowAlloc
- libsystem_malloc.dylib:malloc
Mac,iOS中的malloc函数分配的内存大小总是16的倍数
通过class_getInstanceSize可以得知类的对象真正使用的内存大小
class Point {
var x = 11
var test = true
var y = 22
}
var p = Point()
class_getInstanceSize(type(of: p))
class_getInstanceSize(Point.self)
swift对象的内存结构 HeapObject (OC objc_object) ,有两个属性(一个是 Metadata,一个是 RefCount,默认占用 16 字节大小。),
三、结构体 & 类方法区别
Class 与 Struct 都能定义方法,但值类型属性不能被自身的实例方法修改
struct Point {
var x = 0.0, y = 0.0
func moveBy(x deltaX: Double, y deltaY: Double) {
//self - 提示值类型属性 x,y 不能被实例方法修改 ❎
x += deltaX
y += deltaY
}
}
可通过查看 SIL 文件来查看添加与否 mutating 修饰词实例方法的差异:
sil hidden [ossa] @$s4main5PointV4testyyF : $@convention(method) (Point) -> Void
debug_value %0 : $Point, let, name "self", argno 1 // id: %1
// 未使用 mutating 修饰传递的是 let point = self
sil hidden [ossa] @$s4main5PointV6moveBy1x1yySd_SdtF : $@convention(method) (@inout Point) -> Void
debug_value_addr %2 : $*Point, var, name "self", argno 3 // id: %5
// 使用 mutating 修饰传递的是 var point = &self (传递地址,并且是 var,可修改的)
异变方法的本质:对于变异方法, 传入的 self 被标记为 inout 参数。无论在 mutating 方法内部发生什么,都会影响外部依赖类型的一切。
输入输出参数:函数能够修改一个形式参数的值,而且希望这些改变在函数结束之后依然生效,那么就需要将形式参数定义为 输入输出形式参数,(inout关键字可以定义一个输入输出形式参数)
四、方法调度
OC 方法调用是采用 objc_msgSend,而Swift 类型方法调度,是采用函数表调度
class JHTest {
func test() {}
}
// 函数调用:-> metedata -> 函数地址(metedata + 偏移量)
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
}
// typeDescriptor: Class, Struct, Enum 都有自己的类描述
struct TargetClassDescriptor{
var flags: UInt32
var parent: UInt32
var name: Int32
var accessFunctionPointer: Int32
var fieldDescriptor: Int32 // 属性信息
var superClassType: Int32
var metadataNegativeSizeInWords: UInt32
var metadataPositiveSizeInWords: UInt32
var numImmediateMembers: UInt32
var numFields: UInt32
var fieldOffsetVectorOffset: UInt32
var Offset: UInt32
var size: UInt32
//V-Table(函数表)
}
方法调度总结:
类型 | 调度方式 | 拓展(extension ) |
---|---|---|
值类型 | 静态派发(编译时函数地址已确定) | 静态派发(编译时函数地址已确定) |
类 | 函数表派发 | 静态派发(猜测原因:之前类型已经确定了,如果在其他类拓展类,静态调用,不会改变原类的结构) |
NSObject 子类 | 函数表派发 | 静态派发 |
影响函数的派发方式:
- final: 添加了 final 关键字的函数无法被重写,使用静态派发,不会在 vtable 中出现,且
对 objc 运行时不可见。 - dynamic: 函数均可添加 dynamic 关键字,为非objc类和值类型的函数赋予动态性,但派发
方式还是函数表派发。 - @objc: 该关键字可以将Swift函数暴露给Objc运行时,依旧是函数表派发。
- @objc + dynamic: 消息派发的方式
- static / class:静态派发
函数内联: 是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,从而优
化性能。
- 将确保有时内联函数。这是默认行为,我们无需执行任何操作. Swift 编译器可能会自动内
联函数作为优化。 - always - 将确保始终内联函数。通过在函数前添加 @inline(__always) 来实现此行为
- never - 将确保永远不会内联函数。这可以通过在函数前添加 @inline(never) 来实现。
- 如果函数很长并且想避免增加代码段大小,请使用@inline(never)(使用@inline(never))