Swift结构体探究

1,677 阅读7分钟

结构体和类

结构体和类都是我们经常使用的类型,那么它们究竟有什么区别

class Person {
    var height = 180
}

struct People {
    var age : Int
}

对于上面一个类一个结构体的代码得到SIL -w703 可以看到结构体直接提供了属性的初始化方法,而类并没有主动提供,但是如果结构体成员变量给出默认值,系统则不会提供合成的初始化方法 -w698

那么之前已经分析过类的初始化过程,类是使用从堆上分配的内存空间,那么现在同样来分析一下结构体的初始化过程,还是老样子先写一段很简单的代码然后进入其SIL文件来分析

struct People {
    var age : Int = 18
    var height : Int = 180
}

var p = People()

-w936 结构体的初始化过程相比类就简单了许多,直接进入了init()方法,没有发现分配内存空间的痕迹,直接进入init()内部 -w925 -w998 从文档里可以知道,结构体的初始化方法直接是使用了alloc_stack分配到了在栈上的一块内存空间 结构体初始化流程:

  • 使用了alloc_stack分配到了在栈上的一块内存空间
  • 18作为初始化为Int类型的结构体的默认值初始化结构体并存入3%
  • struct_element_addr得到结构体People中的age在内存中的地址,并将3%的值存入
  • 同理将180存入height
  • 返回结构体

值类型和引用类型

值类型

我们一直听到的是,是引用类型,结构体是值类型,这里不再赘述随处可查到的何时使用类或者何时使用结构体,接下来就来看看它们究竟有什么区别。同样从一段代码切入:

var a = 10
var b = a
a += 1

结果a=11``b=10,赋值操作明显就是值拷贝,那么这里可以说变量不仅是个名字如:a,b,c...这么简单,它表示的是内存中的一个位置,在这个位置中包含了某个值。那么如上代码所示,我们使用a来指向内存中的一个位置,这个位置中有一个类型为Int的值10,b指向了内存中的另一个位置同样这个位置有一个类型为Int的值10,所以a指向内存中的值进行了加一操作后重新写回a指向的内存,而b指向的内存并没有任何操作。现在来验证一下

-w819

正如我们上面分析的一样,ab是指向不同的内存空间,并且直接将值存入其中,所以修改了其中一方并不影响另一方。 结论 值类型的特征:在变量的背后,值直接保存在变量指向的内存位置。

引用类型

class Person {
    var age: Int = 18
}

var p = Person()
var p1 = p
p1.age = 10

-w741

从上面类的这段代码及地下调试结果图来看,pp1指向同一块内存地址0x100727ef0,所以改变p1的属性值同样会导致p内部的改变,这就是引用类型的本质:变量不含有"事物"本身,而是持有一个对"事物"的引用。其他变量也可以含有对同一个实例的引用,并可以通过任意一个指向它的变量对其做修改。

mutating

struct Person {
    var age: Int = 18
     func changeAge(_ modifyAge: Int){
        //age = modifyAge
    }
}

如果在上述代码中的结构体内函数想修改其自身的属性,编译器会报错,一定要让加上关键字mutating,为了弄清为什么会这样,来看看其SIL代码有何不同 -w930 这里可以看到函数changeAge会包含一个隐藏参数self回指到它本身,同时可以看到有一个关键字let意味着不可变,所以如果想修改自身属性是不允许的,那么加上mutating会有什么不同呢 -w1120

  • Person加上了关键字@inout
  • self不是直接取值了,而是改成了取地址
  • 使用var来修饰self,意味此时可进行修改

总结 mutating关键字的本质其实是inout,类并没有也不需要可变方法,在类的方法中,self表现得就像一个用let声明的变量。不过虽然我们不能对self重新赋值,但可以通过使用self来修改其所指向的实例的属性,同时只需要该属性是用var来修饰的。

延迟存储属性

先来看一段简单代码

class Person {
    lazy var height = 180
}

var p = Person()
var h = p.height
p.height = 200

那么在属性前加上关键字lazy到底有什么影响呢,使用方法class_getInstanceSize(Person.self)发现结果是32,根据之前对实例对象的分析,发现与现在得到的结果居然不匹配,只好进入SIL文件中,看看能否得到答案

SIL文件分析

-w907 可以看到这里有个__lazy_storage_$_height变量,同时后面有个?,那么这里是否代表它是一个可选类型呢,在我们创建Person的实例对象后,使用了h来接收实例对象的height-w862 那么其实就是调用了heightget方法 -w950get方法中大致的流程:

  • 拿到__lazy_storage_$_height的地址
  • 得到__lazy_storage_$_height的值
  • 对该值进行判断,如果有值走入分支bb1,如果为空则走入bb2 那么我们此时应当是没有值的,则走入bb2的流程:
  • 180赋值给10%
  • 开始构建Int结构体,并将10%中的值作为初始值
  • 拿到__lazy_storage_$_height的地址赋值给14%
  • 把初始值180给到__lazy_storage_$_height

get方法中发生了什么我们已经有了大致的了解,接下来看看对应的set方法中是如何呢 -w879 set方法第一个参数是200第二个参数是Person的实例对象 -w905

其中流程大致与get方法中一样,且仅仅是更为简单的赋值操作而已。从这两个方法中不难看出__lazy_storage_$_height这个类型其实为Optional<Int>,所以它的内存结构是如何呢,通过MemoryLayout方法可以得到Optional<Int>实际占有的内存为9字节,那么一开始的问题得解,16+9=25,并且通过字节对齐后可得32字节。 -w999

总结lazy修饰的存储属性:

  • 延迟存储属性必须有默认初始值
  • 延迟存储在第⼀次访问之前是nil
  • 延迟存储属性是非线程安全的
  • 延迟存储属性对实例对象⼤⼩有影响

类型属性

class Person {
    static var height = 180
}

let h = Person.height
Person.height = 200

同样,直接切入SIL文件 -w944 首先可以看到声明在Person中的height属性变成了一个全局变量 -w904 在用h来接收类型属性height的过程中会调用unsafeMutableAddressor -w939 这里会调用到一个全局的初始化方法globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 -w898 显然这个方法就是将初始值赋值给前面说height的全局变量,现在通过lldb调试回到unsafeMutableAddressor这个方法中,看看能不能找到其他的符号 -w1078 这里发现的swift_once应当与SIL文件中的builtin "once" 相对应,看到这里是不是有点单例的意思在里面,直接去Swift源码中寻找 -w843 看到这里轻松得到类型属性height只会被初始化一次的结论。 总结static修饰的类型属性:

  • 必须有一个默认初始值
  • 类型属性是线程安全的,因为只会被初始化一次

结构体的方法调用

struct Person {
    func study(){
        print("study")
    }
}

var p = Person()
p.study()

-w1073

结构体中的函数调用是静态调用,函数地址在编译链接之后就已经确定了,方法并不存储在结构体中。同时可以注意到后面有一串文字swift_2.Person.study() -> (),这个就是符号,符号存在于符号表中,但符号表又不直接存储符号,符号表存储的是相对于字符串表的偏移,符号都以字符串的形式存储于字符串表中。 同时存于字符串表中的符号是进行过命名重整的(tip:swift通过复杂的命名重整规则来确保swift可以通过不同的参数来重载同一个函数)可以在mach-o文件中找到 使用以下命令: nm+当前mach-o文件地址得到当前符号表后在其中找到字符串进行xcrun swift-demangle s7swift_26PersonV5studyyyF解码便可得到真实的符号。 -w1225 -w1216 -w1008