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 <stdio.h>
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层代码,一一阐述了静态派发动态派发的底层实现流程,然后解释了关于方法的一些常用关键字的含义,希望大家能够掌握。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容