Swift 属性

Swift 属性

Swift中属性主要分为存储属性计算属性延迟存储属性类型属性这四种,并且Swift还提供了属性观察者,以便开发者能在属性的改变前后进行观察。下面我就来一一探索。

1. 存储属性

存储属性顾名思义,就是需要存储的一种属性,既然存储就要占用内存空间,下面我们通过一个例子来对其进行解释。

示例代码:

class Teacher {
    let age: Int = 18
    var age2: Int = 100
}

let t = Teacher()

我们通过我的上一篇文章对Swift的类和对象的探索,我们可以知道Swift对象的存储前16个字节是metadatarefCounts

1.1 通过lldb查看存储属性

下面我们就通过lldb调试来查看一下示例代码中t这个对象的内存结构。

16086182160555.jpg

通过lldb调试我们可以看到,我们的两个age的值都存储在了连续的对象存储空间中(注:这里是16进制)

1.2 通过SIL文件查看存储属性

我们使用如下命令将我们的swift文件编译成sil文件,并打开(注:如果打开失败就设置一下用VSCode打开sil类型的文件就可以了)

swiftc -emit-sil main.swift >> ./main.sil && open main.sil

1.2.1 类

编译后的SIL代码:

class Teacher {
  @_hasStorage @_hasInitialValue final let age: Int { get }
  @_hasStorage @_hasInitialValue var age2: Int { get set }
  @objc deinit
  init()
}

sil代码中我们可以看到属性的前面加上了@_hasStorage关键字,这是存储属性独有的,对应的是计算属性,就没有这个关键字。

1.2.2 get&set

get方法及分析:

// Teacher.age.getter
sil hidden [transparent] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self"                                      // users: %2, %1
bb0(%0 : $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  // 找出实例内部物理实例变量的地址
  %2 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %3
  // 开始访问内存
  %3 = begin_access [read] [dynamic] %2 : $*Int   // users: %4, %5
  // 内存中的值加载到%4虚拟寄存器
  %4 = load %3 : $*Int                            // user: %6
  // 结束内存访问
  end_access %3 : $*Int                           // id: %5
  // 返回获取到的值
  return %4 : $Int                                // id: %6
} // end sil function 'main.Teacher.age.getter : Swift.Int'

set方法及分析:

// Teacher.age.setter
sil hidden [transparent] @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value"                                     // users: %6, %2
// %1 "self"                                      // users: %4, %3
bb0(%0 : $Int, %1 : $Teacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
  // 找出实例内部物理实例变量的地址
  %4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
  // 开始访问内存
  %5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
  // 将第一个参数%0的值存储到%5
  store %0 to %5 : $*Int                          // id: %6
  // 停止内存访问
  end_access %5 : $*Int                           // id: %7
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function 'main.Teacher.age.setter : Swift.Int'

1.3 小结

打印内存占用大小:

16086275671125.jpg

对象的内存结构

16086252349746.jpg

这个32其实就是8+8+8+8得到的,在Swift中对象的本质是HeapObject内部有两个属性metadata占用8字节,refCounts占用8字节,SwiftInt实际是个结构体,占用8自己,这里两个Int就是16字节。

综上所述:

  1. 存储属性是占用对象的分配的内存空间

2. 计算属性

计算属性,指的就是需要通过计算得到一些想要的值,那么对于属性来说计算就是getset,下面我们就通过一个举例来说明一下计算属性。

示例代码:

class Teacher {
    var age: Int {
        get {
            return 18
        }
        set {
            age = newValue
        }
    }
}
let t = Teacher()
t.age = 10

print("end")

对于计算属性就是属性自己实现getset方法,但是示例代码中这样写是不对的,首先会有警告:

16086261150433.jpg

⚠️警告的意思就是会自己调用自己,如果运行这段代码就会造成无限递归调用而崩溃:

16086261800055.jpg

2.1 计算属性的使用

在上面的介绍中,是我们通常对属性的getset写法,既然这样不对,那么计算属性该怎么用呢?下面我们在来看一个正方形的示例。

实例代码:

class Square{
    var width: Double = 10.0
    var area: Double{
        get{
            //这里的return可以省略,编译器会自动推导
            return width * width
        }
        set{
            width = sqrt(newValue)
        }
    }
}

let s = Square()

s.width = 8.0
print(s.area)

s.area = 81.0
print(s.width)


print("end")

打印结果:

16086265899242.jpg

这里我们可以看到可以通过对计算属性的getset方法内的一些操作,实现对其他属性的修改。

2.3 通过lldb查看计算属性的存储

16086299169390.jpg

这里的0x4024000000000000就是Swift中对Double类型的存储,这里我们对width的赋值为10。

2.2 通过SIL文件查看计算属性

按照上面所说的命令,我们在编译一个sil文件,sil代码如下:

2.2.1 类

class Square {
  @_hasStorage @_hasInitialValue var width: Double { get set }
  var area: Double { get set }
  @objc deinit
  init()
}

我们看到计算属性area没有了@_hasStorage的修饰。

2.2.2 get&set

get方法及分析:

// Square.area.getter
sil hidden @main.Square.area.getter : Swift.Double : $@convention(method) (@guaranteed Square) -> Double {
// %0 "self"                                      // users: %5, %4, %3, %2, %1
bb0(%0 : $Square):
  debug_value %0 : $Square, let, name "self", argno 1 // id: %1
  // width get 方法的函数地址
  %2 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %3
  // 调用函数,参数是%0-self,返回值width
  %3 = apply %2(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %6
  // width get 方法的函数地址
  %4 = class_method %0 : $Square, #Square.width!getter : (Square) -> () -> Double, $@convention(method) (@guaranteed Square) -> Double // user: %5
  // 调用函数,参数是%0-self,返回值width
  %5 = apply %4(%0) : $@convention(method) (@guaranteed Square) -> Double // user: %7
  // 转成 结构体
  %6 = struct_extract %3 : $Double, #Double._value // user: %8
  // 转成 结构体
  %7 = struct_extract %5 : $Double, #Double._value // user: %8
  // 计算,并将返回值存储到%8
  %8 = builtin "fmul_FPIEEE64"(%6 : $Builtin.FPIEEE64, %7 : $Builtin.FPIEEE64) : $Builtin.FPIEEE64 // user: %9
  // 将%8 的值包装到%9
  %9 = struct $Double (%8 : $Builtin.FPIEEE64)    // user: %10
  // 返回
  return %9 : $Double                             // id: %10
} // end sil function 'main.Square.area.getter : Swift.Double'

set方法及分析:

// Square.area.setter
sil hidden @main.Square.area.setter : Swift.Double : $@convention(method) (Double, @guaranteed Square) -> () {
// %0 "newValue"                                  // users: %5, %2
// %1 "self"                                      // users: %7, %6, %3
bb0(%0 : $Double, %1 : $Square):
  debug_value %0 : $Double, let, name "newValue", argno 1 // id: %2
  debug_value %1 : $Square, let, name "self", argno 2 // id: %3
  // function_ref sqrt
  // 获取sqrt函数的地址
  %4 = function_ref @sqrt : $@convention(c) (Double) -> Double // user: %5
  // 调用sqrt函数
  %5 = apply %4(%0) : $@convention(c) (Double) -> Double // user: %7
  // 获取width set 方法的函数地址
  %6 = class_method %1 : $Square, #Square.width!setter : (Square) -> (Double) -> (), $@convention(method) (Double, @guaranteed Square) -> () // user: %7
  // 调用width set 方法,传入参数%5---sqrt方法的返回值,%1---self
  %7 = apply %6(%5, %1) : $@convention(method) (Double, @guaranteed Square) -> ()
  %8 = tuple ()                                   // user: %9
  return %8 : $()                                 // id: %9
} // end sil function 'main.Square.area.setter : Swift.Double'

2.3 小结

打印内存占用大小:

16086288542018.jpg

这里的24 就是8+8+8得来的,所以可以得到一个结论就是计算属性不占用对象分配的内存空间。

综上所述:

  1. 计算属性的本质就是getset方法
  2. 计算属性不占用对象的内存空间

3. 延迟存储属性

延迟存储属性就是我们常说的懒加载,懒加载就是使用的时候在加载,那么我们就来看看Swift中这个延迟是什么样的。举个例子:

class Teacher {
    lazy var age:Int = 10
}

let t = Teacher()

print(t.age)

print("end")

3.1 通过lldb查看延迟存储属性在内存中的存储

延迟存储属性实际上还是存储属性,既然是存储属性肯定是占用对象分配的内存的,下面我们通过lldb来看看延迟存储属性在内存中是怎样存储的。

lldb调试:

16086939120494.jpg

在这个截图中第一段x/8gx打印的是第一个断点时的数据,第二个是打印的第二断点时的数据。

  • 通过以上的数据我们可以看到,延迟存储属性在不使用的时候是没有存储值的,虽然在代码的初始化中我们给它赋了个初始值10
  • 当我们使用了这个属性后,在内存中打印的时候就可以在看到这个值出现在了内存段中,这里的10是就是16进制的0xa

3.2 通过sil代码进一步探索延迟存储属性

修改Swift为如下,这里我们即使用set也使用get

class Teacher {
    lazy var age:Int = 10
}

let t = Teacher()

t.age = 18
print(t.age)

print("end")

使用如下命令将我们的main.swift文件编译成sil文件,这里还删除了原有的sil文件,并通过xcrun swift-demangle命令还原sil代码中的混淆。

rm -rf main.sil && swiftc -emit-sil main.swift | xcrun swift-demangle >> ./main.sil && open main.sil

3.2.1 类

class Teacher {
  lazy var age: Int { get set }
  @_hasStorage @_hasInitialValue final var $__lazy_storage_$_age: Int? { get set }
  @objc deinit
  init()
}

我们可以看到在sil代码中Teacher这个类中的延迟存储属性:

  1. 有一个和swift代码中书写差不多的属性,带有getset方法
  2. 在存储属性的基础上对var变量增加了final修饰
  3. 生成了$__lazy_storage_$_age这个可选(optional)变量

3.2.2 main 函数

16087064255177.jpg

针对main函数中的主要调用在截图中加了注释:

  1. 首先创建变量t的内存地址方到%3
  2. 获取tmetadata
  3. 获取__allocating_init的函数地址,用于对象的初始化
  4. 调用__allocating_init函数传入metadata初始化变量t
  5. 将初始化的结果存如%3的地址中
  6. %3这个内存地址中的t对象的数据加载到%8
  7. 将要赋值的18这个值放到%9这个虚拟寄存器中
  8. 通过%9中的值初始化一个Int类型的的结构体放到%10
  9. age属性的set方法放到%11
  10. 调用ageset方法,传入%10--18%8--self
  11. %8一致,将%3这个内存地址中的t对象的数据加载到%19
  12. ageget方法放到%20
  13. 调用ageget方法,传入%19--self参数,并将返回值存储到%21

3.2.3 set方法

16087103442712.jpg
// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value"                                     // users: %4, %2
// %1 "self"                                      // users: %5, %3
bb0(%0 : $Int, %1 : $Teacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
  // 将传入的value值%0存储到%4中
  %4 = enum $Optional<Int>, #Optional.some!enumelt, %0 : $Int // user: %7
  // 获取懒加载的$__lazy_storage_$_age的地址存放到%5中
  %5 = ref_element_addr %1 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %6
  // 开始访问age的内存
  %6 = begin_access [modify] [dynamic] %5 : $*Optional<Int> // users: %7, %8
  // 将%4的值存储到%6中
  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.Teacher.age.setter : Swift.Int'

sil中的set方法我们可以知道,在延迟存储属性中:

  1. 获取延迟属性的内存地址
  2. 将传入的值存储到内存地址中

3.2.4 get方法

16087116765812.jpg
// Teacher.age.getter
sil hidden [lazy_getter] [noinline] @main.Teacher.age.getter : Swift.Int : $@convention(method) (@guaranteed Teacher) -> Int {
// %0 "self"                                      // users: %14, %2, %1
bb0(%0 : $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  // 获取$__lazy_storage_$_age的内存地址存放到%2中
  %2 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %3
  // 开始访问内存
  %3 = begin_access [read] [dynamic] %2 : $*Optional<Int> // users: %4, %5
  // 加载内存中数据存放到%4中
  %4 = load %3 : $*Optional<Int>                  // user: %6
  // 结束内存访问
  end_access %3 : $*Optional<Int>                 // id: %5
  // 如果%4中有值(some)则跳转bb1,如果没有值(none)则跳转到bb2
  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
  // 跳转bb3
  br bb3(%7 : $Int)                               // id: %9

bb2:                                              // Preds: bb0
  // 将给age的初始值10取出存放到%10中
  %10 = integer_literal $Builtin.Int64, 10        // user: %11
  // 用一个Int类型的结构体包装这个10存放到%11
  %11 = struct $Int (%10 : $Builtin.Int64)        // users: %18, %13, %12
  debug_value %11 : $Int, let, name "tmp2"        // id: %12
  // 判断self是否为空,不空则将%11的值存放到%13中,否则$Int
  %13 = enum $Optional<Int>, #Optional.some!enumelt, %11 : $Int // user: %16
  // 获取$__lazy_storage_$_age的内存地址存放到%14中
  %14 = ref_element_addr %0 : $Teacher, #Teacher.$__lazy_storage_$_age // user: %15
  // 开始访问内存
  %15 = begin_access [modify] [dynamic] %14 : $*Optional<Int> // users: %16, %17
  // 将%13中的值存储到%15
  store %13 to %15 : $*Optional<Int>              // id: %16
  // 结束内存访问
  end_access %15 : $*Optional<Int>                // id: %17
  // 跳转bb3
  br bb3(%11 : $Int)                              // id: %18

// %19                                            // user: %20
bb3(%19 : $Int):                                  // Preds: bb2 bb1
  // 返回传入的值%19,如果%19为空则返回$Int  
  return %19 : $Int                               // id: %20
} // end sil function 'main.Teacher.age.getter : Swift.Int'

sil中的get方法我们可以知道,在延迟存储属性中:

  1. 获取延迟属性的内存地址
  2. 然后取出该地址中的值
  3. 如果获取的值为空则将默认值取出并返回
  4. 如果不为空则直接返回取的值

通过该sil代码我们可以看出,延迟存储属性的get是线程不安全的,为什么不安全呢?

  1. 如果线程1调用get,执行完bb0,判断age没有值,将会执行bb2
  2. 此时正好cpu时间片分配给了线程2,线程2也访问age,此时age依然没有值,同样会走bb2age赋初始值
  3. 待线程2执行完毕后线程1又获得执行权限,同样开始执行bb2,又会给age赋初值值
  4. 以上并不会影响age的值,如果线程2调用的是set此时修改了age的值,然后线程1在执行bb2的时候,给age赋初始值,就会因线程问题导致我们age的值不准确

3.3 内存的占用

打印非延迟属性和延迟属性类的内存的占用大小

16087170949276.jpg

可以看到在使用lazy修饰的延迟存储属性在对象的内存占用上会比不使用lazy修饰的多,那么这是为什么呢?

  • 我们在上面分析sil代码的时候知道lazy修饰的属性在底层是可选类型,在这里实际就是Optional<Int>
  • 不使用lazy修饰的时候,对象的内存占用是24,是由metadat+refCounts+Int得来的
  • 使用lazy修饰的时候,对象的内存占用是32是由metadat+refCounts+Optional<Int>得来的

这里我们就来看看Optional<Int>的内存占用:

16087176888057.jpg

可以看到Optional<Int>的size是9,其实这里是Int的8,加上Optional的1,Optional在底层是个枚举,默认是Int8占用1字节,在后续会详细说明。由于需要内存对齐,都是8字节8字节的读取,所以剩下不用的也要补齐。

3.4 lazy的其他用法

let data = 0...3
let result = data.lazy.map { (i: Int) -> Int in
    print("Handling...")
    return i * 2
}

print("Begin:")
for i in result {
    print(i)
}

在 Swift 标准库中,也有一些 Lazy 方法,就可以在不需要运行时,避免消耗太多的性能。

打印结果如下:

16097387023079.jpg

3.5 小结

经过上面的分析现总结如下:

  1. 延迟存储属性需要使用lazy修饰
  2. 延迟存储属性必须有一个默认的初始值
  3. 延迟存储属性在第一次访问的时候才能被赋值
  4. 延迟存储属性并不保证线程安全
  5. 延迟存储属性对实例对象的大小有影响

4. 类型属性

类型属性跟类方法的命名类似,是属于类的一个属性,不能通过实例对象去访问,类型属性需要使用static进行修饰,举个例子:

class Teacher {
    static var age:Int = 18
}

let t = Teacher()

Teacher.age = 20
print(Teacher.age)

print("end")

4.1 通过lldb对类型属性初步探索

类型属性也是属性,那么类型属性会存储在哪里呢?下面我们通过lldb调试,分别从对象、类、类型属性的内存来对类型属性进行初步探索。

16087775583517.jpg

我们首先打印了对象t,查看对象的内存,并没有找到对象中对age的存储。

随后我们查看对象的metadata,也并没有相关属性。

我们又打印了Teacher.age,得到的直接是age的值

那么类型属性到底存储在哪里呢?下面我们通过sil代码来看看。

4.2 通过sil进一步探索类型属性

生成sil代码的方式见3.2

这里为了简化sil代码,将Swift代码修改成如下:

class Teacher {
    static var age:Int = 18
}

Teacher.age = 20

4.2.1 类

class Teacher {
  @_hasStorage @_hasInitialValue static var age: Int { get set }
  @objc deinit
  init()
}

在类中主要的区别就是:相对于普通的存储属性增加了static修饰。

在这里我们并没有得出什么有用的结论,下面我们继续往下看:

16087880666372.jpg

此时我们看到如上图所示的sil_global,这里我们的Teacher.age已经是一个全局变量了。下面我们就去main函数中看看age的初始化。

4.2.2 main函数

16087884747572.jpg
// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  %2 = metatype $@thick Teacher.Type
  // function_ref Teacher.age.unsafeMutableAddressor
  %3 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %4
  // 调用%3中存储的函数,按照命名应该是获取Teacher.age的内存地址,并将返回结果存储到%4
  %4 = apply %3() : $@convention(thin) () -> Builtin.RawPointer // user: %5
  // 将%4的地址存放到%5
  %5 = pointer_to_address %4 : $Builtin.RawPointer to [strict] $*Int // user: %8
  // 构建20这个值
  %6 = integer_literal $Builtin.Int64, 20         // user: %7
  // 构建Int结构体
  %7 = struct $Int (%6 : $Builtin.Int64)          // user: %9
  // 开始方法内存,%5中的,也就是age的地址
  %8 = begin_access [modify] [dynamic] %5 : $*Int // users: %9, %10
  // 将%7存储到%8中,也就是将20存储到age的地址
  store %7 to %8 : $*Int                          // id: %9
  // 结束内存访问
  end_access %8 : $*Int                           // id: %10
  %11 = integer_literal $Builtin.Int32, 0         // user: %12
  %12 = struct $Int32 (%11 : $Builtin.Int32)      // user: %13
  return %12 : $Int32                             // id: %13
} // end sil function 'main'

经过对main函数的分析,我们可以知道:

  • 类型属性的内存地址是通过xxx.unsafeMutableAddressor函数获取到的
  • 然后将需要存储的值存储到这个内存中
  • 这也就很好的解释了我们在lldb调试的时候为什么打印出来的是存储的值。

4.2.3 xxx.unsafeMutableAddressor

16087901415141.jpg
// Teacher.age.unsafeMutableAddressor
sil hidden [global_init] @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer {
bb0:
  // 全局地址
  %0 = global_addr @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0 : $*Builtin.Word // user: %1
  // 全局地址存放到%1
  %1 = address_to_pointer %0 : $*Builtin.Word to $Builtin.RawPointer // user: %3
  // function_ref globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0
  // 函数地址存储到%2
  %2 = function_ref @globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_func0 : $@convention(c) () -> () // user: %3
  // once 保证全局地址只被初始化一次
  %3 = builtin "once"(%1 : $Builtin.RawPointer, %2 : $@convention(c) () -> ()) : $()
  // 初始化的全局地址存放到%4
  %4 = global_addr @static main.Teacher.age : Swift.Int : $*Int // user: %5
  // 地址转为指针存放到%5
  %5 = address_to_pointer %4 : $*Int to $Builtin.RawPointer // user: %6
  // 返回
  return %5 : $Builtin.RawPointer                 // id: %6
} // end sil function 'main.Teacher.age.unsafeMutableAddressor : Swift.Int'

我们可以在这个函数中看到:

  • 通过@globalinit_029_12232F587A4C5CD8B1EEDF696793B2FC_token0创建一个全局地址,这个就是类下面那段代码
  • 这里面还用到了builtin "once"来保证值初始化一次

4.2.4 builtin "once"

那么这个builtin "once"是怎么保证类型属性只初始化一次的呢?

我们通过添加断点和汇编调试来看看。

添加断点:

16087907583429.jpg

开启汇编调试:

16087907853587.jpg

汇编代码:

16087908489836.jpg

在汇编代码中我们看到断点在c处,我们按住command点击鼠标,跳转到下一层中:

16087909729953.jpg

我们看到这里调用的是swift_once

我们在swift_once这行添加断点,过掉上一个断点,command点击鼠标,跳转到下一层中:

16087910734447.jpg

此时我们看到了dispatch_once_f,这里基本就能证明了builtin "once"实际上还是使用了GCD中的dispatch_once来保证类型属性只被初始化一次的。

其实我们来到Swift源码中,搜索一下swift_once

16087913530439.jpg
/// Runs the given function with the given context argument exactly once.
/// The predicate argument must point to a global or static variable of static
/// extent of type swift_once_t.
void swift::swift_once(swift_once_t *predicate, void (*fn)(void *),
                       void *context) {
#if defined(__APPLE__)
  dispatch_once_f(predicate, context, fn);
#elif defined(__CYGWIN__)
  _swift_once_f(predicate, context, fn);
#else
  std::call_once(*predicate, [fn, context]() { fn(context); });
#endif
}

通过对源码的探索,我们也可以清楚的看到,swift_onceapple平台调用的是dispatch_once

4.2.5 get&set

下面我们在来看看类型属性的getset方法

get方法及分析:

// static Teacher.age.getter
sil hidden [transparent] @static main.Teacher.age.getter : Swift.Int : $@convention(method) (@thick Teacher.Type) -> Int {
// %0 "self"                                      // user: %1
bb0(%0 : $@thick Teacher.Type):
  debug_value %0 : $@thick Teacher.Type, let, name "self", argno 1 // id: %1
  // function_ref Teacher.age.unsafeMutableAddressor
  // 将获取地址的函数存储到%2
  %2 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %3
  // 调用函数并将返回结果存储到%3
  %3 = apply %2() : $@convention(thin) () -> Builtin.RawPointer // user: %4
  // 指针转地址存放到%4
  %4 = pointer_to_address %3 : $Builtin.RawPointer to [strict] $*Int // user: %5
  // 开始内存访问
  %5 = begin_access [read] [dynamic] %4 : $*Int   // users: %6, %7
  // 加载内存地址中的数据存储到%6
  %6 = load %5 : $*Int                            // user: %8
  // 结束内存访问
  end_access %5 : $*Int                           // id: %7
  // 返回取到的数据
  return %6 : $Int                                // id: %8
} // end sil function 'static main.Teacher.age.getter : Swift.Int'

set方法及分析:

// static Teacher.age.setter
sil hidden [transparent] @static main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @thick Teacher.Type) -> () {
// %0 "value"                                     // users: %8, %2
// %1 "self"                                      // user: %3
bb0(%0 : $Int, %1 : $@thick Teacher.Type):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $@thick Teacher.Type, let, name "self", argno 2 // id: %3
  // function_ref Teacher.age.unsafeMutableAddressor
  // 获取age的地址的函数存放到%4
  %4 = function_ref @main.Teacher.age.unsafeMutableAddressor : Swift.Int : $@convention(thin) () -> Builtin.RawPointer // user: %5
  // 调用函数并将返回值存放到%5
  %5 = apply %4() : $@convention(thin) () -> Builtin.RawPointer // user: %6
  // 指针转地址存储到%6
  %6 = pointer_to_address %5 : $Builtin.RawPointer to [strict] $*Int // user: %7
  // 开始方法指针
  %7 = begin_access [modify] [dynamic] %6 : $*Int // users: %8, %9
  // 将传入的参数%0 存储到%7
  store %0 to %7 : $*Int                          // id: %8
  // 结束内存访问
  end_access %7 : $*Int                           // id: %9
  %10 = tuple ()                                  // user: %11
  return %10 : $()                                // id: %11
} // end sil function 'static main.Teacher.age.setter : Swift.Int'

4.3 类型属性的内存占用

通过上面的分析,我们可以知道类型属性是一个全局变量,那么类型属性是不是不会占用类的存储空间呢?下面我们打印来看看:

16087925470522.jpg

通过打印我们可以知道,类型属性并不会占用类的空间。

4.4 单例的创建

回想一下,在Objective-C我们会使用如下的方式创建一个单例:

@implementation SITeacher
+ (instancetype)shareInstance{
    static SITeacher *shareInstance = nil;
    dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        shareInstance = [[SITeacher alloc] init];
    });
    return shareInstance;
}
@end

既然类型存储属性是个全局变量,那么我们在Swift中创建一个单例就简单多了,代码如下:

class Teacher {
    // 声明一个不可变的 let 类型 static 属性
    // 为当前类的实例对象
    static let shared = Teacher()
    // 将当前类的 init 方法 添加private访问权限
    private init(){}
}

其实经常写Swift代码的人都知道Swift中单例是怎么写的,但是其原理是什么并不是很多人都知道的,其实就是类型属性是个全局变量,而且只初始化一次,符合单例的条件。

4.5 小结

通过上面的分析,我们总结如下:

  1. 类型属性通过static来修饰
  2. 类型属性需要附一个默认的初始值
  3. 类型属性只会被初始化一次
  4. 类型属性是线程安全的
  5. 类型属性存储为全局变量
  6. 类型属性不占用对象的存储空间
  7. 类型属性可以用作单例来使用,前提是需要private类的init方法。

5. 属性观察者

介绍了完了Swift中的几种属性后,下面我们来看看在属性中使用频率最高的属性观察者。属性观察者在代码中实际就是willSetdidSet

class Teacher {
    var age:Int = 18 {
        //赋新值之前调用
        willSet {
            print("willSet newValue --- \(newValue)")
        }
        // 赋新值之后调用
        didSet {
            print("didSet oldValue --- \(oldValue)")
        }
    }
}

let t = Teacher()
t.age = 20

print("end")

打印结果:

16087981771930.jpg

通过打印结果我们可以看到,在赋新值前我们可以拿到即将要赋予的新值,在赋新值后也能够拿到赋值前的旧值。

5.2 通过sil探索属性观察者

sil编译命令详见3.2

5.2.1 类

class Teacher {
  @_hasStorage @_hasInitialValue var age: Int { get set }
  @objc deinit
  init()
}

在类中相比于普通的存储属性没有什么区别。

5.2.2 set 方法

其实属性观察主要是在设置新值的时候触发的,这里我们直接来看set方法。

set方法及分析:

// Teacher.age.setter
sil hidden @main.Teacher.age.setter : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () {
// %0 "value"                                     // users: %13, %10, %2
// %1 "self"                                      // users: %16, %11, %10, %4, %3
bb0(%0 : $Int, %1 : $Teacher):
  debug_value %0 : $Int, let, name "value", argno 1 // id: %2
  debug_value %1 : $Teacher, let, name "self", argno 2 // id: %3
  // 找出实例内部物理实例变量的地址
  %4 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %5
  // 开始访问内存
  %5 = begin_access [read] [dynamic] %4 : $*Int   // users: %6, %7
  // 将内存中的值加载到%6
  %6 = load %5 : $*Int                            // users: %8, %16
  // 结束内存访问
  end_access %5 : $*Int                           // id: %7
  debug_value %6 : $Int, let, name "tmp"          // id: %8
  // function_ref Teacher.age.willset
  // 获取 willset 的函数地址
  %9 = function_ref @main.Teacher.age.willset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %10
  // 调用 willset 函数,传入%0---新值,%1---self
  %10 = apply %9(%0, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
  // 找出实例内部物理实例变量的地址
  %11 = ref_element_addr %1 : $Teacher, #Teacher.age // user: %12
  // 开始访问内存
  %12 = begin_access [modify] [dynamic] %11 : $*Int // users: %13, %14
  // 存储新值
  store %0 to %12 : $*Int                         // id: %13
  // 结束内存访问
  end_access %12 : $*Int                          // id: %14
  // function_ref Teacher.age.didset
  // 获取 didset 的函数地址
  %15 = function_ref @main.Teacher.age.didset : Swift.Int : $@convention(method) (Int, @guaranteed Teacher) -> () // user: %16
  // 调用 didset 传入参数 %6---旧值,%1---self
  %16 = apply %15(%6, %1) : $@convention(method) (Int, @guaranteed Teacher) -> ()
  %17 = tuple ()                                  // user: %18
  return %17 : $()                                // id: %18
} // end sil function 'main.Teacher.age.setter : Swift.Int'

set方法中我们可以看到,使用了属性观察者的属性,在set方法中:

  1. 会将属性中的旧值取出,存起来
  2. 在设置新值前会调用willSet函数,并将新值作为参数传入
  3. 在设置新值后会调用didSet函数,并将旧值作为参数传入

5.2.3 willSet&didSet

willSet.jpg

willSet 函数中我们可以看到,其内部声明了newValue变量,所以我们可以直接使用

didSet.jpg

didSet 函数中我们同样也看到了oldValue变量的声明,同样也为我们直接使用提供了变量。

5.3 属性观察者的使用

5.3.1 init方法中会触发属性观察者吗

class Teacher {
    var age: Int = 18 {
        //赋新值之前调用
        willSet {
            print("willSet newValue --- \(newValue)")
        }
        // 赋新值之后调用
        didSet {
            print("didSet oldValue --- \(oldValue)")
        }
    }
    
    init() {
        age = 20
    }
}

print("end")

运行结果:

16088029261282.jpg

我们可以看到在init方法中给属性赋值,并没有触发属性观察者,那是为什么呢?

init 方法会初始化当前变量,对于存储属性,在分配内存后,会调用memset清理内存,因为可能存在脏数据,清理完才会赋值。

还有一种原因就是,我们如果有多个属性,我们给A属性赋值,并在A属性的观察者中访问了B属性,此时B属性可能还没有被初始化,此时我们就有可能获取到脏数据,导致结果不准确。

(PS:)仔细想想也不是很严谨,所以通过sil代码来看看,调用流程如下:

@main.Teacher.__allocating_init()——>@main.Teacher.init()

// Teacher.init()
sil hidden @main.Teacher.init() -> main.Teacher : $@convention(method) (@owned Teacher) -> @owned Teacher {
// %0 "self"                                      // users: %14, %10, %4, %1
bb0(%0 : $Teacher):
  debug_value %0 : $Teacher, let, name "self", argno 1 // id: %1
  %2 = integer_literal $Builtin.Int64, 18         // user: %3
  %3 = struct $Int (%2 : $Builtin.Int64)          // user: %6
  %4 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %5
  %5 = begin_access [modify] [dynamic] %4 : $*Int // users: %6, %7
  store %3 to %5 : $*Int                          // id: %6
  end_access %5 : $*Int                           // id: %7
  %8 = integer_literal $Builtin.Int64, 20         // user: %9
  %9 = struct $Int (%8 : $Builtin.Int64)          // user: %12
  %10 = ref_element_addr %0 : $Teacher, #Teacher.age // user: %11
  %11 = begin_access [modify] [dynamic] %10 : $*Int // users: %12, %13
  store %9 to %11 : $*Int                         // id: %12
  end_access %11 : $*Int                          // id: %13
  return %0 : $Teacher                            // id: %14
} // end sil function 'main.Teacher.init() -> main.Teacher'

init方法中,我们可以看到,属性的赋值都是操作的属性的内存地址,并没有调用willSetdidSet相关方法。但是赋值也是在属性初始化之后,所以对于memset应该是站不住脚的,对于属性观察者中调用其他存储属性按道理也是站不住脚的。

下面我们换一下测试代码:

class Teacher {
    
    var age2: Int {
        get{
            return age
        }
        set{
            self.age = newValue
        }
    }
    
    var age: Int = 18 {
        //赋新值之前调用
        willSet {
            print("willSet newValue --- \(newValue)")
        }
        // 赋新值之后调用
        didSet {
            print("didSet oldValue --- \(oldValue)")
        }
    }
    
    init() {
        age2 = 20
    }
}

打印结果:

16088621679034.jpg

此时我们发现在计算属性中给存储属性赋值就会调用属性观察者,下面我们通过sil代码看看调用。

init 函数:

16088625229134.jpg

我们可以看到在init函数中对于计算属性是直接调用了其setter方法。并且是在存储属性age初始化完毕后才调用的计算属性的setter

16088626625701.jpg

我们可以看到在计算属性的的setter方法中给存储属性赋值是调用的存储属性的setter方法,在上面的分析中我们已经知道了,在存储属性的setter方法中会调用willSetdidSet

综上所述,在init方法中给存储属性赋值是否触发属性观察者应该是这样的:

  • 赋值前后自己是能知道的,没必要在调用属性观察者
  • 在计算属性中给存储属性赋值,是因为计算属性的代码一般是固定的,并且这个值有可能计算后的,所以需要触发属性观察者,使其做后续处理

5.3.2 在那些地方可以添加属性观察者

  • 属性观察者应用在存储属性上
  • 计算属性自己实现了getset,此时要想做些什么事情,可以随便去做,不需要属性观察者
  • 但是继承的计算属性也是可以添加属性观察者的

应用在存储属性上在上面一直在使用,这里就不验证了

对于计算属性,验证结果如下:

16088641842589.jpg

我们可以看到willSetdidSet不能和getter一起提供,那么我们注释掉getter呢?

16088643020537.jpg

此时我们看到有setter的变量也必须有getter

所以属性观察者不能添加到计算属性中

下面我们来验证在继承的计算属性中的属性观察者:

16088646388045.jpg

我们看到对于继承计算属性的同样可以添加属性观察者,并在赋值的时候调用。

5.3.3 属性观察者的调用顺序

如果是继承存储属性,并且父类和子类都添加了属性观察者,那么这个调用顺序是什么呢?,验证如下:

16088648987009.jpg

我们看到调用顺序是:

  1. 先调用子类的willSet,再调用父类的willSet
  2. 先调用父类的didSet,在调用子类的didSet

其实也很好理解,首先通知子类要改变,毕竟是给子类赋值,然后在通知父类改变,父类改变完了先在父类做些处理,然后在让子类改变,子类在做最后的处理。

5.3.4 在子类的init方法中给属性赋值

如果在子类的init方法中给属性赋值会怎样调用属性观察者呢?测试结果如下:

16088656016892.jpg

我们可以看到在子类的init方法中给属性赋值与直接给子类对象赋值的调用属性是一模模一样样的。我的理解是这样的:

  • 在子类的init方法中赋值,父类是监听不到,所以需要触发父类的属性观察者,使其作出相应处理
  • 当父类做完处理子类也是监听不到的,所以同意需要触发子类属性观察者通知子类作出相应处理

6. Then

Swift中对于属性也会经常用到一个叫Then的框架:

devxoul/Then

其用法如下:

6.1 Then

import Then

let label = UILabel().then {
  $0.textAlignment = .center
  $0.textColor = .black
  $0.text = "Hello, World!"
}

以上等效于

let label: UILabel = {
  let label = UILabel()
  label.textAlignment = .center
  label.textColor = .black
  label.text = "Hello, World!"
  return label
}()

6.2 with

let newFrame = oldFrame.with {
  $0.size.width = 200
  $0.size.height = 100
}
newFrame.width // 200
newFrame.height // 100

6.3 do

UserDefaults.standard.do {
  $0.set("devxoul", forKey: "username")
  $0.set("devxoul@gmail.com", forKey: "email")
  $0.synchronize()
}

其原理其实也蛮简单的,感兴趣的可以去看看。

7. 总结

至此我们对Swift中的属性基本就分析完毕了先在总结如下:

存储属性:

  1. 存储属性是占用实例存储空间的
  2. sil代码中使用``@_hasStorage关键字修饰
  3. 再其getset方法中通过访问内存获取和修改属性值

计算属性:

  1. 计算属性的本质就是getset方法
  2. 计算属性不占用实例对象的内存空间

延迟存储属性:

  1. 延迟存储属性也是存储属性
  2. 延迟存储属性需要使用lazy修饰
  3. 延迟存储属性必须有一个默认的初始值
  4. 延迟存储属性在第一次访问的时候才能被赋值
  5. 延迟存储属性并不保证线程安全
  6. 延迟存储属性对实例对象的大小有影响,一般会增加内存占用

类型属性:

  1. 类型属性通过static来修饰
  2. 类型属性需要赋一个默认的初始值
  3. 类型属性只会被初始化一次
  4. 类型属性是线程安全的
  5. 类型属性存储为全局变量
  6. 类型属性不占用对象的存储空间
  7. 类型属性可以用作单例来使用,前提是需要private类的init方法。

属性观察者:

  1. 属性观察者是观察属性改变前后的,本质就是willSetdidSet
  2. 属性观察者可以添加在
    1. 存储属性
    2. 继承的存储属性
    3. 继承的计算属性
  3. 如果并没有在属性中添加属性观察者,则不会调用属性观察者
  4. 在底层代码中会根据开发者对属性观察者的依赖调用属性观察者
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容

  • Swift属性 存储属性(要么是常量(let 修饰)存储属性,要么是变量(var 修饰)存储属性) 计算属性(顾名...
    Mjs阅读 322评论 0 0
  • 原文博客地址: 浅谈Swift的属性(Property) 今年期待已久的Swift5.0稳定版就已经发布了, 感兴...
    TitanCoder阅读 1,674评论 0 6
  • 属性分类在Swift中, 严格意义上来讲属性可以分为两大类: 实例属性和类型属性 实例属性(Instance Pr...
    1980_4b74阅读 431评论 0 0
  • 存储属性 计算属性 属性观察者 静态属性 使用下标 存储属性 存储属性概念 存储属性可以存储数据,分为常量属性(用...
    优雅的步伐阅读 250评论 0 1
  • 存储属性 - Stored Properties 相当于 OC 的下划线成员变量 适用于:结构体 、 类 类型:常...
    Sunday_David阅读 255评论 0 0