Swift底层原理探索之结构体和类

1,304 阅读5分钟

结构体

  • 在swift标准库中,绝大多数公开类型都是结构体,而枚举和类只是一小部分:比如BoolIntDoubleStringArrayDicttionary等常见类型都是结构体。
struct Person {
    var name: String
    var age: Int
    var sex: String
}
var person = Person(name: 'Tom', age: 18, sex: '男')//编译器自动为结构体生成的初始化器
  • 所有结构体都有一个编译器自动生成的初始化器(initializer,初始化方法,构造器,构造方法),在上面最后一行调用,可以传入所有成员值,用以初始化所有成员(存储属性,Stored Property)

结构体的初始化器

编译器会根据情况,可能会为结构体生成多个初始化器,宗旨是:保证所有成员有初始值,保证代码的安全性,看一下以下几种情况,报错的原因都是因为有成员变量没有赋初始化值造成的:

图一: image.png

图二: image.png

图三: image.png

下面这样就没有问题:

image.png

或者这样:

image.png

由于 var x: Int? var y: Int? 声明后,默认有初始值nil,因此也不会报错。

自定义初始化器

image.png

但是当我们给Point添加了自定义初始化器之后,为啥又报错了呢?这是因为一旦在定义结构体时自定义了初始化器,编译器就不会再帮我们自动生成其他初始化器了。

窥探初始化器的本质

image.png

image.png 上面两段代码是等效的,第一段使用了系统生成的无参初始化器,第二段使用了我们自定义的无参初始化器。

如果要探索初始化器的本质的话,就需要看一下他们的汇编代码:

image.png 汇编代码如下:

image.png

自定义初始化器:

image.png 汇编代码如下:

image.png 对于上述两段程序,他们所调用的无参初始化器函数底层是一模一样的,汇编代码连行数都是一样的,因此判断是等效的。语法糖或许会骗人,但是汇编永远不会骗你的。

结构体内存结构

struct Point {
    var x: Int = 10
    var y: Int = 50
    var origin: Bool = true
}
var point = Point()

print(MemoryLayout<Point>.size)
print(MemoryLayout<Point>.stride)
print(MemoryLayout<Point>.alignment)
print(Mems.memStr(ofVal: &point))//输出结构体内存里面的数据

===============运行结果===============
17
24
8
0x000000000000000a 0x0000000000000032 0x00007f95e1e07d01

由上面可以看得出,Swift的结构体和C语言的结构体内存结构是一样的,成员变量内存都是紧挨在一起的。

类的定义和结构体类似,但编译器并没有为类自动生成可以传入成员值的初始化器

image.png

在类的每个成员都有默认值时,系统则会为它创建一个无参初始化器:

image.png

需要注意的是 var x: Int?optional,因此它会自动获得一个默认值nil。相比较于StructClass的声明方式几乎和Struct一模一样,也可以在内部增加方法。从表面上看,只有初始化器有一点不同。

类的初始化器

成员的初始化是在这个初始化器中完成的,证明方法和上面的Struct中的汇编证明一样,在这里就不啰嗦了。

结构体与类的区别

结构体是值类型(枚举也是值类型),类是引用类型(指针类型

class Size {
    var width = 1
    var height = 2
}

struct Point {
    var x = 3
    var y = 4
}

func test() {
    var size = Size()
    var point = Point()
}

上面代码中的size和point在内存中的分布情况如下:

image.png

对象的堆空间申请过程

image.png

进入到汇编调试页面

image.png

image.png

image.png

image.png

image.png

在swift中,创建类的实例对象,要向堆空间申请内存,大概流程如下:

  • Class.__allocating_init()

  • libswiftCore.dylib:_swift_allocObject_

  • libswiftCore.dylib:swift_slowAlloc

  • libsystem_malloc.dylib:malloc

    在Mac、iOS中的malloc函数分配的内存大小总是16的倍数,通过class_getInstanceSize可以得到类的对象真正使用的内存大小:

image.png

值类型

  • 值类型赋值给 varlet或者给函数传参,是直接将所有内容拷贝一份,类似于对文件进行 copypaste操作,产生了全新的文件副本。属于深拷贝

image.png

值类型的赋值操作

  • 在swift标准库中,为了提升性能,StringArrayDictionarySet 采取了Copy On Write的技术

    1. 比如仅当有“”操作时,才会真正执行拷贝操作

    2. 对于标准库值类型的赋值操作,Swift能确保最佳性能,所以没有必要为了保证最佳新能来避免赋值

    需要注意的是,上面说的仅仅针对Swift标准库,对于自定义的结构体来说,Swift不会使用Copy On Write技术

  • 建议:不需要修改的,尽量定义成let

var s1 = "Tom"
var s2 = s1
s2.append("_Rose")
print(s1)//Tom
print(s2)//Tom_Rose

var a1 = [1,2,3]
var a2 = a1
a2.append(4)
a1[0] = 2
print(a1)//[1,3,4]
print(a2)//[1,2,3,4]

var d1 = ["max": 10, "min":2]
var d2 = d1
d1["other"] = 7
d2["max"] = 12
print(d1)//["other": 7, "max": 10, "min":2]
print(d2)//["max": 12, "min":2]

引用类型

  • 引用赋值给varlet或者给函数传参,是将内存地址拷贝一份
    类似于制作一个文件的替身(快捷方式,链接),指向的是同一个文件。属于浅拷贝

    class Size {
        var width: Int
        var height: Int
        init(width: Int, height: Int) {
            self.width = width
            self.height = height
        }
    }
    
    func test() {
        var s1 = Size(width: 10, height: 20)
        var s2 = s1
    
        s2.width = 11
        s2.height = 22
    }
    
    test()
    

image.png

引用类型的赋值操作

class Size {
    var width: Int
    var height: Int
    init(width: Int, height: Int) {
        self.width = width
        self.height = height
    }
}

var s1 = Size(width: 10, height: 20)
s1 = Size(width: 11, height: 12)

image.png

值类型、引用类型的let

image.png

总结一下就是:let的含义就是其修饰的常量所对应的那段内存空间里面的内容不可以修改。
值类型由于所有的成员都在其内存里面,因此被let修饰时候,其内部的成员都是不可以修改的
引用类型由于它的内存里存放的只是指针,所以只有这个指针的值不能修改,但是该指针所指向的堆空间的那个对象实例,并不是引用类型内存里面的东西,因此被let修饰之后,仍然可以修改堆空间对象的成员的值。