Swift

Swift-进阶 03:值类型 & 引用类型

2020-12-10  本文已影响0人  Style_月月

Swift 进阶之路 文章汇总

本文主要介绍为什么结构体是值类型,类是引用类型

值类型

前提:需要了解内存五大区,内存五大区可以参考这篇文章iOS-底层原理 24:内存五大区,如下所示

值类型-1

我们通过一个例子来引入什么是值类型

func test(){
    //栈区声明一个地址,用来存储age变量
    var age = 18
    //传递的值
    var age2 = age
    //age、age2是修改独立内存中的值
    age = 30
    age2 = 45
    
    print("age=\(age),age2=\(age2)")
}
test()

从例子中可以得出,age存储在栈区

值类型-2 值类型-3

所以,从上面可以说明,age就是值类型

值类型 特点

结构体

结构体的常用写法

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

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

结构体的SIL分析

为什么结构体是值类型?

定义一个结构体,并进行分析

struct CJLTeacher {
    var age: Int = 18
    var age2: Int = 20
}
var  t = CJLTeacher()
print("end")
值类型-8

问题:此时将t赋值给t1,如果修改了t1,t会发生改变吗?

SIL验证

同样的,我们也可以通过分析SIL来验证结构体是值类型

总结

引用类型

**类的常用写法 **

//****** 写法一 *******
class CJLTeacher {
    var age: Int = 18
    
    func teach(){
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
}
var t = CJLTeacher.init(20)

//****** 写法二 *******
class CJLTeacher {
    var age: Int?
    
    func teach(){
        print("teach")
    }
    init(_ age: Int) {
        self.age = age
    }
}
var t = CJLTeacher.init(20)

为什么类是引用类型?

定义一个类,通过一个例子来说明

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

类初始化的对象t1,存储在全局区

引用类型-4

引用类型 特点

问题1:此时将t1赋值给t2,如果修改了t2,会导致t1修改吗?

问题2:如果结构体中包含类对象,此时如果修改t1中的实例对象属性,t会改变吗?

代码如下所示

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

struct CJLTeacher {
    var age: Int = 18
    var age2: Int = 20
    var teacher: CJLTeacher1 = CJLTeacher1()
}

var  t = CJLTeacher()

var t1 = t
t1.teacher.age = 30

//分别打印t1和t中teacher.age,结果如下
t1.teacher.age = 30 
t.teacher.age = 30

从打印结果中可以看出,如果修改t1中的实例对象属性,会导致t中实例对象属性的改变。虽然在结构体中是值传递,但是对于teacher,由于是引用类型,所以传递的依然是地址

同样可以通过lldb调试验证

引用类型-6

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

查看当前的SIL文件,尽管CJLTeacher1是放在值类型中的,在传递的过程中,不管是传递还是赋值,teacher都是按照引用计数进行管理的

引用类型-7
可以通过打印teacher的引用计数来验证我们的说法,其中teacher的引用计数为3
引用类型-8
主要是是因为:

mutating

通过结构体定义一个,主要有push、pop方法,此时我们需要动态修改栈中的数组

struct CJLStack {
    var items: [Int] = []
    func push(_ item: Int){
        print(item)
    }
}
引用类型-11
从图中可以看出,push函数除了item,还有一个默认参数selfselflet类型,表示不允许修改
struct CJLStack {
    var items: [Int] = []
    func push(_ item: Int){
        var s = self
        s.items.append(item)
    }
}

打印结果如下

引用类型-12
可以得出上面的代码并不能将item添加进去,因为s是另一个结构体对象,相当于值拷贝,此时调用push是将item添加到s的数组中了
struct CJLStack {
    var items: [Int] = []
    mutating func push(_ item: Int){
        items.append(item)
    }
}

查看其SIL文件,找到push函数,发现与之前有所不同,push添加mutating(只用于值类型)后,本质上是给值类型函数添加了inout关键字,相当于在值传递的过程中,传递的是引用(即地址)

引用类型-13

inout关键字

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

总结

总结

通过上述LLDB查看结构体 & 类的内存模型,有以下总结:

方法调度

通过上面的分析,我们有以下疑问:结构体和类的方法存储在哪里?下面来一一进行分析

静态派发

值类型对象的函数的调用方式是静态调用,即直接地址调用,调用函数指针,这个函数指针在编译、链接完成后就已经确定了,存放在代码段,而结构体内部并不存放方法。因此可以直接通过地址直接调用

对于上面的分析,还有个疑问:直接地址调用后面是符号,这个符号哪里来的?

image
是从Mach-O文件中的符号表Symbol Tables,但是符号表中并不存储字符串,字符串存储在String Table(字符串表,存放了所有的变量名和函数名,以字符串形式存储),然后根据符号表中的偏移值到字符串中查找对应的字符,然后进行命名重整:工程名+类名+函数名,如下所示
image
-Symbol Table:存储符号位于字符串表的位置

还可以通过终端命令nm,获取项目中的符号表

函数符号命名规则

#include <stdio.h>
void test(){    }
image

补充:ASLR

关于ASLR的详细说明参考iOS-底层原理 32:启动优化(一)基本概念中对于ASLR的解释,下面是针对函数地址的一个验证

动态派发

汇编指令补充

探索class的调度方式

首先介绍下V_Table在SIL文件中的格式

//声明sil vtable关键字
decl ::= sil-vtable
//sil vtable中包含 关键字、标识(即类名)、所有的方法
2 sil-vtable ::= 'sil_vtable' identifier '{' sil-vtable-entry* '}'
//方法中包含了声明以及函数名称
3 sil-vtable-entry ::= sil-decl-ref ':' sil-linkage? sil-function-na
me

例如,以CJLTacher为例,其SIL中的v-table如下所示

class CJLTeacher{
    func teach(){}
    func teach2(){}
    func teach3(){}
    func teach4(){}
    @objc deinit{}
    init(){}
}
image

观察这几个方法的偏移地址,可以发现方法是连续存放的,正好对应V-Table函数表中的排放顺序,即是按照定义顺序排放在函数表中

image

函数表源码探索

下面来进行函数表底层的源码探索

对于class中函数来说,类的方法调度是通过V-Taable,其本质就是一个连续的内存空间(数组结构)。

问题:如果更改方法声明的位置呢?例如extension中的函数,此时的函数调度方式还是函数表调度吗?

通过以下代码验证

extension CJLTeacher{
    func teach5(){ print("teach5") }
}
class CJLStudent: CJLTeacher{}

开发注意点:

extension CJLTeacher{
    var age: Int{
        get{
            return 18
        }
    }
    func teach(){
        print("teach")
    }
}

class CJLMiddleTeacher: CJLTeacher{
    override func study() {
        print("CJLMiddleTeacher study")
    }
}

var t = CJLMiddleTeacher()
//子类有父类extension中方法的访问权限,只是不能继承和重写
t.teach()
t.study()
print(t.age)

<!--运行结果-->
teach
CJLMiddleTeacher study
18

final、@objc、dynamic修饰函数

final 修饰

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

@objc 修饰

使用@objc关键字是将swift中的方法暴露给OC

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

通过SIL+断点调试,发现@objc修饰的方法是 函数表调度

image

【小技巧】:混编头文件查看方式:查看项目名-Swift.h头文件

image
<!--swift类-->
class CJLTeacher: NSObject {
    @objc func teach(){ print("teach") }
    func teach2(){ print("teach2") }
    func teach3(){ print("teach3") }
    func teach4(){ print("teach4") }
    @objc deinit{}
    override init(){}
}

<!--桥接文件中的声明-->
SWIFT_CLASS("_TtC9_3_指针10CJLTeacher")
@interface CJLTeacher : NSObject
- (void)teach;
- (nonnull instancetype)init OBJC_DESIGNATED_INITIALIZER;
@end

<!--OC调用-->
//1、导入swift头文件
#import "CJLOCTest-Swift.h"
//2、调用
CJLTeacher *t = [[CJLTeacher alloc] init];
[t teach];

查看SIL文件发现被@objc修饰的函数声明有两个:swift + OC(内部调用的swift中的teach函数)

image
即在SIL文件中生成了两个方法

dynamic 修饰

以下面代码为例,查看dynamic修饰的函数的调度方式

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

其中teach函数的调度还是 函数表调度,可以通过断点调试验证,使用dynamic的意思是可以动态修改,意味着当类继承自NSObject时,可以使用method-swizzling

@objc + dynamic

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

通过断点调试,走的是objc_msgSend流程,即 动态消息转发

image

场景:swift中实现方法交换

在swift中的需要交换的函数前,使用dynamic修饰,然后通过:@_dynamicReplacement(for: 函数符号)进行交换,如下所示

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

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

将teach方法替换成了teach5


image

总结

补充:内存插件

主要补充内存插件libfooplugin.dylib安装及使用

安装 & 使用

可以在这里下载插件文件,密码: go4q

内存分区实践

堆区

有以下代码,通过cat查看t属于哪个区

class CJLTeacher{
    func teach(){
        
    }
}
let t = CJLTeacher()
image
从结果中可以看出,是在堆区,即heap pointer

栈区

查看以下代码的内存地址位于哪个区?

func test(){
    var age: Int = 10
    print(age)
}
image
从结果来看,位于栈区,即stack pointer

全局区

对于C的分析

下面是C语言的部分代码,查看其变量的内存地址

//全局已初始化变量
int a = 10;
//全局未初始化变量
int age;

//全局静态变量
static int age2 = 30;

int main(int argc, const char * argv[]) {
    
    char *p = "CJLTeacher";
    printf("%d", a);
    printf("%d", age2);
    return  0;
}
static const int age3 = 40;

对于swift的分析

let age = 10

由于是不可变所以不能通过po+cat查看内存,通过汇编 首地址+偏移 来获取age的内存,发现是在Mach-O的__DATA.__common

image
从这里可以发现,这与C中是有所区别的。swift的不同之处:已经初始化的全局变量放在__DATA.__common段,猜测是因为 age开始是被标记为未初始化的,当我们执行代码之后才将10存储到对应的内存地址中
var age2 = 10
image

总结

上一篇下一篇

猜你喜欢

热点阅读