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
,有get
和set
方法
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 {
// 省略
}
结论:
- 如果开发者没有给属性添加
get
,set
方法,则这个属性被认为是存储属性,编译器再给存储属性自动添加get
,set
方法;- 如果开发者给属性添加
get
,set
方法,则这个属性是计算属性。
计算属性是通过get
,set
方法进行取值和赋值很好理解,那存储属性也有get
,set
方法,那是不是存储属性也是通过get
,set
方法进行取值和赋值呢?
存储属性的get
和set
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
从汇编代码上看,根本没有调用get
和set
方法。那又是为什么呢?
我们再仔细看看下面的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
存储属性,并且添加属性监听器, 我们来探究下为什么属性赋值的时候会调用willSet
和didSet
?和OC
的KVO
是相同的实现吗?
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
}
汇编验证得到同样的结论。
延迟存储属性(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 返回值
}
太长不看版:
- 延迟存储属性的本质是可选项
- 第一次获取的时候会进行判断,如果延迟存储属性值不为空,直接返回值,如果属性值为空,进行设值
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。