Swift-属性

280 阅读9分钟

存储属性

存储属性是一个作为特定类和结构体实例一部分的常量或变量。存储属性要么是变量存储属性(由var关键字引入),要么是常量存储属性(由let关键字引入)。存储属性这里没有什么特别要强调的,因为随处可⻅,先看下面的定义:

class ATTeacher {
    var age: Int
    var name: String
}

比如这里的agename就是我们所说的存储属性,这里需要加以区分的是letvar两者的区别,从定义上:let用来声明常量,常量的值一旦设置好便不能再被更改;var用来声明变量,变量的值可以在将来设置为不同的值。

示例

再看下面的示例: 01.png

汇编代码分析

我们在main.swift文件分别用varlet定义。

var age = 18
let height = 20

然后以汇编的模式打个断点,连接真机运行: 02.png0x12放入w8寄存器,把0x14放在w8寄存器中,从汇编的角度来分析,两者没有什么区别。在打印age的地址信息可看出它存放在Mach-O文件数据区__DATA.__commonSection里,以8字节格式化(x/8g)打印输出前8字节存放age,后8字节存放heightproperty.gif

SIL分析

把上面的声明直接导出SIL文件,生成的定义如下:

@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue let height: Int { get }

可以看出2个都是存储属性,只是用var声明的有get方法和set方法,而通过let声明的只有get方法。因此用let声明属性赋值后就不能再更改了。

计算属性

存储的属性是最常⻅的,除了存储属性,类、结构体和枚举也能够定义计算属性,计算属性并不存储值,他们提供gettersetter来修改和获取值。对于存储属性来说可以是常量或变量,但计算属性必须定义为变量。于此同时我们书写计算属性时必须包含类型,因为编译器需要知道期望返回值是什么。

struct square {
    // 存储属性:实例当中占据内存 (Double:8字节)
    var width: Double
    
    // 计算属性
    var area: Double {
        get {
            return width * width
        }
        set {
            self.width = newValue // newValue: 编译器默认给的新值名称
        }
    }
}

实例中就定义了计算属性的方法,需要提供getset方法,再看一下下面这个例子:

struct square {
    var width: Double = 30
    var area: Double {
        get {
            return width * width
        }
    }
    let height: Double = 20
}

当前这个struct和上面的有什么区别呢,我们通过生成SIL文件看一下

struct square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get }
  @_hasStorage @_hasInitialValue let height: Double { get }
  init()
  init(width: Double = 30)
}

areaheight相同点是都没有set方法,但本质是有区别的,area本质是方法,而height是存储值。

属性观察者

属性观察者会观察用来观察属性值的变化,一个willSet当属性将被改变调用,即使这个值与原有的值相同,而didSet在属性已经改变之后调用。它们的语法类似于gettersetter

存储属性

class SubjectName {
    var subjectName: String = ""{
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet {
            print("subjectName has been changed \(oldValue)")
        }
    }
}

let s = SubjectName()
s.subjectName = "Swift"

以上代码运行就调用了subjectNamewillSetdidSet方法,可以看到输出了print的打印信息。 03.png 再把上面代码编译成SIL文件,然后找到它的setter方法的定义。 04.png 从编译的SIL文件可以看到,在setter方法里先调用了willset,然后设值,再触发了didset。这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用willSetdidSet观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这段代码,你会发现当前并不会有任何的输出。

class SubjectName {
    var subjectName: String = "" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet {
            print("subjectName has been changed \(oldValue)")
        }
    }

    init(subjectName: String) {
        self.subjectName = subjectName
    }
}

let s = SubjectName(subjectName: "Swift")

sub.gif 再把上面代码编译成SIL文件,然后找到init方法 05.png 可以看出init方法没有调用willsetdidset方法,而是直接把subjectName拷贝到内存中。

计算属性

上面的属性观察者只是对存储属性起作用,如果想对计算属性起作用,只需将相关代码添加到属性的setter中。代码如下:

struct square {
    var width: Double
    
    var area: Double {
        get {
            return width * width
        }
        set {
            // 这里添加观察代码
            self.width = newValue
        }
    }

    init(width: Double) {
        self.width = width
    }
}

set方法里设值之前添加类似willset方法,设值之后添加类似didset的代码即可。

继承

当在继承的时候,属性的观察者的调用方式是什么样的?下面通过一个示例来看一下调用流程。 06.png 从打印日志可以看出:

  1. 调用子类的的willset
  2. 调用父类的willset
  3. 调用父类的didset
  4. 调用子类的didset

延迟存储属性

延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性。在属性声明前使用lazy来标示一个延迟存储属性。

定义一个延迟存储属性,代码如下:

class Subject {
    lazy var age: Int = 18
}

var s = Subject()
print(s.age)

在print(s.age)打个断点,然后运行 07.png16字节存的是metadatarefCount,然后才存age,这里看到age还没有值,我们再往下走一个断点,再输出内存地址看看结果。 08.png 这时可以看到已经读到了内存中age的值。

SIL分析

我们再通过SIL文件分析一下,还是把上面代码编译成SIL文件,然后找到它的定义: 09.png 可以看到用lazy修饰的属性编译出来的是个可选类型,而且是final修饰的。再看一下__lazy_storage_的初始化表达式 10.png __lazy_storage_是个枚举,默认是Optional.none,相当于nil。然后再看一下agegetter方法。 11.pnggetterSIL代码可以看到,延迟存储属性大概流程如下:

  1. 当初始化时会给个默认值nil
  2. 第一次访问时由于没有值走bb2代码块
  3. bb2: 把当前的值构建出来给到枚举变量,存储在__lazy_storage_的地址中
  4. 第二次访问有值就不需要做赋值操作,调用bb1代码块,直接从原有的值返回出去 如果延迟存储属性存在闭包表达式,修饰的是个引用类型,在实际开发中就可以节省内存,只有在需要的时候才调用。还有一点,延迟存储属性不是线程安全的。

类型属性

类型属性用于定义某个类型所有实例共享的数据,比如所有实例都能用的一个常量(就像C语言中的静态常量),或者所有实例都能访问的一个变量(就像C语言中的静态变量)。

  • 类型属性其实就是一个全局变量
  • 类型属性只会被初始化一次

先看一下类型属性的写法,其实就是在存储属性前加个static关键字:

// 定义
class ATTeacher {
    static var age: Int = 18
}

// 类型属性的访问
ATTeacher.age = 20

还是把它编译成SIL文件,定义如下:

class ATTeacher {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}

// one-time initialization token for age
sil_global private @$s4main9ATTeacherC3age_Wz : $Builtin.Word

// static ATTeacher.age
sil_global hidden @$s4main9ATTeacherC3ageSivpZ : $Int

可以看出age是个全局变量。再看一下main函数的定义: 13.png 这里有个函数地址,再搜索一下可以看到下面的定义: 12.png 所以这个ATTeacher.age.unsafeMutableAddressor实际返回的就是全局变量的地址。再看一下上面调用函数的定义: 14.png 从上面的函数调用可以看到有builtin "once",再把上面的代码编译成IR,可以找到下面这段代码: 15.png 通过xcrun swift-demangle命令还原一下上面的方法: 16.png 可以看到就是上面的age.unsafeMutableAddressor,这时调用了swift_once,我们在Swift源码中搜索一下,在Once.cpp中找到相关的定义: 17.png swift_once实际上也是使用了GCDdispatch_once函数,所以static修饰的本质上也是只调用一次。因此在Swift中创建单例代码就很简单了,示例如下:

class ATTeacher {
    static let sharedInstance = ATTeacher()
    private init(){}
}

在Mach-O的位置信息

Swift-类与结构体(下)已经分析了Metadata的数据结构,这里再回顾一下:

struct Metadata {
    var kind: Int
    var superClass: Any.Type
    var cacheData: (Int, Int)
    var data: Int
    var classFlags: Int32
    var instanceAddressPoint: UInt32 
    var instanceSize: UInt32
    var instanceAlignmentMask: UInt16
    var reserved: UInt16
    var classSize: UInt32
    var classAddressPoint: UInt32
    var typeDescriptor: UnsafeMutableRawPointer 
    var iVarDestroyer: UnsafeRawPointer
}

上篇在方法调度的过程中我们认识了typeDescriptor,里面记录了V-Table的相关信息,接下来我们需要认识一下typeDescriptor中的fieldDescriptor

struct TargetClassDescriptor { 
    var flags: UInt32
    var parent: UInt32
    var name: Int32
    var accessFunctionPointer: Int32
    var fieldDescriptor: Int32
    var superClassType: Int32
    var metadataNegativeSizeInWords: UInt32 
    var metadataPositiveSizeInWords: UInt32 
    var numImmediateMembers: UInt32
    var numFields: UInt32
    var fieldOffsetVectorOffset: UInt32 
    var Offset: UInt32
    var size: UInt32
    // VTable
}

fieldDescriptor记录了当前的属性信息,其中fieldDescriptor在源码中的结构如下:

struct FieldDescriptor {
    MangledTypeName: Int32
    Superclass: Int32
    Kind: UInt16
    FieldRecordSize: UInt16
    NumFields: UInt32
    FieldRecords: [FieldRecord]
}

其中NumFields代表当前有多少个属性,FieldRecords记录了每个属性的信息,FieldRecords的结构如下:

struct FieldRecord {
    Flags UInt32 
    MangledTypeName Int32
    FieldName Int32
}

我们结合示例来验证一下。

示例

首先在main.swift定义一个类,定义2个属性。代码如下:

class ATTeacher {
    var age = 18
    var age1 = 20
}

然后编译,把编译后的可执行文件拖入到MachOView工具中。 18.png 上一篇中已经介绍了TargetClassDescriptor存放在Section64(__TEXT,__swift5_types)中,这里计算一下TargetClassDescriptorMach-O文件中的内存地址

0xFFFFFF2C + 0x3F40 = 0x100003E6C
# 减去虚拟基地址
0x100003E6C - 0x100000000 = 0x3E6C

0x3E6C就是TargetClassDescriptor的地址,在Section64(__TEXT,__const)找到了0x3E6C,也就是0x3E68偏移4个字节,根据fieldDescriptorTargetClassDescriptor的结构位置,需要往后偏移4个4字节 19.png

# FieldDescriptor的首地址
0x3E7C + 0x9C = 0x3F18

计算得到的结果我们在Section64(__TEXT,__swift5_fieldmd)找到了,根据FieldDescriptor的结构,要得到FieldRecords的值,需要偏移4个4字节 20.png 根据FieldRecords的结构,要得到FieldName的地址,需要偏移8个字节,然后根据地址+偏移量得出

0x3F28 + 0x8 + 0xFFFFFFDD = 0x100003F0D
# 减去基地址
0x100003F0D - 0x100000000 = 0x3F0D

Section64(__TEXT,__swift5_reflstr)就找到了0x3F0D的地址,也就找到了变量ageage1对应的地址信息。 21.png 以上就是通过示例代码结合Mach-O文件计算得出变量在内存中的地址信息。

总结

本篇主要分析了不同类型属性的定义,以及通过SIL代码了解不同属性的异同,结合案例去验证;最后通过编译导出的Mach-O文件去计算属性存储在内存中的位置信息。