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。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容