Swift 进阶:属性

一、存储属性

1.1 存储属性概述

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

class SSLPerson {
    var age: Int
    var name: String
}
复制代码

比如这里的 agename 就是我们所说的存储属性,这里我们需要加以区分的是 letvar 两者的区别:从定义上:

  • let 用来声明常量,常量的值一旦设置好便不能再被更改
  • var 用来声明变量,变量的值可以在将来设置为不同的值。

1.2 let 和 var 案例

class 案例:

image.png

struct 案例:

image.png

1.3 let 和 var 比较

1.3.1 汇编角度分析

先创建代码:

var age = 18
let x = 20
复制代码

进行汇编调试:

image.png

从汇编调试来看没有区别,都是将值存储到了寄存器中,下面通过 lldb 调试进行分析

扫描二维码关注公众号,回复: 13634575 查看本文章

image.png

lldb 调试来看也没有什么区别,都是存储在了 __DATA.__common 中,而且是相邻的地址。

1.3.2 sil 角度分析

将 main.swift 编译成 main.sil :

@_hasStorage @_hasInitialValue var age: Int { get set }
@_hasStorage @_hasInitialValue let x: Int { get }
...
复制代码
  • 通过 sil 我们可以发现,var 修饰的属性有 get 和 set 方法
  • let 修饰的属性只有 get 方法,所有 let 修饰的属性不能修改。

二、计算属性

2.1 计算属性 概述

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

下面的 area 就是计算属性:

struct Square {
    // 实例中占据内存
    var width: Double
    let height: Double
    
    // 实例中不占用内存
    var area: Double {
        get {
            return width * height
        }
        set {
            self.width = newValue // newValue : 编译器默认生成
        }
    }   
}

var s = Square(width: 10, height: 20)
s.area = 30
复制代码

2.2 计算属性 sil分析

将 main.swift 编译成 main.sil :

struct Square {
  @_hasStorage var width: Double { get set }
  @_hasStorage let height: Double { get }
  var area: Double { get set }
}
复制代码

可以看到计算属性没有 @_hasStorage 的标记

2.3 private(set) 分析

将 area 改为 private(set) 修饰:

struct Square {
    // 实例中占据内存
    var width: Double
    let height: Double
    
    private(set) var area: Double = 40
}
复制代码

生成 sil 文件:

struct Square {
  @_hasStorage var width: Double { get set }
  @_hasStorage let height: Double { get }
  @_hasStorage @_hasInitialValue private(set) var area: Double { get set }
}
复制代码

通过 sil 可以发现,private(set) 修饰后,依然是存储属性,只不过 set 方法是私有的。

三、属性观察者

3.1 属性观察者 分析

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

看下面的代码 willSet 和 didSet 将会被调用:

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"

输出:
subjectName will set value Swift
subjectName has been changed
复制代码

观察者内部是怎么实现的呢,将上面代码编译成 sil :

class SubjectName {
  @_hasStorage @_hasInitialValue var subjectName: String { get set }
}

// SubjectName.subjectName.setter
sil hidden @$s4main11SubjectNameC07subjectC0SSvs : $@convention(method) (@owned String, @guaranteed SubjectName) -> () {
   ...
  // function_ref SubjectName.subjectName.willset
  ...
  // function_ref SubjectName.subjectName.didset
  ...
} 
复制代码

可以看到原来 willsetdidset 都是在 setter 方法中被调用了。

3.2 初始化期间设置属性

  • 这里我们在使用属性观察器的时候,需要注意的一点是在初始化期间设置属性时不会调用 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进阶")
复制代码

为什么会出现这种情况呢,查看 sil 文件:

// SubjectName.init(subjectName:)
sil hidden @$s4main11SubjectNameC07subjectC0ACSS_tcfc : $@convention(method) (@owned String, @owned SubjectName) -> @owned SubjectName {
  ...
  %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
  ...
}
复制代码

通过 sil 文件,我们发现初始化时并没有调用 setter 方法,而是对属性的地址直接进行了赋值,所以监听方法不会被调用。

3.3 计算属性观察者

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

class Square {
    var width: Double
    var area: Double {
        get {
            return width * width
        }
        set {
            self.width = sqrt(newValue)
        }
    }
    init(width: Double) {
        self.width = width
    }
}
复制代码

3.4 继承属性观察者

继承属性下的观察者是什么样的呢,看下面代码:

class SSLTeacher {
    var age: Int {
        willSet{
            print("age will set value \(newValue)")
        } didSet{
            print("age has been changed \(oldValue)")
        }
    }
    var height: Double
    
    init(_ age: Int, _ height: Double) {
        self.age = age
        self.height = height
    }
}

class SSLParTimeTeacher: SSLTeacher {
    
    override var age: Int {
        willSet{
            print("override age will set value \(newValue)")
        } didSet{
            print("override age has been changed \(oldValue)")
        }
    }
    
    var subjectName: String
    
    init(_ subjectName: String) {
        self.subjectName = subjectName
        super.init(10, 180)
        self.age = 20
    }
}

var t = SSLParTimeTeacher("Swift")
复制代码

运行程序,可以得到继承属性下方法的执行顺序:

override age will set value 20   // 子类的 will
age will set value 20            // 父类的 will
age has been changed 10          // 父类的 did
override age has been changed 10 // 子类的 did
复制代码

四、延迟存储属性

4.1 延迟存储属性的使用

  • 用关键字 lazy 来标识一个延迟存储属性,延迟存储属性必须有初始值:

    class Person {
        lazy var age: Int = 18
    }
    复制代码
  • 延迟存储属性的初始值在其第一次使用时才进行计算

    • 如下所示,初始化以后 age 是没有值的:

      image.png

    • 调用 age 的 getter 方法后,可以看到 age 已经有值了:

      image.png

4.2 sil 原理探索

4.2.1 初始化过程

首先生成 sil 文件,观察 age 的声明是有个 符号的,说明 age 是个可选类型

image.png

再看 age 的初始化,默认是被赋值了 Optional.none 也就是 0

image.png

4.2.2 调用过程

查看 age 的 getter 方法,看下它的调用过程:

// Person.age.getter
sil hidden [lazy_getter] [noinline] @$s4main6PersonC3ageSivg : $@convention(method) (@guaranteed Person) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Person):
  debug_value %0 : $Person, let, name "self", argno 1 // id: %1
  %2 = ref_element_addr %0 : $Person, #Person.$__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
  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

bb2:                                              // Preds: bb0
  %10 = integer_literal $Builtin.Int64, 18        // 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 : $Person, #Person.$__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
复制代码
  • getter 方法调用过程先会获取到 age 的地址,看它有没有值
  • 如果没有值也就是 Optional.none ,就会调用 bb2,这里会得到值并赋值到 age 的地址空间
  • 如果有值,就会调用 bb1,将值直接返回
  • 所以延迟存储属性并不是线程安全的。

五、类型属性

5.1 类型属性初探

  • 类型属性其实就是一个全局变量
  • 类型属性只会被初始化一次
class SSLTeacher {
    // 只被初始化一次
    static var age: Int = 18
}

// 可以修改
SSLTeacher.age = 30
复制代码

5.2 sil & 源码分析

生成 sil 文件,初始化调用时可以看到 builtin "once"的调用

image.png

builtin "once"的调用在源码中就是 swift_once 的调用,打开 swift 源码,找到 swift_once 的实现:

void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#ifdef SWIFT_STDLIB_SINGLE_THREADED_RUNTIME
  if (! *predicate) {
    *predicate = true;
    fn(context);
  }
#elif defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}
复制代码

源码的实现可以看到dispatch_once_f,这不是就是我们的 GCD 吗!!

5.3 单例的实现

class SSLTeacher {
    
    static let sharedInstance = SSLTeacher()
    
    // 指定初始化器私有化,外界访问不到
    private init(){}
}

SSLTeacher.sharedInstance
复制代码

六、属性与 Mach-O

6.1 fieldDescriptor && FieldRecord

上一篇文章 探索方法调度的过程中我们认识了 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
    //V-Table
}
复制代码

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
}
复制代码

6.2 属性在 Mach-O 文件的位置信息

先创建一个类,编译出 Mach-O 文件

class SSLTeacher {
    var age = 18
    var age2 = 20
}
复制代码

接下来在 Mach-O 文件中,计算并寻找属性的相关信息

  • 先计算出 typeDescriptor 在 Mach-O 中的地址

    FFFFFF2C + 3F40 = 0x100003E6C
    0x100003E6C - 0x100000000 = 3E6C
    复制代码

    image.png

  • 定位到 3E6C

image.png

  • 平移 4 个 4 字节,找到 fieldDescriptor,并计算 fieldDescriptor 的地址

    image.png

    3E7C + 9C = 3F18
    复制代码
  • 定位 fieldDescriptor, 并偏移 4 个 4 字节找到 FieldRecords

    image.png

  • 计算 age 的 FieldName 在 Mach-O 中的地址

    image.png

    3F30 + FFFFFFDF = 0x100003F0F
    0x100003F0C - 0x1000000000 = 3F0F
    复制代码
  • 成功定位 age 的 FieldName 在 Mach-O 中的位置!!

    image.png

猜你喜欢

转载自juejin.im/post/7048595981201309732