Swift 进阶 01: 类与结构体 (上)

322 阅读6分钟

一、初始类与结构体

class (类) && struct (结构体)

Class:

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

Struct:

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

结构体和类的主要相同

  1. 定义存储值的属性
  2. 定义方法
  3. 定义下标以使用下标语法提供对其值的访问
  4. 定义初始化器
  5. 使用 extension 来拓展功能
  6. 遵循协议来提供某种功能

主要的不同点

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

类是引用类型

  • 类是引用类型。也就意味着一个类类型的变量并不直接存储具体的实例对象,是对当前存储具体实例内存地址的引用。
class LGTeacher{     
    var age: Int     
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
} 
var t = LGTeacher.init(age: 18, name: "joker") 
var t1 = t

截屏2021-12-25 17.21.59.png 这里可借助两个指令来查看当前变量的内存结构

截屏2021-12-25 17.24.40.png

po : p 和 po 的区别在于使用 po 只会输出对应的值,而 p 则会返回值的类型以及命令结果 的引用名。
x/8g: 读取内存中的值(8g: 8字节格式输出)

Struct是值类型

值类型,最典型的就是 Struct ,相比较 类类型的变量中存储的是地址,那么值类型存储的就是具体的实例(或者说具体的值)。

具体实例如下:

struct LGTeacher{
    var age: Int
    var name: String
}
var s = LGTeacher.init(age: 18, name: "joker")
var s1 = s

使用lldb命令查看地址信息

截屏2021-12-25 17.39.58.png

截屏2021-12-25 17.38.44.png

引用类型(class)和值类型总结(struct)

  1. 引用类型就相当于在线的 Excel ,这个链接共享给别人的时候,别人的修改我们 是能够看到的
  2. 值类型就相当于本地的 Excel ,当我们把本地的 Excel 传递给别人的时候,就相当于重新复制了一份给别人,
  3. 存储的位置不同: 一般情况,值类型存储的在栈(Stack)上,引用类型存储在堆(Heap)上;读写速度:栈(Stack) > 堆(Heap)

开发过程中,对于不需要继承的实体,优先选择使用结构体(struct)

内存区域的基本概念

截屏2021-12-25 17.52.35.png

  1. 栈区(stack): 局部变量和函数运行过程中的上下文
  2. Heap: 存储所有对象
  3. Global: 存储全局变量;常量;代码区
  4. Segment & Section: Mach-O 文件有多个段( Segment ),每个段有不同的功能。然后每 个段又分为很多小的 Section
    • TEXT.text : 机器码

    • TEXT.cstring : 硬编码的字符串

    • TEXT.const: 初始化过的常量

    • DATA.data: 初始化过的可变的(静态/全局)数据 DATA.const: 没有初始化过的常量

    • DATA.bss: 没有初始化的(静态/全局)变量

    • DATA.common: 没有初始化过的符号声明

栈区(stack)实例查看

class LGPerson {
    var age = 18
    var name = "joker"
}
struct LGTeacher{
    var age = 18
    var name = "Kody"
    var p = LGPerson()
}
func test(){
    var t = LGTeacher()
    print("end")
}
test()

LLDB调试 使用命令

frame varibale -L xxx

截屏2021-12-25 23.49.35.png

使用cat address 0xxxxxxxxx 可查看当前是属于哪个区位置。

cat address xxxx 命令 LLDB 插件下载: libLgCatAddress.dylib

  • t:在栈(Stack)上
  • t中的p:在堆(Heap)上

堆区(Heap)实例查看

class LGTeacher{
    var age = 18
    var name = "Kody"
}
func test(){
    // 栈上:8字节
    // 堆空间上寻找合适的内存区域
    // value -> 堆
    // 栈上的内存地址指向堆区
    var t = LGTeacher()
    print("end")
}
test()

LLDB调试 截屏2021-12-26 14.25.39.png

  1. 栈上:开辟8字节
  2. 堆空间上寻找合适的内存区域
  3. value -> 堆
  4. 栈上的内存地址指向堆区

可通过github上StructVsClassPerformance这个案例来直观的测试当前结构体和类的时间分配。

二、类的初始化器

当前的类编译器默认不会自动提供成员初始化器,但是对于结构体来说编译 器会提供默认的初始化方法(前提是我们自己没有指定初始化器)!

Swift 中创建类和结构体的实例时必须为所有的存储属性设置一个合适的初始值。

结构体直接写属性,不用写init初始化器方法

编译器会提供默认的初始化方法

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

var t = LGTeacher.init(age: 18, name: "joker")

类的初始化器

1. 必须要提供对应的指定初始化器: init()

class LGTeacher{
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
}
var t = LGTeacher.init(age: 18, name: "joker")

2. 便捷初始化器: convenience init()

便捷初始化器调用

class LGTeacher{
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    
    convenience init() {
        self.init(age: 18, name: "joker")
    }
    convenience init(_ age: Int) {
        self.init(age: age, name: "joker")
    }
    convenience init(_ name: String) {
        self.init(age: 18, name: "joker")
    }
}

var t = LGTeacher.init();

注意:便捷初始化器必须从相同的类里调用另一个初始化器。

使用便捷初始化器时给变量赋值时,需放在调用另一个初始化器之后;未初始化当前对象,访问不存在!

截屏2021-12-26 15.27.25.png

使用便捷初始化器时,含有派生子类时需要注意以下几点:

  1. 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成。
  2. 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
  3. 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括 同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖。
  4. 初始化器在第一阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例属性的值,也不能引用 self 作为值。
class LGPerson{
    var age: Int
    var name: String
    init(age: Int, name: String) {
        self.age = age
        self.name = name
    }
    convenience init(_ age: Int) {
        self.init(age: age, name: "joker")
    }
}
//成员变量访问安全
class LGTeacher: LGPerson {
    var subjectName: String
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(age: 18, name: "joker")
    }
}

3. 可失败初始化器

前因为参数的不合法或者外部条件 的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句, 来表明可失败初始化器在何种情况下会触发初始化失败。

class LGPerson{
    var age: Int
    var name: String
    init?(age: Int, name: String) {
        if age < 18 { return nil}
        self.age = age
        self.name = name
    }
    convenience init?(_ age: Int) {
        self.init(age: age, name: "joker")
    }
}

条件age 小于18,返回nil,为失败初始化器

4. 必要初始化器

在类的初始化器前添加 required 修饰符来表明所有该类的子类都必须 实现该初始化器

class LGPerson{
    var age: Int
    var name: String
    required init(age: Int, name: String) 
        self.age = age
        self.name = name
    }
    convenience init(_ age: Int) {
        self.init(age: age, name: "joker")
    }
}
//成员变量访问安全
class LGTeacher: LGPerson {
    var subjectName: String
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(age: 18, name: "joker")
    }
    required init(age: Int, name: String) {
        fatalError("init(age:name:) has not been implemented")
    }
}

三、类的生命周期

iOS开发语言OC和Swift后端都是通过LLVM编译的。

截屏2021-12-26 16.06.04.png

  1. OC 通过 clang 编译器,编译成 IR,然后再生成可执行文件 .o(这里也就是我们的机器 码)
  2. Swift 则是通过 Swift 编译器编译成 IR,然后在生成可执行文件。

截屏2021-12-26 16.08.21.png

// 分析输出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 源码分析:

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

截屏2021-12-26 16.29.36.png

  1. @main: 入口函数
  2. %0/%1/%2: 寄存器,虚拟的
  3. alloc_global: 分配全局变量
  4. xcrun swift-demangle: 还原混淆的名称 截屏2021-12-26 16.34.12.png
  5. %3 = global_addr @$s4main1tAA9LGTeacherCvp : 全局变量地址 -> %3
  6. %4 = metatype $@thick LGTeacher.Type: 获取LGTeacher的元类型
  7. %5 = function_ref @$s4main9LGTeacherCACycfC : 函数引用,拿到function_refLGTeacher. __allocating_init() 函数的指针地址
  8. %6 = apply %5(%4) : $@convention(method): 把结果值给%6
  9. store %6 to %3 : $*LGTeacher : 把当前的%6 存储到 %3

swift 源码对象内存分配:

  • _ _allocating_init --> swift_allocObject --> _swift_allocObject_ --> swift_slowAlloc --> Malloc
  • Swift 对象的内存结构 HeapObject (OC objc_object{isa}) ,有两个属性: 一个是 Metadata ,一个是 RefCount ,默认占用 16 字节大小。

Metadata 结构:

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
}

通过类的数据结构查看 => Metadata 结构

struct HeapObject{
    var metadata: UnsafeRawPointer
    var refcounted1: UInt32
    var refcounted2: UInt32
}
class LGTeacher{
  var age: Int = 18
  var name: String = "Joker"
}
var t = LGTeacher()
// 获取当前实例对象的指针
let objcRawPtr = Unmanaged.passUnretained(t as AnyObject).toOpaque()
// 绑定成为 HeapObject 类型
let objcPtr = objcRawPtr.bindMemory(to: HeapObject.self
    , capacity: 1)
// 访问当前的指针 (Swift固定语法)
print(objcPtr.pointee)

// objcPtr.pointee.metadata  绑定Metadata ->  获取数据结构信息
// MemoryLayout<Metadata>.stride 测量数据指针大小
let metadata = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride).pointee
print(metadata)
print("end")

输出结果为:

截屏2021-12-26 17.47.29.png