Swift 值类型 引用类型 & 方法调度

前言

本篇文章将会带大家分析Swift中结构体Struct类Class底层结构,解释为什么结构体Struct值类型,而类Class引用类型?接着我们通过对Swift中的方法function的调度分析,解释静态派发动态派发的区别。

一、值类型

我们先通过一个示例看看,什么是值类型?

func test(){
    var age = 18
    var age2 = age

    age = 30
    age2 = 45
    
    print("age=\(age),age2=\(age2)")
}
test()
print("end")

断点run运行

上图可见,age2的值开始和age的值一样,但是后面改变了age2的值,age的值并未受到影响,接着我们lldb看看它们的内存地址

注意:可通过下面的指令查看某一变量的内存地址
po withUnsafePointer(to: &age){print($0)}

上图可见,age和age2的地址相差8字节,age的地址大于age2,是因为栈区是从高地址向低地址分布内存地址空间的。

内存的分区可参考内存五大分区。

同时,age2的值的改变并不会影响age的值,说明var age2 = age这句代码执行的是深拷贝。所以,值类型有以下特点

  • 地址中存储的是
  • 值类型的传递过程中,相当于传递了一个副本,也就是所谓的深拷贝
  • 值传递过程中,并不共享状态

1.1 Swift中的结构体

通常,定义结构体的属性时,可以给默认值,也可以不给

//***** 写法一 *****
struct LGTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
}
var t = LGTeacher()

//***** 写法二 *****
struct LGTeacher1 {
    var age: Int
    
    func teach(){
        print("teach")
    }
}
var t1 = LGTeacher1(age: 18)

写法二种不给属性默认值,编译器也不会报错。并且,初始化init方法可以重写,也可以使用系统默认的。

1.1.1 查看SIL

基本操作,我们看看结构体在中间层SIL代码中是什么样的结构

上图红框处可见,系统会帮我们添加默认的初始化方法。

1.1.2 结构体是值类型

为什么说结构体值类型,我们lldb看看内存就知道答案了

po withUnsafePointer(to: &t){print($0)}

上图中属性age的值就存在结构体的地址当中。和我们之前看的值类型的示例方法test一样,再将t的值赋给变量t1,接着改变t1结构体age的值,那么tage值会随着一起改变吗?

var t = LGTeacher()
var t1 = t
t1.age = 20

运行

tage值并没有变化,仍然是18,所以可以验证--> 结构体是值类型

还是老套路,我们看看sil层代码

从上图sil中间层代码可知,t1完全是alloc出来另一个内存地址,和t的地址完全不同,t1和t并不共享状态,所以将20赋给t1的age并不会影响到t的age。

二、引用类型

分析完结构体,剩下就是了,究竟是不是引用类型呢?
我们将上面的例子的struct 改成 class

上图可知,属性age如果没有给默认值,会报错:没有初始化方法!我们可以给一个可选类型?

所以,类class中的属性,要么给个默认值,要么指定是可选项类型。

2.1 类是引用类型?

我们可以通过示例验证一下

class LGTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var t1 = LGTeacher()

首先lldb查看内存

我们接着查看0x0000000105905010这个地址里的信息

所以,引用类型有2个特点

  1. 地址中存储的是堆区地址
  2. 堆区地址中存储的是

同样的问题,t1会影响t的值吗?

可见,t中和t1中存储的堆区地址是同一个,所以修改t1会导致t一起变化,即浅拷贝

我们再改变下代码,看看下面的代码会怎么样?

class LGTeacher1 {
    var age: Int = 18
    var age2: Int = 20
}

struct LGTeacher {
    var age: Int = 18
    var age2: Int = 20
    var teacher: LGTeacher1 = LGTeacher1()
}

var t = LGTeacher()

var t1 = t
t1.teacher.age = 30

print("t1.teacher.age = \(t1.teacher.age) t.teacher.age = \(t.teacher.age)")

我们在结构体中声明一个类的成员,t是结构体对象,t赋给t1,再改变t1中类成员的age属性值,t中的类成员age属性会变化吗?run

会一起改变!同样,我们看看地址

上图可见,结构体t中成员变量是类class时,存的也是堆区的地址。那么t赋给t1时,teacher成员当然是浅拷贝。所以

在编写代码过程中,应该尽量避免值类型包含引用类型

最后我们依然看看sil中间层代码

接着看看age的 gettersetter

我们发现,gettersetter中均进行了一次ref_element_addr引用计数+1,那么,通过CFGetRetainCount看看t.teacher的引用计数是多少?

是3,这就解释了main中strong_retain 以及 gettersetter中各一次的ref_element_addr

三、方法调度

上面讨论的值类型引用类型,都是针对成员变量或属性,那么结构体或类的方法,底层是如何调度的呢?接下来我们看看方法的调度。Swift方法的调用有2种方式:静态派发 & 动态派发

3.1 静态派发

什么是静态派发?值类型对象的方法调用是静态派发,即直接对地址的调用,这个地址就是函数的指针,而且在编译、链接完成后就已经确定了,存放在代码段。例如:结构体是值类型,但是结构体的内部不存储方法

3.1.2 找入口

首先,在结构体方法调用时打上断点

查看汇编

我们发现了对teach()方法的调用,在汇编层是callq 0x100003d00,显然是对地址的直接操作。

接着我们查看Mach-O文件

Section64(__TEXT, __text)区间中,确实有对地址0x100003d00进行调用,该区间就是所谓的代码段。接下来问题来了,底层是如何通过将地址0x100003d00 和 方法teach()联系在一起的?也就是汇编层

上图红框处,汇编层代码中后面的符号,底层是如何解析出是结构体LGTeacher的teach()方法?

还是回到mach-O文件中,可以看到

  • Symbol Tables就是符号表
  • 符号表中并不存储字符串,字符串存储在String Table字符串表中。
  • 右侧第一个红框处,符号表中记录的teach()函数字符串表偏移起始地址0000CA30,偏移值Data是00000119,然后计算得出的Value值就是工程名+类名+函数名
  • 右侧第2个红框处,是teach()函数在内存区间的定位,在Section64(__TEXT,__text)区间里,而函数的地址就是1000003D00(右侧第3个红框处),

  • 字符串表中所记录的字符串值的地址则是

3.1.2 命令行查看符号表

我们还可以通过命令行查看以上teach()函数的地址信息。

查看符号表:nm mach-o文件路径
通过命令还原符号名称:xcrun swift-demangle 符号

示例中的符号表信息

其中,我们可以看到符号信息s9SwiftTest9LGTeacherV5teachyyF,就是之前在符号表中定位的工程名+类名+函数名,还原其符号名称

Release模式的区别

注意:以上都是Debug模式下生成的Mach-O文件,如果是Release模式,会是什么样的情况呢?
首先,将edit scheme -> run中的debug改成release,编译后查看,在可执行文件目录下,多一个后缀为dSYM的文件

接着,去Mach-O文件中的符号表Symbol Table中搜索teachyyF,也就是teach()函数

发现找不到,其主要原因是 静态链接的函数,一旦编译完成,其地址确定后,当前的符号表就会删除当前函数对应的符号,在release环境下,符号表中存储的只是不能确定地址的符号。

对于不能确定地址的符号,是在运行时确定的,即函数第一次被调用时(相当于懒加载)。

扩展1:函数符号的命名规则

  • C函数
    对于C函数来说,命名的重整规则就是在函数名之前加_,所以不支持函数重载
    示例
#include 
void test(){    }
  • OC方法
    不支持函数重载,命名的重整规则是-[类名 函数名]
  • Swift方法
    支持函数重载,Swift中的重整命名规则比较复杂,可以确保函数符号的唯一性。

扩展2:ASRL

地址空间布局随机化 address space layout randomizes,简称ASRL。上述的teach()方法调用的汇编

对应的Mach-O的地址是

接着我们lldb image list查看调用栈寄存器信息

其中0x0000000100000000程序运行的首地址后8位是随机偏移00000000,这就是ASLR。将Mach-O中的文件地址0x0000000100003D00 + 0x00000000 = 0x100003D00,正好对应汇编里调用的地址。

3.2 动态派发

既然值类型对象的方法调度是静态派发,那么引用类型对象的方法调度则是动态派发了,所以我们以类Class为例看看。

3.2.1 找入口

首先查看以下示例代码

class LGTeacher{
    func teach(){}
    func teach2(){}
    func teach3(){}
    func teach4(){}
    init(){}
}

类LGTeacher声明了4个方法,编译,查看Mach-O文件

上图红框处我们可以看到,类LGTeacher声明的4个方法,它们的地址是连续的0x100039E0 --> 0x10003A00 --> 0x10003A20 --> 0x10003A40

不信?我们再看SIL中间层代码

swiftc -emit-sil xx.swift | xcrun swift-demangle >> ./xx.sil && vscode xx.sil

拉到最下面,发现有个sil_vtable LGTeacher,你可以将其理解为类LGTeacher函数表,这个函数表也可以理解为数组,为什么这么说是数组呢?我们接下来看看函数表底层的源码。

3.2.2 函数表底层的源码

我们在源码中搜索initClassVTable,并加上断点,然后写上源码进行调试

源码中可见,其内部是通过for循环编码,然后offset+index偏移,然后获取method,将其存入到偏移后的内存中,这里也可以验证函数是连续存放的

3.2.3 在extension中声明的方法

如果更改方法声明的位置呢?例如extension中的函数,此时的函数调度方式还是函数表调度吗?我们可以通过代码验证一下

extension LGTeacher {
    func teach5(){ print("teach5") }
}

接着定义一个子类LGStudent继承自LGTeacher

class LGStudent: LGTeacher{}

然后查看SIL中的V-Table

子类LGStudent只继承了LGTeacher class中定义的函数,即函数表中的函数,并没有继承extension中声明的方法,why?

原因是子类是将父类的函数表全部继承了,此时子类增加函数,那么就继续在连续的地址插入,如果extension函数也是在函数表中,则意味着子类也有extension中声明的函数,但是子类并没有相关的指针记录函数父类方法 还是 子类方法,所以子类方法不知道该插入到哪里,导致extension中的函数无法安全的放入子类中,所以extension中的方法是直接调用的,且只属于当前类,子类是无法继承的。

那么我们在日常的开发过程中需要注意:

  1. 继承方法和属性,不能写extension
  2. extension中创建的函数,一定是只属于当前类,但是其子类也有其访问权限,只是不能继承和重写,例如

补充1:关键字 mutating & inout

先看下面代码

struct LGStack {
    var items: [Int] = []
    func push(_ item: Int){
        items.append(item)
    }
}

直接报错,原因是值类型的结构体是不允许修改成员变量的。接着修改代码

struct LGStack {
    var items: [Int] = []
    func push(_ item: Int){
        print(item)
    }
}

我们看看上面代码的SIL层代码

找到push方法,发现self是let类型,当然不允许修改

我们换一种写法

struct LGStack {
    var items: [Int] = []
    func push(_ item: Int){
        var s = self
        s.items.append(item)
    }
}

var t = LGStack()
t.push(1)

运行看看

还是不能不能将item添加进去,因为s是值拷贝,是另一个结构体,此时调用push是将item添加到s的数组中了,那么t里当然没有。

根据编译器错误的提示,给push添加mutating,发现可以添加到数组了

struct LGStack {
    var items: [Int] = []
    mutating func push(_ item: Int){
        items.append(item)
    }
}

运行

可以添加进去了,接下来看看SIL层代码,看看mutating到底做了什么?

我没看到,mutating对应的SIL层是inout关键字,而self由之前的let类型变成了var类型。那么接下来我们看看inout关键字。

inout

一般情况下,在函数的声明中,默认的参数都是不可变的,如果想要直接修改,需要给参数加上inout关键字。例如:

不加inout关键字,直接报错!

添加inout关键字,可以给参数赋值。

小结

  1. 结构体中的函数如果想修改其中的属性,需要在函数前加上mutating,而类则不用
  2. mutating本质也是加一个 inout修饰的self
  3. Inout相当于取地址,可以理解为地址传递,即引用
  4. mutating修饰方法,而inout 修饰参数

补充2:关键字 final、@objc、dynamic修饰函数

final

final 修饰的方法是直接调度的,可以通过SIL + 断点验证

class LGTeacher {
    final func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

SIL代码

方法调度表里没有teach()方法。接下来打断点

查看汇编

汇编可知,teach()直接地址调用,teach2() teach3() teach4()是地址偏移函数表调用

@objc

修改代码

class LGTeacher {
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    init(){}
}

查看SIL

teach()方法依旧在函数表中,那么@objc修饰的方法是函数表调度,再看汇编层

注意:如果只是通过@objc修饰函数,OC还是无法调用swift方法的,class还需要继承NSObject

class LGTeacher : NSObject {
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

查看混编的头文件


打开往下拉

看到了系统自动转换的OC类LGTeacher和方法。接着我们看SIL

上图,一个是Swift的teach()方法,一个是OC的teach()方法。但是OC的teach()方法内部是调用Swift的teach()方法,代码

%2 = function_ref @main.LGTeacher.teach() -> () : $@convention(method) (@guaranteed LGTeacher) -> () // user: %3
%3 = apply %2(%0) : $@convention(method) (@guaranteed LGTeacher) -> () // user: %5

dynamic

修改代码

class LGTeacher : NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

查看SIL

teach()依旧是函数表调用。
使用dynamic的意思是可以动态修改-->当类继承NSObject时,可以使用method-swizzling

swift中实现方法交换
class LGTeacher : NSObject {
    dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

extension LGTeacher{
    @_dynamicReplacement(for: teach)
    func teach5(){
        print("teach5")
    }
}

// 调用代码
let t = LGTeacher()
t.teach()

run

我们注意到,在swift中的需要交换的函数前,需要使用dynamic修饰被交换的函数,然后通过@_dynamicReplacement(for: 被交换的函数符号)进行交换。

@objc dynamic

再看一个例子,如果通过@objc dynamic修饰的方法teach()

class LGTeacher : NSObject {
    @objc dynamic func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

查看汇编

teach()方法调用走的是objc_msgSend流程,即动态消息转发

总结

本篇文章重点讲解了结构体对象类对象的方法调度方式,结合示例代码,分析汇编层,SIL层代码,一一阐述了静态派发动态派发的底层实现流程,然后解释了关于方法的一些常用关键字的含义,希望大家能够掌握。

你可能感兴趣的:(Swift 值类型 引用类型 & 方法调度)