通过SIL了解Swift的延迟存储属性(Lazy)

延迟存储属性(Lazy)

  • 用Lazy修饰的存储属性
  • 延迟存储实行必须有一个默认的初始值
  • 延迟存储属性在第一次访问的时候才被赋值
  • 延迟存储属性并不能保证线程安全
  • 延迟存储属性对实例对象大小有影响
Swift 复制代码
class FFHobbyGirls{
   lazy var age: Int = 20
}
var t = FFHobbyGirls()  
t.age = 30   //此处设置了断点 ①
print("end") //此处设置了断点 ②

当断点停留在①的地方打印FFHobbyGirls的内存结果为

terminal 复制代码
(lldb) x/8g 0x000000010049c730
0x10049c730: 0x00000001000081e8 0x0000000000000002
0x10049c740: 0x0000000000000000 0x0000000000000001 
0x10049c750: 0x5000000010040840 0x0000000000000000
0x10049c760: 0x0000000000000002 0x0002000010049e99

因为lazy关键字的原因,表现为延迟存储属性,并不会占用内存,所以当前0x000000000000000 为0,当过掉了断点①之后,打印FFHobbyGirls的内存结果为

terminal 复制代码
(lldb) x/8g 0x000000010049c730
0x10049c730: 0x00000001000081e8 0x0000000000000002
0x10049c740: 0x000000000000001e 0x0000000000000000
0x10049c750: 0x5000000010040840 0x0000000000000000
0x10049c760: 0x0000000000000002 0x0002000010049e99
(lldb) 

由于完成了t.age = 30 的set操作,age的内存发生了变化,变成了0x000000000000001e

在SIL角度来剖析一下原理: 打开项目文件夹目录

cd /Users/zhou/Desktop/SwiftTwoPractice/SwiftTwoPractice

把 mian.swift编译成main.sil并打开(推荐使用vs code)

swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil No application knows how to open /Users/zhou/Desktop/SwiftTwoPractice/SwiftTwoPractice/main.sil.

重点来了

第一点:

Swift 复制代码
class FFHobbyGirls{
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

当前__lazy_storage_$_age是使用final来修饰的,在访问这个变量的过程中就是在访问这个函数的get方法,接下来找到main函数看一下操作

sil 复制代码
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @main.t : main.FFHobbyGirls         // id: %2
  %3 = global_addr @main.t : main.FFHobbyGirls: $*FFHobbyGirls// users: %7, %8
  %4 = metatype $@thick FFHobbyGirls.Type            // user: %6
  // function_ref FFHobbyGirls.__allocating_init()
  %5 = function_ref @main.FFHobbyGirls.__allocating_init() -> main.FFHobbyGirls: $@convention(method) (@thick FFHobbyGirls.Type) -> @owned FFHobbyGirls// user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick FFHobbyGirls.Type) -> @owned FFHobbyGirls// user: %7
  store %6 to %3 : $*FFHobbyGirls                   // id: %7
  %8 = begin_access [read] [dynamic] %3 : $*FFHobbyGirls// users: %9, %11
  %9 = load %8 : $*FFHobbyGirls                     // users: %16, %14, %15, %10
  strong_retain %9 : $FFHobbyGirls                  // id: %10
  end_access %8 : $*FFHobbyGirls                    // id: %11
  %12 = integer_literal $Builtin.Int64, 30        // user: %13
  %13 = struct $Int (%12 : $Builtin.Int64)        // user: %15
  %14 = class_method %9 : $FFHobbyGirls, FFHobbyGirls.age!setter : (FFHobbyGirls) -> (Int) -> (), $@convention(method) (Int, @guaranteed FFHobbyGirls) -> () // user: %15
  %15 = apply %14(%13, %9) : $@convention(method) (Int, @guaranteed FFHobbyGirls) -> ()
  strong_release %9 : $FFHobbyGirls                 // id: %16
  %17 = integer_literal $Builtin.Int32, 0         // user: %18
  %18 = struct $Int32 (%17 : $Builtin.Int32)      // user: %19
  return %18 : $Int32                             // id: %19
} // end sil function 'main'

在%14的地方,去找到FFHobbyGirls.age!setter方法

sil 复制代码
// FFHobbyGirls.age.setter
sil hidden @main.FFHobbyGirls.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed FFHobbyGirls) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $FFHobbyGirls):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $FFHobbyGirls, let, name "self", argno 2 // id: %3
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  %5 = ref_element_addr %1 : $FFHobbyGirls, #FFHobbyGirls.$__lazy_storage_$_age // user: %6
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  store %4 to %6 : $*Optional<Int>                // id: %7
  end_access %6 : $*Optional<Int>                 // id: %8
  %9 = tuple ()                                   // user: %10
  return %9 : $()                                 // id: %10
} // end sil function 'main.FFHobbyGirls.age.setter : Swift.Int'

把我们设置的参数传递了进来(t.age = 30),看%6的这一行,$*Optional,也就是说修饰Int的类型是Optional,那也意味着,对当前延迟存储来说,在默认编译器生出的结尾也是一个"?"

@_hasStorage @_hasInitialValue final var _lazy_storage$_age: Int? { get set }

在编译器操作的过程当中,默认这哥们是一个可选类型Optional,把一个确定好的值赋值给一个可选类型

第二点:

对于get方法来说的话,基于上面打印的内存信息,在为负值之前内存地址是0x0,那么sil的switch_enum的表现形式应该为Optional.none!结合FFHobbyGirls.age.getter方法看一下:

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

switch_enum %4 : $Optional, case Optional.some!enumelt: bb1, case Optional.none!enumelt: bb2 // id: %6

getter内当case是Optional.none!时候,会进入bb2,那也就意味着进入了bb2代码分支,在我第一次访问的过程中,把age(20)给到了可选类型,也就意味着在第一次访问的过程中从没有值变成了有值的操作

%10 = integer_literal Builtin.Int64, 20 store %13 to %15 : *Optional 综上论述就是懒加载(Lazy),及延迟存储属性的本质

总结: 在我们的底层过程中,首先是一个Optional,在没有被访问之前默认值是nil,在内存的表现是0x0,在第一次访问的过程中,访问的是getter方法,通过enum值得分支进行一个赋值的操作

相关推荐
大熊猫侯佩15 小时前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(五)
swiftui·swift·apple watch
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(三)
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(二)
数据库·swiftui·swift
大熊猫侯佩2 天前
用异步序列优雅的监听 SwiftData 2.0 中历史追踪记录(History Trace)的变化
数据库·swiftui·swift
大熊猫侯佩2 天前
由一个 SwiftData “诡异”运行时崩溃而引发的钩深索隐(一)
数据库·swiftui·swift
season_zhu2 天前
iOS开发:关于日志框架
ios·架构·swift
大熊猫侯佩3 天前
SwiftUI 中如何花样玩转 SF Symbols 符号动画和过渡特效
swiftui·swift·apple
大熊猫侯佩3 天前
SwiftData 共享数据库在 App 中的改变无法被 Widgets 感知的原因和解决
swiftui·swift·apple
大熊猫侯佩3 天前
使用令牌(Token)进一步优化 SwiftData 2.0 中历史记录追踪(History Trace)的使用
数据库·swift·apple
大熊猫侯佩3 天前
SwiftUI 在 iOS 18 中的 ForEach 点击手势逻辑发生改变的解决
swiftui·swift·apple