Swift属性底层探究

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

存储属性(Stored Property)

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

struct Sequence {
    // 存储属性
    var first: Int
}
占用内存大小

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

let size = MemoryLayout<Sequence>.size              // 8
let stride = MemoryLayout<Sequence>.stride          // 8
let alignment = MemoryLayout<Sequence>.alignment    // 8

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

计算属性(Computed Property)

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

struct Sequence {
    // 存储属性
    var first: Int
    // 计算属性
    var second: Int {
        get {
            return first + 1
        }
        set (value) {
            first -= 1
        }
    }
}
占用内存大小

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

let size = MemoryLayout<Sequence>.size              // 8
let stride = MemoryLayout<Sequence>.stride          // 8
let alignment = MemoryLayout<Sequence>.alignment    // 8

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

方法列表分析
  • 我们使用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. 如果开发者没有给属性添加getset方法,则这个属性被认为是存储属性,编译器再给存储属性自动添加getset方法;
  2. 如果开发者给属性添加getset方法,则这个属性是计算属性。

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

存储属性的getset
func test() {
    var seq = Sequence(first: 1)
    seq.first = 2
    let b = seq.first
}
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

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

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

// Sequence.first.getter
sil hidden [transparent] @$s4main8SequenceV5firstSivg : $@convention(method) (Sequence) -> Int {
 // 省略 
}

// Sequence.second.getter
sil hidden @$s4main8SequenceV6secondSivg : $@convention(method) (Sequence) -> Int {
 // 省略   
}

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

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

@_transparent(扩展内容)

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

var a: Int = 10

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

public struct Int
  : FixedWidthInteger, SignedInteger,
    _ExpressibleByBuiltinIntegerLiteral {
}

extension ExpressibleByIntegerLiteral
  where Self: _ExpressibleByBuiltinIntegerLiteral {
  @_transparent
  public init(integerLiteral value: Self) {
    self = value
  }
}

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

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

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

结论:为了隐藏某些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")
        }
    }
}

先给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
}

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

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

third.setter

third.setter

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

延迟存储属性(Lazy Stored Property)
struct Sequence {
    lazy var fourth: Int = 9
}

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<Int>                  // user: %4
  switch_enum %3 : $Optional<Int>, 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<Int>, #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<Int>              // 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. init(fourth: Int? = nil) // 默认nil, 如果init不给fourth传参,默认就是nil, 如果传参就直接赋值
汇编探究
let size = MemoryLayout<Sequence>.size              // 9
let stride = MemoryLayout<Sequence>.stride          // 16
let alignment = MemoryLayout<Sequence>.alignment    // 8

可选项的本质是枚举类型public enum Optional<Wrapped>: 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
不能置为nil

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

总结
  • 开发者没有写set和或者get的属性就是存储属性,会占用实例对象的内存;编译器默认会为存储属性添加set和或者get方法,这些方法会被内联和隐藏调试信息,比较难以发现它们的存在;
  • 开发者有写set和或者get的属性是计算属性,不会会占用实例对象的内存;本质就是几个方法的组合体;
  • 添加了属性监听器的存储属性,编译器会在set方法中先调用willSet方法,然后调用赋值方法,最后再调用didSet方法, 监听的本质就是set方法中进行了其他方法的调用;
  • 延迟存储属性的本质是可选项,init(property: T? = nil)如果没有传值则默认是nil,获取延迟存储属性时候需要先判断是否为nil,如果为nil需要先赋默认值再返回,如果不为nil则可以直接返回。为了确保有值,不能调用方法设置为`nil。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值