• Swift属性底层探究


    Swift的属性分为存储属性(Stored Property)计算属性(Computed Property),存储属性还有一个懒加载延迟存储属性(Lazy Stored Property),存储属性还能够添加属性监听器(Property Observer),这篇文章我们就来探究下属性背后的实现原理。

    存储属性(Stored Property)

    建一个结构体Sequence, 代码如下:

    struct Sequence {
        // 存储属性
        var first: Int
    }
    
    • 1
    • 2
    • 3
    • 4
    占用内存大小

    我们来看看结构体Sequence的内存大小:

    let size = MemoryLayout.size              // 8
    let stride = MemoryLayout.stride          // 8
    let alignment = MemoryLayout.alignment    // 8
    
    • 1
    • 2
    • 3

    Int占用8个字节,所以说明存储属性会存储在实例的内存中,这个很好理解,因为每个实例对象分别持有一个first属性值。

    计算属性(Computed Property)

    加一个计算属性second,有getset方法

    struct Sequence {
        // 存储属性
        var first: Int
        // 计算属性
        var second: Int {
            get {
                return first + 1
            }
            set (value) {
                first -= 1
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    占用内存大小

    我们来看看结构体Sequence的内存大小:

    let size = MemoryLayout.size              // 8
    let stride = MemoryLayout.stride          // 8
    let alignment = MemoryLayout.alignment    // 8
    
    • 1
    • 2
    • 3

    加上计算属性后,实例大小没有增加,所以说明计算属性是不占用实例对象的存储空间的,这样看来计算属性只是一些方法组成的一个整体。

    方法列表分析
    • 我们使用MachOView来看下Sequence添加second计算属性前后,编译出来的执行文件的方法列表对比:

    没有计算属性时的方法列表

    添加计算属性后的方法列表

    我们看到添加计算属性后确实多了些方法

    • 我们使用swiftc -emit-sil main.swift来看下SIL中间代码
    struct Sequence {
      @_hasStorage var first: Int { get set }
      var second: Int { get set }
      init(first: Int)
    }
    
    // Sequence.first.getter
    sil hidden [transparent] @$s4main8SequenceV5firstSivg : $@convention(method) (Sequence) -> Int {
     // 省略 
    }
    
    // Sequence.first.setter
    sil hidden [transparent] @$s4main8SequenceV5firstSivs : $@convention(method) (Int, @inout Sequence) -> () {
     // 省略 
    }
    
    // Sequence.first.modify
    sil hidden [transparent] @$s4main8SequenceV5firstSivM : $@yield_once @convention(method) (@inout Sequence) -> @yields @inout Int {
     // 省略 
    }
    
    // Sequence.second.getter
    sil hidden @$s4main8SequenceV6secondSivg : $@convention(method) (Sequence) -> Int {
     // 省略   
    }
    
    // Sequence.second.setter
    sil hidden @$s4main8SequenceV6secondSivs : $@convention(method) (Int, @inout Sequence) -> () {
      // 省略   
    } 
    
    // Sequence.second.modify
    sil hidden [transparent] @$s4main8SequenceV6secondSivM : $@yield_once @convention(method) (@inout Sequence) -> @yields @inout Int {
    // 省略 
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    结论:

    1. 如果开发者没有给属性添加getset方法,则这个属性被认为是存储属性,编译器再给存储属性自动添加getset方法;
    2. 如果开发者给属性添加getset方法,则这个属性是计算属性。

    计算属性是通过getset方法进行取值和赋值很好理解,那存储属性也有getset方法,那是不是存储属性也是通过getset方法进行取值和赋值呢?

    存储属性的getset
    func test() {
        var seq = Sequence(first: 1)
        seq.first = 2
        let b = seq.first
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    JJSwift`test():
        0x100003ee4 <+0>:  sub    sp, sp, #0x20             ; =0x20 
        0x100003ee8 <+4>:  stp    x29, x30, [sp, #0x10]
        0x100003eec <+8>:  add    x29, sp, #0x10            ; =0x10 
        0x100003ef0 <+12>: str    xzr, [sp, #0x8]
        0x100003ef4 <+16>: mov    w8, #0x1
        0x100003ef8 <+20>: mov    x0, x8
        0x100003efc <+24>: bl     0x100003ee0           // 结构体实例初始化    
        0x100003f00 <+28>: str    x0, [sp, #0x8]        // seq赋值为1
        0x100003f04 <+32>: mov    w8, #0x2              
    ->  0x100003f08 <+36>: str    x8, [sp, #0x8]        // seq赋值为2
        0x100003f0c <+40>: ldp    x29, x30, [sp, #0x10]
        0x100003f10 <+44>: add    sp, sp, #0x20
        0x100003f14 <+48>: ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    从汇编代码上看,根本没有调用getset方法。那又是为什么呢?

    我们再仔细看看下面的SIR就会发现两个方法还是有区别的:

    // Sequence.first.getter
    sil hidden [transparent] @$s4main8SequenceV5firstSivg : $@convention(method) (Sequence) -> Int {
     // 省略 
    }
    
    // Sequence.second.getter
    sil hidden @$s4main8SequenceV6secondSivg : $@convention(method) (Sequence) -> Int {
     // 省略   
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    没错,存储属性的方法前面有一个[transparent]属性标记。这个标记是不是会让你直接联想到@_transparent

    这个特性标记的函数会进行函数内联,并且会让编译器屏蔽调试信息,也就是说Xcode是没法进入对应的函数源码断点调试,更没法看到对应的函数实现。

    @_transparent(扩展内容)

    为了解释@_transparent特性,我们用一个例子解释下。

    var a: Int = 10
    
    • 1

    我们知道上面这段代码的底层逻辑是因为Int实现了ExpressibleByIntegerLiteral协议,

    public struct Int
      : FixedWidthInteger, SignedInteger,
        _ExpressibleByBuiltinIntegerLiteral {
    }
    
    extension ExpressibleByIntegerLiteral
      where Self: _ExpressibleByBuiltinIntegerLiteral {
      @_transparent
      public init(integerLiteral value: Self) {
        self = value
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    但是如果你想通过对这段源码进行断点调试,很遗憾,断点是不会进入的。

    并且汇编也看不到任何函数调用的痕迹,函数进行了内联。

    0x100003f04 <+8>:  mov    w8, #0xa
    0x100003f08 <+12>: str    x8, [sp, #0x8]
    
    • 1
    • 2

    结论:为了隐藏某些Swift实现,苹果公司真是费劲了心思啊。

    属性监听器(Property Observer)
    struct Sequence {
        // 存储属性
        var first: Int
        // 计算属性
        var second: Int {
            get {
                return first + 1
            }
            set (value) {
                first -= 1
            }
        }
        // 有属性监听器的属性
        var third: Int {
            willSet {
                print("third willSet")
            }
            didSet {
                print("third didSet")
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    先给Sequence添加一个third存储属性,并且添加属性监听器, 我们来探究下为什么属性赋值的时候会调用willSetdidSet?和OCKVO是相同的实现吗?

    SIL探究
    struct Sequence {
      @_hasStorage var first: Int { get set }
      var second: Int { get set }
      @_hasStorage var third: Int { get set }
      init(first: Int, third: Int)
    }
    
    // Sequence.third.willset
    sil private @$s4main8SequenceV5thirdSivw : $@convention(method) (Int, @inout Sequence) -> () {
    // %0 "newValue"                                  // user: %2
    // %1 "self"                                      // user: %3
    // 省略
    }
    
    
    // Sequence.third.didset
    sil private @$s4main8SequenceV5thirdSivW : $@convention(method) (@inout Sequence) -> () {
    // %0 "self"                                      // user: %1
    // 省略
    }
    
    // Sequence.third.getter
    sil hidden [transparent] @$s4main8SequenceV5thirdSivg : $@convention(method) (Sequence) -> Int {
    // %0 "self"                                      // users: %2, %1
    // 省略
    }
    
    // Sequence.third.setter
    sil hidden @$s4main8SequenceV5thirdSivs : $@convention(method) (Int, @inout Sequence) -> () {
    // %0 "value"                                     // users: %10, %6, %2
    // %1 "self"                                      // users: %12, %8, %4, %3
      // function_ref Sequence.third.willset
      %5 = function_ref @$s4main8SequenceV5thirdSivw : $@convention(method) (Int, @inout Sequence) -> () // user: %6
      %12 = begin_access [modify] [static] %1 : $*Sequence // users: %15, %14
      // function_ref Sequence.third.didset
      %13 = function_ref @$s4main8SequenceV5thirdSivW : $@convention(method) (@inout Sequence) -> () // user: %14
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37

    解释:编译器生成的set方法中会先调用willset方法,然后调用modify进行值的修改,然后调用didset方法。

    汇编验证
    func test() {
        var seq = Sequence(first: 1, third: 3)
        seq.third = 4
    }
    
    • 1
    • 2
    • 3
    • 4

    third.setter

    third.setter

    汇编验证得到同样的结论。

    延迟存储属性(Lazy Stored Property)
    struct Sequence {
        lazy var fourth: Int = 9
    }
    
    
    • 1
    • 2
    • 3
    • 4
    SIL探究
    struct Sequence {
      lazy var fourth: Int { mutating get set }
      @_hasStorage @_hasInitialValue var $__lazy_storage_$_fourth: Int? { get set }
      init()
      init(fourth: Int? = nil) // 默认nil
    }
    
    // Sequence.fourth.getter
    sil hidden [lazy_getter] [noinline] @$s4main8SequenceV6fourthSivg : $@convention(method) (@inout Sequence) -> Int {
    // 省略不看版本:可选项不为空,直接返回值,如果为空,进行设值
    // %0 "self"                                      // users: %2, %12, %1
    bb0(%0 : $*Sequence):
      %2 = struct_element_addr %0 : $*Sequence, #Sequence.$__lazy_storage_$_fourth // user: %3
      %3 = load %2 : $*Optional                  // user: %4
      switch_enum %3 : $Optional, case #Optional.some!enumelt: bb1, case   #Optional.none!enumelt: bb2 // id: %4
    
    // %5                                             // users: %7, %6
    bb1(%5 : $Int):                                   // Preds: bb0
      br bb3(%5 : $Int)                               // id: %7
    
    bb2:                                              // Preds: bb0
      %8 = integer_literal $Builtin.Int64, 9          // user: %9
      %9 = struct $Int (%8 : $Builtin.Int64)          // users: %16, %11, %10
      debug_value %9 : $Int, let, name "tmp2"         // id: %10
      %11 = enum $Optional, #Optional.some!enumelt, %9 : $Int // user: %14
      %12 = begin_access [modify] [static] %0 : $*Sequence // users: %15, %13 没有值就设置值
      %13 = struct_element_addr %12 : $*Sequence, #Sequence.$__lazy_storage_$_fourth // user: %14
      store %11 to %13 : $*Optional              // id: %14
      end_access %12 : $*Sequence                     // id: %15
      br bb3(%9 : $Int)                               // id: %16
    
    // %17                                            // user: %18
    bb3(%17 : $Int):                                  // Preds: bb2 bb1 
      return %17 : $Int                               // id: %18  返回值
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    太长不看版:

    1. 延迟存储属性的本质是可选项
    2. 第一次获取的时候会进行判断,如果延迟存储属性值不为空,直接返回值,如果属性值为空,进行设值
    3. init(fourth: Int? = nil) // 默认nil, 如果init不给fourth传参,默认就是nil, 如果传参就直接赋值
    汇编探究
    let size = MemoryLayout.size              // 9
    let stride = MemoryLayout.stride          // 16
    let alignment = MemoryLayout.alignment    // 8
    
    • 1
    • 2
    • 3

    可选项的本质是枚举类型public enum Optional: ExpressibleByNilLiteral {},所以9个字节是合理的,前8个字节存放关联值,1个字节存放枚举类型值。

    
    var seq = Sequence()
    var fourth = seq.fourth
        
    
    JJSwift`Sequence.fourth.getter:
    ->  0x100003aa8 <+0>:  sub    sp, sp, #0x30        
        0x100003aac <+4>:  str    x20, [sp, #0x10]
        0x100003ab0 <+8>:  str    xzr, [sp, #0x28]
        0x100003ab4 <+12>: str    xzr, [sp, #0x20]
        0x100003ab8 <+16>: str    x20, [sp, #0x28]
        0x100003abc <+20>: ldr    x8, [x20]
        0x100003ac0 <+24>: str    x8, [sp, #0x18]
        0x100003ac4 <+28>: ldrb   w8, [x20, #0x8]
        0x100003ac8 <+32>: tbnz   w8, #0x0, 0x100003ae4  
        0x100003acc <+36>: ldr    x8, [sp, #0x18]
        0x100003ad0 <+40>: str    x8, [sp, #0x8]
        0x100003ad4 <+44>: ldr    x8, [sp, #0x8]
        0x100003ad8 <+48>: str    x8, [sp, #0x20]
        0x100003adc <+52>: str    x8, [sp]
        0x100003ae0 <+56>: b      0x100003b00        // 如果有值直接返回
        0x100003ae4 <+60>: ldr    x10, [sp, #0x10]
        0x100003ae8 <+64>: mov    w8, #0x9           // 没有值,开始赋值(初始值)
        0x100003aec <+68>: str    x8, [x10]
        0x100003af0 <+72>: mov    w9, #0x0
        0x100003af4 <+76>: and    w9, w9, #0x1
        0x100003af8 <+80>: strb   w9, [x10, #0x8]
        0x100003afc <+84>: str    x8, [sp]
        0x100003b00 <+88>: ldr    x0, [sp]
        0x100003b04 <+92>: add    sp, sp, #0x30             
        0x100003b08 <+96>: ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    不能置为nil

    延迟存储属性不能用seq.fourth = nil置空,会报错。

    总结
    • 开发者没有写set和或者get的属性就是存储属性,会占用实例对象的内存;编译器默认会为存储属性添加set和或者get方法,这些方法会被内联和隐藏调试信息,比较难以发现它们的存在;
    • 开发者有写set和或者get的属性是计算属性,不会会占用实例对象的内存;本质就是几个方法的组合体;
    • 添加了属性监听器的存储属性,编译器会在set方法中先调用willSet方法,然后调用赋值方法,最后再调用didSet方法, 监听的本质就是set方法中进行了其他方法的调用;
    • 延迟存储属性的本质是可选项,init(property: T? = nil)如果没有传值则默认是nil,获取延迟存储属性时候需要先判断是否为nil,如果为nil需要先赋默认值再返回,如果不为nil则可以直接返回。为了确保有值,不能调用方法设置为`nil。
  • 相关阅读:
    document对象概念介绍、验证用户名是否有效、正则表达式对象、两种常见的验证提示效果
    【牛客面试必刷TOP101】Day11.BM63 跳台阶和 BM67 不同路径的数目(一)
    JavaWeb | 常用的HTML(JavaWeb)标签
    仪表板支持水印设置,数据集新增脱敏规则支持,DataEase开源数据可视化分析平台v1.17.0发布
    windows 下 vs code 格式化代码(clang-format)
    Nacos 自定义扩展的 Data Id 配置
    观察者模式
    【从测试小白到管理】这几点让我终身受益,没有人永远18岁,但永远有人18岁......
    [python 刷题] 238 Product of Array Except Self
    Spring框架-Aop
  • 原文地址:https://blog.csdn.net/lcl130/article/details/126530662