Swift:类与结构体

2,793 阅读16分钟

前言

随着整个IT行业的越来越内卷,在不得已的情况下,需要更新自己的知识库了。如果你继续想从事iOS开发,那么Swift是你必须要掌握的。话不多说,让我们一起走进Swift的世界

准备工作

初识类与结构体

OC中的类和结构体大家已经很熟悉了,但是Swift中可能还是很陌生。下面和大家一起探究下Swift中的类和结构体

Swift中类的定义

class LWPerson{
     //属性
    var age  = 20;
    var name = "LW"
    //成员初始化器
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
    //默认初始化器系统自动提供可以不写
    init(){     
    }
}

代码中,创建了一个LWPerson类,LWPerson类中有两个属性agename,一个成员初始化器init(_ age: Int,_ name: String)和一个默认初始化器init()。默认初始化器是系统提供的。 声明类的关键字是class

Swift中结构体的定义

结构体的定义是通过struct关键字来声明的

struct WTeacher{
     //属性
    var age  = 20;
    var name = "LW"
    //成员初始化器
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
    //默认初始化器系统自动提供可以不写
    init(){     
    }
}

结构体的定义和类定义的很相似,它们在Swift中的重要性也是差不多的,那么类和结构有什么异同点的

类和结构体的异同点

类和结构体相同点

  • 定义存储属性
  • 定义方法
  • 定义下标以使用下标语法提供对其值的访问
  • 定义初始化器
  • 使用extension来扩展功能
  • 遵循协议来实现某种功能

下面以结构体为例来通过代码来实现上面的相同点,代码如下

 //协议
protocol testProtocol {
    func testFunc()
}

struct LWPerson {
    //定义存储属性
    var age = 20
    var name = "LW"
    
    //定义方法
    func personFunc(){
        print("定义方法")
    }
    //定义下标以使用下标语法提供对其值的访问
    subscript(index:Int) -> Any {
        set {
            if index == 0 {
                age = newValue as! Int
            }else{
                name = newValue as! String
            }
        }
        get{
            if index == 0 {
               return age
            }else{
               return name
            }
        }
    }
    //定义初始化器
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
}

//使用`extension`来扩展功能
//遵循协议来实现某种功能
extension LWPerson:testProtocol{
    //扩展的功能
    func extensionFunc(){
        print("结构体的扩展功能")
    }
    //协议实现功能
    func testFunc() {
         print("协议方法")
    }
}

func text(){
    var p = LWPerson(10, "LW")
    p[0]  = 100
    print(p[0])
    p.extensionFunc()
    p.testFunc()
}
 
text()

运行结果如下:

image.png

运行结果显示,上述的功能结构和类都可以实现

类和结构体不同点

  • 类有继承的特性,而结构体没有
  • 类型转换使您能够在运行时检查和解释类实例的类型(后面会进行详细的解释)
  • 类有析构函数用来释放其分配的资源
  • 引用计数允许对一个类实例有多个引用

下面通过代码来实现上面的不同点,代码如下

image.png

图中结果显示:类有继承特性和析构函数而结构体没有

image.png

图中结果显示:pp1p2存储的地址是相同的,而结构体修改了值以后只影响其本身。所以 类是引用类型,所以引用计数允许对一个类实例有多个引用,而结构体是值类型所以不具备此功能。 下面重点探究下引用类型和值类型的区别

引用类型

引用类型:所有的变量中保存的地址是相同的,即共享一份数据。当修改数据时,所有的变量值也会受影响。这就是大家经常说的指针拷贝即浅拷贝。在Swift中常见的引用类型有classclosure。下面通过代码来验证下

class LWTest{
    var age = 10
}

let test = LWTest()
let tTest = test
let newTest = test

lldb调试结果如下

(lldb) po test
<LWTest: 0x600000bb2000>

(lldb) po tTest
<LWTest: 0x600000bb2000>

(lldb) po newTest
<LWTest: 0x600000bb2000>

调试结果显示:实例变量testtTestnewTest存储的相同的地址0x600000bb2000,即他们指向同一块内存,由此可以得出一个简单的示意图

image.png

总结引用类型相当于在线的Excel,当我们把链接共享别人的时候,别人在线修改时候,我们可以看到,相当于修改了源数据

值类型

值类型:即每个变量保持一份数据拷贝,每个变量的值被修改时只会影响当前的变量,对其它的变量不会产生影响。这就是大家经常说的值拷贝即深拷贝。在Swift中常见的值类型有structenumtupleDouble等。下面通过代码来验证下

struct LWPerson {
    var age = 20
    var name = "LW"
}

var p = LWPerson(age: 18, name: "哈哈")
var p1 = p
p.age = 30

lldb调试结果如下

(lldb) po p 
▿ LWPerson
  - age : 30
  - name : "哈哈"

(lldb) po p1
▿ LWPerson
  - age : 18
  - name : "哈哈"

调试结果显示:变量和pp1中存的不在是实例对象地址而是具体的值,且修改p中的属性修改以后,p1中的值不会受到影响

总结值类型就就相当于本地的Excel,当我们把本地的Excel传递给别人的时候,就相当于复制了一份给别人,他们修改内容时,别人的不会受到影响

注意:引用类型值类型最直观的区别是存储位置不同,一般情况下引用类型存储在堆上,值类型存储在栈上

类和结构体的存储位置

类和结构体的存储位置单独拿出来讲探究,是因为个人感觉这个真的很重要。在探究这一问题之前,首先了解下内存区域(大家通常说的内存五区)

image.png

  • 栈区:存储局部变量和函数运行过程中的上下文
    • 栈区是一块连续的内存,一般是从高地址--> 低地址进行存储
    • 栈区一般在运行时分配,在iOS中的x86架构下以0x7开头
  • 堆区:存储所有对象
    • 堆区是不连续的内存(便于增删,不便于查询),一般是从低地址--> 高地址进行存储
    • 堆区的空间分配是动态的,在iOS中的x86架构下以0x6开头
  • 全局静态区:存储全局变量和静态变量
    • 该区是编译时分配的内存空间,在iOS中的x86架构下一般以0x1开头
    • 程序运行过程中,内存数据一直存在,程序结束后由系统释放
  • 常量区:存储常量:整型,字符串等
  • 代码区:存储程序被编译成的二进制 全局静态区,常量区和代码区,可以统称为全局区,因为栈区一般是从高地址--> 低地址进行存储,而堆区一般是从低地址--> 高地址进行存储,这样就会出现图中指出的堆栈溢出的情况

下面通过代码来探究下

class LWTest{
    var age = 10
}

struct LWPerson {
    var age = 20
    var name = "LW"
}

text()

func text(){
    var p = LWPerson(age: 18, name: "哈哈")
    
}

var lw = LWTest()
var age = 10

lldb调试结果如下

image.png

图中结果显示:结构体是值类型的一般存储在栈上,而类是引用类型的一般存储在堆上

注意:在结构体中添加一个引用类型的属性,并不会改变结构体本身的类型即还是值类型

image.png

图中结果显示:t1变量中存的是实例对象的地址,结构体还是值类型

补充知识:上面我们看到了.__DATA.__common,那么就简单介绍下Mach-O中的SegmentSection
Segment&Section:Mach-O中有多个段(Segment),每个段中又分为多个Section

  • TEXT.text: 机器码
  • TEXT.cstring : 硬编码的字符串
  • TEXT.const: 初始化过的常量
  • DATA.data: 初始化过的可变的(静态/全局)数据
  • DATA.const: 没有初始化过的常量
  • DATA.bss: 没有初始化的(静态/全局)变量
  • DATA.common: 没有初始化过的符号声明

类和结构体的选择使用

Swift开发过程中到底是使用结构体呢?还是使用类?一般情况下优先使用结构体。当然需要根据你自己项目的功能模块,能使用结构体的情况下优先使用结构体。比如封装一些简单的Model,不需要继承的某个类这种情况下就可以使用结构体。那么优先使用结构体原因是什么呢?

  • 结构体是值类型一般分配在栈区,栈区的内存是连续的,当栈指针指到要运行到需要给结构体开辟内时,它会根据结构体大小在栈上开辟一块内存空间,然后将结构体中的值拷贝到栈中,当函数执行完以后栈空间会自动回收内存自动释放,所以性能消耗低,速度快
  • 类是引用类型,内存分配是在堆区,堆区的内存是不连续的不便于查找。首先需要到堆中找到一块可用的内存,然后返回内存地址存放在堆区。当内存释放时,会根据栈区存放的内存地址去堆区查找释放。这个过程性能消耗较大,速度慢

这里我们也可以通过githubStructVsClassPerformance 这个案例来直观的测试当前结构体和类的时间分配。

image.png

运行结果显示:使用结构体的耗时要小于使用类的耗时,所以在开发过程中能使用结构体的优先使用结构体

类和结构体的初始化器

结构体初始化器

结构体的初始化器一般分为两种系统自动生成的成员初始化器和自定义初始化器,下面分别进行详细的探究

结构体默认初始化器

如果结构体中有储存属性,而且没有自定义的成员变量初始化器。那么编译时系统会自动给你生成一个成员变量初始化器。代码如下

image.png

自定义结构体初始化器

struct LWPerson {
    var age :Int
    var name :String
    
    init(_ age: Int,_ name: String){
        self.age = age
        self.name = name
    }
}
let lw = LWPerson(10, "哈哈")

如果自定义结构体初始化器,那么编译器不会在自动生成其它的初始化器。 结构体中的属性声明时可以不用赋值,但是自定义初始化器中,必须对每个属性都进行赋值,否则编译器会报错

类初始化器

编译器默认不会为类提供成员初始化器 image.png

类默认初始化器

类默认会提供一个初始化器init(){},但是的默认的初始化器不会为任何属性赋值,所以在类中的属性声明时必须要为所有的属性设置一个初始值。代码如下

class LWPerson {
    var age :Int = 10
    var name :String = "哈哈"
}

let lw = LWPerson()

自定义类初始化器

因为要给类中所有的属性设置一个合适的初始值,所以中必须要提供指定的初始化器。代码如下

 class LWPerson {
    var age :Int
    var name :String
    //指定初始化器
    init(_ age: Int,_ name:String){
        self.age = age
        self.name = name
    }
}

let lw = LWPerson(10, "哈哈")

便捷初始化器

指定的初始化器一般情况下只有一个,相当于暴露对外的接口,只能通过这个指定的初始化器去初始化所有属性,虽然指定初始化器只有一个,但是便捷初始化器可以有很多(注意的是:便捷初始化器必须从相同的类里调用另一个初始化器)代码如下

class LWPerson {
    var age :Int
    var name :String
    init(_ age: Int,_ name:String){
        self.age = age
        self.name = name
    }
    convenience init (_ age: Int){
        //self.name = "哈哈哈" //在初始化完成前对其属性赋值或者把self作为值使用是不允许的
        self.init(age, "lG")
        self.name = "哈哈哈"
    }
    convenience init (_ name: String){
        self.init(20, name)
    }
}

let lw = LWPerson.init(10)

注意:在调用self.init(age, "lG")之前对其属性赋值或者把self作为值使用是不允许的,因为此时的self还没有初始化完成

image.png 通过lldb调试在初始化器调用之前,此时self还没有完成初始化,在初始化器调用完成后self才完成初始化

  • Swift官方文档也给出了解释文档在 Initialization 中的Two-Phase Initialization部分给出了4个检测标准。其中第3点和第4点给出了详细的解释

image.png

继承关系初始化器的注意点

创建LWTeacher类继承LWPerson类,代码如下

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

class LWTeacher:LWPerson{
    var height:Float
    init (_ height:Float){
        self.height = height
        super.init(10, "啦啦")
        //这种写法是错误的,指定初始化器必须保证自己的所有属性都被初始化在委托给父类之前
        //self.height = height 
    }
}

let lw = LWTeacher.init(180.0)

LWTeacher类中的指定初始化器中,必须所有的属性都初始化完成才能委托给父类,这是为什么呢? 是为了安全,下面通过lldb调试下

image.png

  • lldb调试的结果,此时self已经初始化完成,也就意味着LWTeacher类所有的属性都已经初始化完成。 如果当前类的属性没有初始化,后面一经使用就会出现问题,不安全
  • Swift官方文档也给出了解释文档在 Initialization 中的Two-Phase Initialization部分给出了4个检测标准

image.png

如上所述,只有在知道其所有存储属性的初始状态后,才认为对象的内存已完全初始化。为了满足这个规则,指定的初始化器必须确保它自己的所有属性在它传递链之前都被初始化。

可失败初始化器

这个也非常好理解,也就意味着当前因为参数的不合法或者外部条件的不满足,存在初始化失败的情况。这种 Swift 中可失败初始化器写 return nil 语句,来表明可失败初始化器在何种情况下会触发初始化失败。写法也非常简单

 class LWPerson {
    var age :Int
    var name :String
    init?(_ age: Int,_ name:String){
        if age < 18 {return nil} // 小于18岁属于违规
        self.age = age
        self.name = name
    }
}

let lw = LWPerson(10, "哈哈")

必要初始化器

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

image.png

总结

  • 指定初始化器必须保证在向上委托给父类初始化器之前,其所在类引入的所有属性都要初始化完成
  • 指定初始化器必须先向上委托父类初始化器,然后才能为继承的属性设置新值。如果不这样做,指定初始化器赋予的新值将被父类中的初始化器所覆盖
  • 便捷初始化器必须先委托同类中的其它初始化器,然后再为任意属性赋新值(包括同类里定义的属性)。如果没这么做,便捷构初始化器赋予的新值将被自己类中其它指定初始化器所覆盖
  • 初始化器在第 一 阶段初始化完成之前,不能调用任何实例方法、不能读取任何实例 属性的值,也不能引用 self 作为值

类的生命周期

iOS开发的语言不管是OC还是Swift后端都是通过LLVM进行编译的,如下图所示:

image.png

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

Swift代码生成可执行文件的详细过程

image.png

  • Swift 源码经过-dump-parse(词法分析) 生成Parse即抽象语法树
  • Parse经过-dump-ast(语法分析)生成Sema检查语法是否正确(编译的时候如果有问题会报错)
  • Sema经过降级生成SILGen,即SIL中间代码
  • SILGen经过-emit-silgen生成Raw SIL,即原生的SIL
  • Raw SIL经过-emit-sil生成SILOpt Canonical SIL,即优化后的SIL
  • SILOpt Canonical SIL 降级生成IRGen,即IR中间代码
  • IRGen 经过-emit-ir生成IR,最后变成机器码

分析输出AST

  swiftc main.swift -dump-parse    // 分析输出AST
  
  swiftc main.swift -dump-ast      // 分析并且检查类型输出AST
  
  swiftc main.swift -emit-silgen   // 生成中间体语言(SIL),未优化

  swiftc main.swift -emit-sil      // 生成中间体语言(SIL),优化后的
  
  swiftc main.swift -emit-ir       // 生成LLVM中间体语言 (.ll文件)

  swiftc main.swift -emit-bc       // 生成LLVM中间体语言 (.bc文件)

  swiftc main.swift -emit-assembly // 生成汇编

  swiftc -o main.o main.swift      // 编译生成可执行.out文件

SIL文件分析

首先生成SIL文件,介绍两种生成SIL文件的方式

  • 终端直接显示,在终端输入swiftc main.swift -emit-sil image.png 这种终端显示的方式,如果你觉着不舒服。那么还有另一种方式直接生成.sil文件
  • 脚本生成.sil文件

image.png

下面就简单分析下main.sil文件

image.png 具体的每一行就不详细的给大家分析了,如果有疑问的可以在下面留言。 关于SIL语法规则,如果有不理解的请查看SIL官方文档

  • s4main9LGTeacherCACycfC是根据LGTeacher.__allocating_init()混淆后生成的,全局搜素s4main9LGTeacherCACycfC定位到函数实现的位置,SIL源码如下

image.png __allocating_init()主要实现两个功能:alloc_ref $LGTeacher在堆上申请开辟一块内存,然后根据元类型进行关联。 LGTeacher.init() 初始化所有的属性

alloc_ref $LGTeacher具体的功能通过 SIL官方文档 查看

image.png

普通的Swift类是通过alloc_ref在堆上申请开辟一块内存,而如果是带有Objc标识的Swift类,则是通过+allocWithZone:方法去申请开辟内存

类的初始化流程

通过上面对实例化对象SIL源码的分析,大致有一个了解。首先是申请开辟内存,然后初始化所有的属性。下面通过汇编 + Swift源码的方式去详细探究下

Swift类汇编

创建一个LGTeacher类,然后创建一个该类型的实例化对象,并打上断点,运行代码,结果如下 image.png 汇编结果和SIL分析的结果是一样的调用了LGTeacher.__allocating_init(),按住control键 + Step into 进入LGTeacher.__allocating_init()方法

image.png

  • swift_allocObject: 通过字面意思加上猜测,应该是申请开辟内存
  • init():初始化完成所有的属性

image.png

汇编里面跳转了一个汇编地址,此时没法在跟踪下去,只能去底层源码去查找,后面会进行详细探究

Swift中继承NSObject的类汇编

创建一个LGTeacher类继承于NSObject,然后创建一个该类型的实例化对象,并打上断点,运行代码,结果如下 image.png

  • objc_allocWithZoneOC的底层方法,方法的作用是申请开辟内存
  • "init":通过objc_msgSend方式进行消息发送

总结:类的初始化过程基本需要两个步骤

  • 申请开辟内存
  • 通过init方法初始化完成所有的属性

swift_allocObject探究

Swift底层源码 中全局搜索swift_allocObject,在HeapObject.cpp文件中定位到swift_allocObject方法的实现

image.png

swift_allocObject中有3个参数metadatarequiredSizerequiredAlignmentMask

  • metadata:元类型,相当于OC中的isa
  • requiredSize:需要开辟的内存大小
  • requiredAlignmentMask:字节对齐方式,比如8字节对齐

swift_allocObject方法中调用了CALL_IMPL方法,进入CALL_IMPL方法

image.png

CALL_IMPL的作用就是将传入的方法名进行包装,把swift_allocObject包装成_swift_allocObject_,全局搜索_swift_allocObject_代码如下

image.png _swift_allocObject_方法中主要实现了两个方法swift_slowAllocHeapObject(metadata)

swift_slowAlloc探究

image.png swift_slowAlloc方法中调用了malloc去开辟内存

Swift对象内存结构

HeapObject(metadata)探究

image.png 图中显示HeapObject有两个变量metadatarefCountsmetadata是一个指针类型所以里面存放的是一个地址,metadata类似OC中的isarefCounts是引用计数

总结Swift 对象内存分配的流程

  • __allocating_init --> swift_allocObject --> _swift_allocObject_ --> swift_slowAlloc --> malloc
  • Swift对象的内存结构 HeapObject (OC objc_object) ,有两个属性:一 个是 metadata , 一 个是 refCount ,默认占用 16 字节大小。

Swift类结构探究

metadata的类型是HeapMetadata,现在就探究下HeapMetadata结构

HeapMetadata探究

image.png HeapMetadataTargetHeapMetadata这个类型的别名,点击进入TargetHeapMetadata结构

TargetHeapMetadata探究

image.png 图中源码显示:TargetHeapMetadata继承TargetMetadata,如果是一个纯Swift类,那么类型为MetadataKind,如果需要与objc进行交互,那么传入的类型就是isa

MetadataKind探究

image.png

MetadataKind是一个uint32_t的类型,具体定义的类型如下

image.png

很明显MetadataKind和我们想要查找类的结构是不相符的,接下去只能查找 TargetHeapMetadata的父类TargetMetadata

TargetMetadata探究

TargetMetadata结构体中的代码比较多,猜测因为根据不同MetadataKind创建不同的类型,经过查找如下图 image.png

getTypeContextDescriptor方法中通过MetadataKind来区分不同的类型,TargetClassMetadata可能是元类型的基类。如果是Class类型,那么就会把当前指针this强转为TargetClassMetadata类型

TargetClassMetadata探究

image.png

图中显示:TargetClassMetadata继承TargetAnyClassMetadataTargetClassMetadata自身有很多属性,现在要查找所有的属性,所以也要把父类中的属性全部找到

TargetAnyClassMetadata探究

image.png

  • TargetAnyClassMetadata 继承TargetHeapMetadataTargetHeapMetadata继承TargetMetadata
  • TargetAnyClassMetadata的结构题中有SuperclassCacheData[2]Data等属性,很熟悉的感觉和OC中的类结构类似

经过整理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
}

至此,通过源码的方式我们得出了Swift类的数据结构

指针重绑定验证数据结构

通过底层源码的分析,我们已经知道实例对象的结构以及类的结构。现在通过指针重绑定的方式来验证下探究的结果是否正确

实例对象的重绑定

首先定义一个实例对象的结构体HeapObject,然后创建一个LWTeacher类型实例对象。代码如下

struct HeapObject{
    var metadata:UnsafeRawPointer //UnsafeRawPointer :swift 中的原生指针
    var refCount:UInt64
}

class LWTeacher{
    var age :Int = 10
    var name :String = "哈哈"
}
 
let lw = LWTeacher()

实例变量lw中存放的地址是指向HeapObject结构体的,那么现在我们要做的就是将lw这个指针重新绑定成HeapObject这个结构体类型。代码如下

let lw = LWTeacher()
// 获取实例对象原生指针
let objcRoWPointer = Unmanaged.passRetained(lw as AnyObject).toOpaque()
print(objcRoWPointer)
// 将原生指针绑定成HeapObject类型
let objcPtr = objcRoWPointer.bindMemory(to: HeapObject.self, capacity: 1)
//objcPtr.pointee 访问指针这是swift中的语法
print(objcPtr.pointee)

运行结果和lldb调试结果如下

image.png

  • objcRoWPointer是原生指针其类型是UnsafeMutableRawPointer类型,实例变量lwLWTeacher类型的指针,但是它们存储的地址是相同的,不同的是类型
  • 查看0x0000600001bcaee0地址的内容和最后打印出的结果HeapObject(metadata: 0x0000000101342198, refCount: 8589934595)是相同的,其中refCount: 8589934595因为refCount用的是UInt64类型接收的,把其转换成16进制的结果是0x0000000200000003 验证了Swift中实例对象本质就是HeapObject结构体

类的重绑定

首先定义一个类的结构体Metadata。代码如下

struct Metadata{
var kind: Int
var superClass: UnsafeRawPointer
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
}

下面继续验证类的重绑定,首先通过objcPtr.pointee.metadata获取HeapObject中的metadata,因为metadata是指向类的结构体。代码如下

//objcPtr.pointee.metadata的内存地址,将其重新绑定成Metadata类型
//MemoryLayout<Metadata>.stride 表示Metadata的内存大小
let metadataStr = objcPtr.pointee.metadata.bindMemory(to: Metadata.self, capacity: MemoryLayout<Metadata>.stride)
print(metadataStr.pointee)

运行结果和lldb调试结果如下

image.png Metadata打印出的结果和lldb调试的结果,在经过1610进制的转换结果是一样的。同时也验证了Swift中类的本质就是Metadata结构体

总结

通过Swift学习,我们大致了解了SwiftOC还是有很大的区别,在学习过程中可以去对比思考,但是尽量不要用OC的思想带入。在读底层代码的时候可能开始会比较的难懂,甚至想放弃,但是这是一个必须经历的过程。在学习的过程中官方文档是一个很重要的资料,不懂的地方都可以出查阅。最后就是这种重绑定的思想是需要掌握理解的,Swift学习过程仍然在继续,希望可以一直坚持下去