Swift 中属性介绍

291 阅读9分钟

一. 存储属性

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

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

 从定义上区分 var 与 let

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

这里我们来看几个案例:

image.png

 从汇编的角度分析 var 与 let 的区别

 var age = 20

let x = 10

print(age,x)

image.png

这里通过汇编调试可以看到,14 行与 18 行分布代表将 0x14 存入 w8 跟 将 0xa 存入 w8,可以看出在汇编上 varlet 并没有区别。

image.png

通过内存的读取也可以看到 agex 就相差 8 个字节,并没有别的区别。

 从 SIL 的角度分析 var 与 let 的区别

  • swift 代码
var age = 20

let x = 10
  • sil 代码
// _hasStorage 代表是存储属性,_hasInitialValue 代表都有初始值,{ get set } : 编译器默认会给存储属性生成 get 跟 set 方法,当我们访问存储属性的时候就是访问 get 方法,当我们改变属性的值的时候就是访问 set 方法
@_hasStorage @_hasInitialValue var age: Int { get set }
// 当用 let 修饰的时候,编译器没有生成 set 方法,所以修改属性值的时候编译器会报错
@_hasStorage @_hasInitialValue let x: Int { get }

通过 sil 代码可以看到,varlet 其实也是一种语法糖,let 修饰的属性不会生成 set 方法。

二. 计算属性

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

  • swift 代码
struct square {
    // 在实例当中需要占用内存空间
    var width: Double
    var hegith: Double
    
    // 代表将 set 方法私有,只能在结构体内部通过 self.area 修改 area 的值
//    private(set) var area: Double
    
    var area: Double {
        get {
            return width * hegith
        }
        // newValue 是编译器默认生成的,自己也可以通过 set(自定义名称) {} 修改
        set {
            self.width = newValue
        }
    }
}
  • sil 代码
struct square {
  @_hasStorage var width: Double { get set }
  @_hasStorage var hegith: Double { get set }
  var area: Double { get set }
  init(width: Double, hegith: Double)
}

通过 sil 代码可以看到,计算属性的本质就是 setget 方法。

三. 属性观察者

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

  • swift 代码
class SubjectName {
    var subjectName: String = "" {
        willSet {
            print("subjectName will set value \(newValue)")
        }
        didSet {
            print("subjectName has been changed \(oldValue)") }
        }
}
  • sil 代码
// 在赋值之前会调用 willset 方法
   // function_ref SubjectName.subjectName.willset
  %10 = function_ref @$s4main11SubjectNameC07subjectC0SSvw : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %11
  %11 = apply %10(%0, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()

// 给 subjectName 属性设置新的值
  retain_value %0 : $String                       // id: %12
  %13 = ref_element_addr %1 : $SubjectName, #SubjectName.subjectName // user: %14
  %14 = begin_access [modify] [dynamic] %13 : $*String // users: %16, %15, %18
  %15 = load %14 : $*String                       // user: %17
  store %0 to %14 : $*String                      // id: %16
  release_value %15 : $String                     // id: %17
  end_access %14 : $*String                       // id: %18

// 在赋值之后会调用 didset 方法
  // function_ref SubjectName.subjectName.didset
  %19 = function_ref @$s4main11SubjectNameC07subjectC0SSvW : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> () // user: %20
  %20 = apply %19(%6, %1) : $@convention(method) (@guaranteed String, @guaranteed SubjectName) -> ()

这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 willSetdidSet 观察者;只有在为完全初始化的实例分配新值时才会调用它们。运行下面这 段代码,你会发现当前并不会有任何的输出。

  • swift 代码
class SubjectName{
    var subjectName: String = "[unnamed]"{
        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")
  • sil 代码
// SubjectName.subjectName.getter
sil hidden [transparent] @$s4main11SubjectNameC07subjectC0SSvg : $@convention(method) (@guaranteed SubjectName) -> @owned String {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $SubjectName):
  debug_value %0 : $SubjectName, let, name "self", argno 1 // id: %1
// 拿到 subjectName 这个属性的内存地址
  %2 = ref_element_addr %0 : $SubjectName, #SubjectName.subjectName // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*String // users: %4, %6
// 将要赋值的字符串的值直接拷贝到内存地址,并没有调用 setter 方法
  %4 = load %3 : $*String                         // users: %7, %5
  retain_value %4 : $String                       // id: %5
  end_access %3 : $*String                        // id: %6
  return %4 : $String                             // id: %7
} // end sil function '$s4main11SubjectNameC07subjectC0SSvg'

通过 sil 代码可以看到,这个时候是直接将字符串的值拷贝到 subjectName 属性的内存地址,并没有调用 setter 方法。编译器这样做的原因可能是这个时候有些属性并没有初始化完成,通过 setter 方法赋值可能会造成内存错误。

上面的属性观察者只是对存储属性起作用,如果我们想对计算属性起作用怎么办?很简单,只需将相关代码添加到属性的 setter。我们先来看这段代码:

image.png

这里可以看到在计算属性中添加 willSetdidSet 会报错,因为这里已经实现 set 方法了,可以直接在 set 中属性赋值前跟属性赋值后进行添加监听。

如果子类继承于父类的情况下,willSetdidSet 方法调用是什么样的呢?

image.png

这里可以看到当子类继承父类的时候,修改子类实例对象 age 属性的值会先调用子类的 willSet 方法,然后调用父类的 willSet,赋值完成后会先调用父类的 didSet 方法,然后再调用子类的 didSet 方法。

四. 延迟存储属性

  • 延迟存储属性的初始值在其第一次使用时才进行计算。
  • 用关键字 lazy 来标识一个延迟存储属性。

image.png

通过案例可以看到,在访问 age 属性之前内存中的值是 0,当访问之后才会对内存空间进行初始化。

  • sil 代码
class CXPerson {
  lazy var age: Int { get set }
// 这里延时属性本质上是可选类型
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

 // variable initialization expression of CXPerson.$__lazy_storage_$_age
sil hidden [transparent] @$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi : $@convention(thin) () -> Optional<Int> {
// Optional.none 相当于 nil,初始化的时候 age 为 0
bb0:
  %0 = enum $Optional<Int>, #Optional.none!enumelt // user: %1
  return %0 : $Optional<Int>                      // id: %1
} // end sil function '$s4main8CXPersonC21$__lazy_storage_$_age029_12232F587A4C5CD8B1EEDF696793G2FCLLSiSgvpfi'
// 访问 age 的时候会调用 getter 方法
// CXPerson.age.getter
sil hidden [lazy_getter] [noinline] @$s4main8CXPersonC3ageSivg : $@convention(method) (@guaranteed CXPerson) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $CXPerson):
  debug_value %0 : $CXPerson, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %3
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  %4 = load %3 : $*Optional<Int>                  // user: %6
  end_access %3 : $*Optional<Int>                 // id: %5
// 这里会进行枚举判断,当age有值的时候走 bb1,没值的时候走 bb2
  switch_enum %4 : $Optional<Int>, case #Optional.some!enumelt: bb1, case #Optional.none!enumelt: bb2 // id: %6

// 返回原因的值
// %7                                             // users: %9, %8
bb1(%7 : $Int):                                   // Preds: bb0
  debug_value %7 : $Int, let, name "tmp1"         // id: %8
  br bb3(%7 : $Int)                               // id: %9

// 构建一个 Int 类型的值并存储到 age 属性的内存地址
bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 6         // user: %11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  %14 = ref_element_addr %0 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %15
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  store %13 to %15 : $*Optional<Int>              // id: %16
  end_access %15 : $*Optional<Int>                // id: %17
  br bb3(%11 : $Int)                              // id: %18

这里延迟属性也可以理解为懒加载,我们使用 lazy 修饰属性的时候可以帮我们节省内存空间。但是延迟属性不能保证变量只能被访问一次,因为会涉及到多个线程同时访问的情况,所以并不是线程安全的。

内存独占

  • swift 代码
class CXPerson {
    lazy var age: Int = 6
//    lazy var age: Int {
//        return 6
//    }()
}

var p = CXPerson()
//
let t = p.age
p.age = 30
  • sil 代码
// CXPerson.age.setter
sil hidden @$s4main8CXPersonC3ageSivs : $@convention(method) (Int, @guaranteed CXPerson) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $CXPerson):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $CXPerson, let, name "self", argno 2 // id: %3
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  %5 = ref_element_addr %1 : $CXPerson, #CXPerson.$__lazy_storage_$_age // user: %6
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  store %4 to %6 : $*Optional<Int>                // id: %7
  end_access %6 : $*Optional<Int>                 // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} 

在我们修改 age 属性的前后会调用 begin_accessend_access,保证在赋值的过程中独占内存,也是为了内存访问安全。

五. 类型属性

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

下面我们通过将以下 swift 代码转成 sil 代码来看一下。

  • swift 代码
class CXPerson {
    static var age: Int = 18
}

CXPerson.age = 30
  • sil 代码、IR 代码、源码

下面我们通过阅读 sil 代码、IR 代码及源码,看一下类型属性的底层实现步骤。

  1. CXPerson 声明
class CXPerson {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}
// one-time initialization token for age
sil_global private @$s4main8CXPersonC3age_Wz : $Builtin.Word
// static CXPerson.age
sil_global hidden @$s4main8CXPersonC3ageSivpZ : $Int

CXPerson 类的声明中 age 还是一个存储属性,只是多了 static 修饰。而且 age 会被声明成一个全局变量。

  1. age 属性的访问
  // function_ref CXPerson.age.unsafeMutableAddressor
  %3 = function_ref @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer // user: %4
  %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5

这里注释说明可以看出这里是通过 CXPerson.age 的内存地址进行访问,所以我们搜索 s4main8CXPersonC3ageSivau

// CXPerson.age.unsafeMutableAddressor
sil hidden [global_init] @$s4main8CXPersonC3ageSivau : $@convention(thin) () -> Builtin.RawPointer {
bb0:
// 通过步骤 1 中声明的 token,拿到 age 属性的内存地址,并把地址赋值给 %1
  %0 = global_addr @$s4main8CXPersonC3age_Wz : $*Builtin.Word // user: %1
// 指针类型转换 
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref one-time initialization function for age 调用 initialization 函数,并把函数地址赋值给 %2
  %2 = function_ref @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () // user: %3
 // 在这里传入 %1 %2 作为参数
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
 //拿到全局变量的内存地址
  %4 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %5
 //将全局变量的内存地址转为原生指针
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
 // 这里返回上面转换得到的原生指针
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function '$s4main8CXPersonC3ageSivau'

 通过 sil 代码可以看到,在 CXPerson 类的声明中 age 还是一个存储属性,只是多了 static 修饰。而且 age 会被声明成一个全局变量。

  1. 找到 initialization 函数 xcrun swift-demangle 命令执行结果:
xcrun swift-demangle s4main8CXPersonC3ageSivpZ
$s4main8CXPersonC3ageSivpZ ---> static main.CXPerson.age : Swift.Int
// one-time initialization function for age
sil private [global_init_once_fn] @$s4main8CXPersonC3age_WZ : $@convention(c) () -> () {
bb0:
// 创建全局变量 age 
  alloc_global @$s4main8CXPersonC3ageSivpZ        // id: %0
// 获取到全局变量内存地址
  %1 = global_addr @$s4main8CXPersonC3ageSivpZ : $*Int // user: %4
// 构建 Int 类型的结构体
  %2 = integer_literal $Builtin.Int64, 18         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %4
// 初始化 age 变量 
  store %3 to %1 : $*Int                          // id: %4
  %5 = tuple ()                                   // user: %6
  return %5 : $()                                 // id: %6
} // end sil function '$s4main8CXPersonC3age_WZ'
  1. swift 代码转为 IR 代码并找到 once
define hidden swiftcc i8* @"$s4main8CXPersonC3ageSivau"() #0 {
entry:
  %0 = load i64, i64* @"$s4main8CXPersonC3age_Wz", align 8
  %1 = icmp eq i64 %0, -1
  %2 = call i1 @llvm.expect.i1(i1 %1, i1 true)
  br i1 %2, label %once_done, label %once_not_done

once_done:                                        ; preds = %once_not_done, %entry
  %3 = load i64, i64* @"$s4main8CXPersonC3age_Wz", align 8
  %4 = icmp eq i64 %3, -1
  call void @llvm.assume(i1 %4)
  ret i8* bitcast (%TSi* @"$s4main8CXPersonC3ageSivpZ" to i8*)

once_not_done:                                    ; preds = %entry
//这里可以看到会调用 swift_once 函数
  call void @swift_once(i64* @"$s4main8CXPersonC3age_Wz", i8* bitcast (void ()* @"$s4main8CXPersonC3age_WZ" to i8*), i8* undef)
  br label %once_done
}

通过 xcrun swift-demangle 命令可以看到 s4main8CXPersonC3ageSivau 就是 sil 代码中的 unsafeMutableAddressor

xcrun swift-demangle s4main8CXPersonC3ageSivau
$s4main8CXPersonC3ageSivau ---> main.CXPerson.age.unsafeMutableAddressor : Swift.Int
  1. 在源码中搜索 swift_once 函数。

image.png

通过源码可以看到,全局变量本质上还是使用了 GCDdispatch_once_f,确保类型属性只会被初始化一次,但是可以在外部修改类型属性的值。

swift 中单例写法

final class CXPerson {
    static let sharedInstance = CXPerson()
    
    private init() {} 
}

类型方法

class CXPerson {
    static func staticFunc() {
        print("staticFunc")
    }

    class func classFunc() {
        print("staticFunc")
    }
}

CXPerson.staticFunc()
CXPerson.classFunc()

image.png

以上在方法前用 static 修饰是类型方法的写法,通过汇编可以看到 static 修饰的方法跟 class 修饰的方法调度的时候都是通过静态派发的方式。

sil_vtable CXPerson {
  #CXPerson.classFunc: (CXPerson.Type) -> () -> () : @$s4main8CXPersonC9classFuncyyFZ	// static CXPerson.classFunc()
  #CXPerson.init!allocator: (CXPerson.Type) -> () -> CXPerson : @$s4main8CXPersonCACycfC	// CXPerson.__allocating_init()
  #CXPerson.deinit!deallocator: @$s4main8CXPersonCfD	// CXPerson.__deallocating_deinit
}

将以上代码转成 sil 代码可以看到,class 修饰的方法会被注册到 vtable 中,这也是 class 修饰的方法能被子类重写的原因。

image.png

如图可以看到 static 修饰的方法在子类中重写会报错。

image.png

这里可以看到 class 不能用来修饰 struct(值)类型。

六. 属性在 MachO 文件中的位置信息

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
}

Swift 中类与结构体(二)中讲到方法调度的过程中我们认识了 typeDescriptor,这里面记录了 V-Table 的相关信息,接下来我们需要认识一下 typeDescriptor 中的 fieldDescripto

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
    //V-Table
}

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

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

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

struct FieldRecord{
    var Flags: uint32
    var MangledTypeName: Int32
    var FieldName: Int32
}

 基于以上认知,下面我们来看一下相关属性在 MachO 中对应的信息。

class CXPerson {
    var age = 10
    var age1 = 20
}

将以上代码编译后的可执行文件用 MachOView 工具打开。

image.png

这里 0xFFFFFE78 + 0x00003EFC - 0x100000000 = 3D74 就是 CXPersontypeDescriptorMachO 文件中的位置。

image.png

_TEXT, _const 文件中找到 3D74,向后偏移 4 个字节之后就是 fieldDescriptorMachO 中的信息,所以 0x00003D84 + 0x00000150 = 0x3ED4 就是 typeDescriptorfieldDescriptor 属性在 MachO 文件中的位置。

image.png

_TEXT,__swift5_fieldmd 文件中可以看到 FieldDescriptor 的结构体的首地址就是 0x3ED4,所以 0x3ED4 向后偏移 4 个字节之后的连续存储空间存储的就是 FieldRecords 属性的信息。所以 0x3EEC + FFFFFFDD - 0x100000000 = 0x3EC9 就是 FieldNameMachO 中的信息。

image.png

FieldName 代表属性的名称,如上图所示,0x3EC9 位置确实存储的就是 CXPerson 类的属性名称,0x00656761 就是 age 的 16 进制,0x31656761 就是 age1 的 16 进制。