Swift进阶(三)—— 属性

223 阅读6分钟

函数内联

函数内联是一种编译器优化技术,它通过使用方法的内容替换直接调用该方法,来优化性能。

  • always:始终内联函数。通过函数前加@inline(_always)来实现
  • never:确保永远不会内联,通过函数前加@inline(never)来实现
  • 如果函数很长但想避免增加代码段大小,可使用@inline(never)
  • Swift中默认有内联函数的行为,编译器可能会自动内联函数作为优化。 如果对象只在声明的文件中可见,可以用privatefileprivate修饰,编译器会对他们修饰的对象进行优化,确保没有其他关系的情况下,自动打上final标记,使得对象获得静态派发的特性。

存储属性

存储属性一种是变量存储属性,由var关键字引入,一种是常量存储属性,由let关键字引入,我们看个实例:

截屏2022-01-26 下午2.19.01.png 我们声明类CTTeacher,用常量t保存CTTeacher的实例,t本身保存的内存地址不可变,但是变量存储属性name的值可以改变,而其中类的常量存储属性age则不能被修改。对于值类型结构体来说,我们看如下示例:

截屏2022-01-26 下午2.26.09.png 结构体student初始化后被常量st保存,这时候st里保存的其实就是agename的值,st为常量,则agename的值也不能被更改。单独声明一个var变量和let常量生成SIL文件看下:

截屏2022-01-26 下午2.44.12.png 其实varlet的区别就是多个了个set方法。

计算属性

计算属性通过提供setter和getter来修改和获取值,计算属性只能是变量,同时计算属性必须包含类型。(编译器需要知道期望返回值是什么)声明一个结构体square,给它一个计算属性area

截屏2022-01-26 下午3.04.37.png 我们汇编调试下如下图:

截屏2022-01-26 下午3.02.45.png

截屏2022-01-26 下午3.03.17.png 我们明显可以看到,它其实访问的就是setter方法,所以计算属性的本质就是settergetter方法。

属性观察者

初始化期间设置属性不会调用属性观察器,我们看下面这个示例:

截屏2022-01-26 下午3.28.20.png 可以看到,最终没有输出willset和didset,因为这时候实例初始化还未完全完成。若初始化完成,我们重新赋值name,可以看下生成的SIL文件:

截屏2022-01-26 下午3.54.21.png 从文件中可以看出,先是调用的namesetter方法告知willset将要进行赋值操作,触发willset,接着进行赋值操作,接着通知didset方法赋值完成。所以willset和didset的本质就是通过setter方法来触发的

延迟存储属性

  • 延迟存储属性的初始值在第一次使用才进行计算。我们看示例调试:

截屏2022-01-26 下午4.21.50.pngCTPerson中有个延迟存储属性name,初始化完成在没使用name之前,可以看到内存并没有存储ct这个值。执行过print(p.name)之后我们再看:

截屏2022-01-26 下午4.31.45.png 发现内存中已经有值,也直接验证了我们上面提到的结论。那至于延迟存储属性怎么做到第一次可以没值,第二次有值,我们可以从SIL文件看到,其实它本质是个可选值并且被final关键字修饰,如下图:

截屏2022-01-26 下午4.41.47.png 我们从SIL中还可以看到延迟存储属性的初始化,下图可看出是个枚举值,默认赋值Optional.none相当于oc中的nil

截屏2022-01-26 下午5.33.46.png 再看下延迟存储属性的getter方法的访问:

截屏2022-01-26 下午6.22.25.png

截屏2022-01-26 下午6.33.57.png 首先访问name的内存地址,取值赋值给寄存器%4,然后swith_enum枚举匹配,有值调用bb1,没值调用bb2,第一次访问肯定是没值的,调用bb2bb2中首先构建字符串的值构建出来给到当前枚举变量%19,然后把值存储到_lazy_storage_name的内存地址中。

类型属性

  • 类型属性其实就是个全局变量,如下示例:

截屏2022-01-26 下午6.55.26.png 生成SIL文件后,我们可以看到它就是个全局变量下图:

截屏2022-01-26 下午6.56.09.png

  • 类型属性只会被初始化一次(线程安全) SIL文件中关键的一个once(本质调用dispatch_once)调用全局初始化函数可以确定,它只会被初始化一次,下图所示:

截屏2022-01-26 下午6.58.55.png

截屏2022-01-26 下午7.07.09.png

那我们的单例就很简单了,如下:

class Person {
    static let shareInstance = Person()
    private init{}   //指定初始化器私有化防止外部调用
}

补充:类方法也是静态派发,不属于函数表调用。

属性在Mach-O文件的位置

我在类与结构体下讲到过TargetClassDescriptor存在Mach-O文件中Section64(_TEXT,__swift,types)这个地方,而我们的属性信息存放在TargetClassDescriptor结构体的fieldDescriptor中,如下图所标注:

截屏2022-01-27 上午9.54.04.png 我们在TargetClassDescriptor的父类中TargetTypeContextDescriptor找到FieldDescriptor,然后进去可看到FieldDescriptor的结构如下图:

截屏2022-01-27 上午10.50.58.png 我们暂且把它还原为一个结构体大致就是这样了:

struct FieldDescriptor {

    var mangledTypeName:Int32

    var superClass:Int32

    var kind:UInt16

    var fieldRecordSize:UInt16

    var numberFields:uint32

    var fieldRecords:Array<FieldRecord>

}
struct FieldRecord {

    var flags:UInt32

    var mangledTypeName:Int32

    var fieldName:Int32

}

首先我们通过MachOView查看下descriptor的信息,首先是当前类示例:

截屏2022-01-27 上午11.24.28.png 当前类在Mach-O文件的内存地址(偏移):

截屏2022-01-27 上午11.22.41.png 上图中0xBBA8加上0xFFFFFB18就是当前类CTPersondescriptor在Mach-O文件的内存地址偏移,经过计算结果是0x10000B6C0,减去虚拟机地址0x10000,最终偏移内存地址是0xB6C0。我们在Section(__TEXT,__const)找到descritor的对应的地址,如下图:

截屏2022-01-27 上午11.38.58.png 根据TargetClassDescriptorfieldDescriptor的位置,如下图:

截屏2022-01-27 下午2.32.33.png 我们需要偏移4个4字节的位置才可以找到它,当前fieldDescriptorMach-O文件的内存偏移信息:

截屏2022-01-27 下午2.49.38.png 这里 58 04 00 00 就是fieldDescriptor的偏移信息,所以0xB6D0加上0x0458,计算fieldDescriptor在Mach-O的内存偏移地址是0xBB28。接着我们在Section(__TEXT,__swift_fieldmd)中查看fieldDescriptor信息,如下图所示:

截屏2022-01-27 下午3.01.10.png 找到fieldDescriptor的首地址后,我们最终需要找的属性信息都存储在fieldRecords中,根据我们上面还原的fieldDescriptor结构体内容,知道找到fieldRecords需要偏移4个4字节,那fieldRecordsMach-O中所在的内存地址如下图:

截屏2022-01-27 下午3.09.55.png 接着根据FieldRecord结构体内容,若想找到fieldName,即当前起始位置开始偏移8个字节加上偏移位0xFFFFFFB5就是fieldName在Mach-O的内存偏移地址,那我们计算0xBB38加上0x8加上0xFFFFFFB5最终结果是0x10000BAF5,减去当前虚拟基地址0x10000,那最终fieldNameMach-O中的内存地址就是0xBAF5,我们Section(__TEXT,__swift_rellstr)中可以查看到CTPerson类中nameage的值,如下图:

截屏2022-01-27 下午3.26.35.png