CodingTour
Swift: weak 的 strong 时机

前言

早前在Swift 内存管理中整理过一张思维导图,当时主要是通过官方资料和前人的经验得出的结论,不过内心里有2个疑问一直没有得到答案:

  • Swift 的 weak 变量会在使用前尝试 +1 strong,但是具体的时机是什么时候呢?

  • Objective-C 中的 Weak-Strong Dance 在 Swift 中是否还需要?

今天就通过 SIL 探寻其中的答案。

探索

先写一个简单的 Swift 测试代码:

/// test.swift

class aClass{
    var value = 1
    func log() {
        print(self.value)
    }
}

var c2 = aClass()
var fSpec = { [weak c2] in
    c2?.log()
    c2?.log()
}
fSpec()

我们主要通过闭包内的两条 log 语句寻找其中的答案。

用以下命令将 Swift 源码转成 SIL:

swiftc -emit-sil test.swift >> ./sil.swift

生成的 SIL 很长,但是没有关系,因为 SIL 很好理解,它和我们印象中的程序执行流程完全一样,比如我们的程序往往会有一个 main 方法,SIL 也不例外,你可以通过 @main 快速找到它:

// main
sil @main : $@convention(c) (Int32, UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>) -> Int32 {
bb0(%0 : $Int32, %1 : $UnsafeMutablePointer<Optional<UnsafeMutablePointer<Int8>>>):
  alloc_global @$s4test2c2AA6aClassCvp            // id: %2
  %3 = global_addr @$s4test2c2AA6aClassCvp : $*aClass // users: %7, %12
  %4 = metatype $@thick aClass.Type               // user: %6
  // function_ref aClass.__allocating_init()
  %5 = function_ref @$s4test6aClassCACycfC : $@convention(method) (@thick aClass.Type) -> @owned aClass // user: %6
  %6 = apply %5(%4) : $@convention(method) (@thick aClass.Type) -> @owned aClass // user: %7
  store %6 to %3 : $*aClass                       // id: %7
  alloc_global @$s4test5fSpecyycvp                // id: %8
  %9 = global_addr @$s4test5fSpecyycvp : $*@callee_guaranteed () -> () // users: %22, %24
  %10 = alloc_box ${ var @sil_weak Optional<aClass> }, var, name "c2" // users: %23, %21, %20, %11
  %11 = project_box %10 : ${ var @sil_weak Optional<aClass> }, 0 // user: %17
  %12 = begin_access [read] [dynamic] %3 : $*aClass // users: %13, %15
  %13 = load %12 : $*aClass                       // users: %16, %14
  strong_retain %13 : $aClass                     // id: %14
  end_access %12 : $*aClass                       // id: %15
  %16 = enum $Optional<aClass>, #Optional.some!enumelt.1, %13 : $aClass // users: %18, %17
  store_weak %16 to [initialization] %11 : $*@sil_weak Optional<aClass> // id: %17
  release_value %16 : $Optional<aClass>           // id: %18
  // function_ref closure #1 in 
  %19 = function_ref @$s4testyycfU_ : $@convention(thin) (@guaranteed { var @sil_weak Optional<aClass> }) -> () // user: %21
  strong_retain %10 : ${ var @sil_weak Optional<aClass> } // id: %20
  %21 = partial_apply [callee_guaranteed] %19(%10) : $@convention(thin) (@guaranteed { var @sil_weak Optional<aClass> }) -> () // user: %22
  store %21 to %9 : $*@callee_guaranteed () -> () // id: %22
  strong_release %10 : ${ var @sil_weak Optional<aClass> } // id: %23
  %24 = begin_access [read] [dynamic] %9 : $*@callee_guaranteed () -> () // users: %25, %27
  %25 = load %24 : $*@callee_guaranteed () -> ()  // users: %29, %28, %26
  strong_retain %25 : $@callee_guaranteed () -> () // id: %26
  end_access %24 : $*@callee_guaranteed () -> ()  // id: %27
  %28 = apply %25() : $@callee_guaranteed () -> ()
  strong_release %25 : $@callee_guaranteed () -> () // id: %29
  %30 = integer_literal $Builtin.Int32, 0         // user: %31
  %31 = struct $Int32 (%30 : $Builtin.Int32)      // user: %32
  return %31 : $Int32                             // id: %32
} // end sil function 'main'

在这个 main 方法里有一些我们熟悉和不熟悉的东西:

  • enum - Optional 实际上就是枚举
  • function_ref - 表示对 SIL 函数的引用

通过上下文和注释可以知道闭包调用就是执行了 @$s4testyycfU_ 函数:

  // function_ref closure #1 in 
  %19 = function_ref @$s4testyycfU_ : $@convention(thin) (@guaranteed { var @sil_weak Optional<aClass> }) -> () // user: %21

再通过 @$s4testyycfU_ 找到具体的 SIL 实现:

// closure #1 in 
sil private @$s4testyycfU_ : $@convention(thin) (@guaranteed { var @sil_weak Optional<aClass> }) -> () {
// %0                                             // user: %1
bb0(%0 : ${ var @sil_weak Optional<aClass> }):
  %1 = project_box %0 : ${ var @sil_weak Optional<aClass> }, 0 // users: %33, %30, %3, %2
  debug_value_addr %1 : $*@sil_weak Optional<aClass>, var, name "c2", argno 1 // id: %2
  %3 = begin_access [read] [dynamic] %1 : $*@sil_weak Optional<aClass> // users: %20, %13, %5
  %4 = alloc_stack $Optional<aClass>              // users: %6, %24, %19, %16, %12, %11, %9
  %5 = load_weak %3 : $*@sil_weak Optional<aClass> // user: %6
  store %5 to %4 : $*Optional<aClass>             // id: %6
  %7 = integer_literal $Builtin.Int1, -1          // user: %9
  %8 = integer_literal $Builtin.Int1, 0           // user: %9
  %9 = select_enum_addr %4 : $*Optional<aClass>, case #Optional.some!enumelt.1: %7, default %8 : $Builtin.Int1 // user: %10
  cond_br %9, bb2, bb1                            // id: %10

我们看到 %9 的位置通过 select_enum_addr 读取了枚举值:

select_enum_addr

Selects one of the “case” or “default” operands based on the case of the referenced enum value. This is the address-only counterpart to select_enum.

这个指令是这里实际上读取了 some,然后执行 cond_br,它是带条件的 br 指令,意为当 %9 为1时执行bb2,为0时执行bb1

注:SIL 会将方法分解成连续的 building blocks

bb = building block

接下来看看bb2bb1这两个 blocks 的实现:

bb1:                                              // Preds: bb0
  destroy_addr %4 : $*Optional<aClass>            // id: %11
  dealloc_stack %4 : $*Optional<aClass>           // id: %12
  end_access %3 : $*@sil_weak Optional<aClass>    // id: %13
  %14 = enum $Optional<()>, #Optional.none!enumelt // user: %15
  br bb3(%14 : $Optional<()>)                     // id: %15

bb2:                                              // Preds: bb0
  %16 = unchecked_take_enum_data_addr %4 : $*Optional<aClass>, #Optional.some!enumelt.1 // user: %17
  %17 = load %16 : $*aClass                       // users: %23, %21, %22, %18
  strong_retain %17 : $aClass                     // id: %18
  destroy_addr %4 : $*Optional<aClass>            // id: %19
  end_access %3 : $*@sil_weak Optional<aClass>    // id: %20
  %21 = class_method %17 : $aClass, #aClass.log!1 : (aClass) -> () -> (), $@convention(method) (@guaranteed aClass) -> () // user: %22
  %22 = apply %21(%17) : $@convention(method) (@guaranteed aClass) -> ()
  strong_release %17 : $aClass                    // id: %23
  dealloc_stack %4 : $*Optional<aClass>           // id: %24
  %25 = tuple ()                                  // user: %26
  %26 = enum $Optional<()>, #Optional.some!enumelt.1, %25 : $() // user: %27
  br bb3(%26 : $Optional<()>)                     // id: %27

可以看到无论是哪个执行,最终都会 br bb3,整个流程是线性的,而 bb1bb2 的区别就是:

  • bb1 直接做了清理工作就跳到了 bb3
  • bb2 在通过 some 拿到值后,在访问前对引用计数+1,调用对象方法,然后清理前再对引用计数-1

后续 bb3 的执行过程和 bb0 很像,也是通过 Optionalsome 值条件跳转 bb5 or bb4:

%39 = select_enum_addr %34 : $*Optional<aClass>, case #Optional.some!enumelt.1: %37, default %38 : $Builtin.Int1 // user: %40
  cond_br %39, bb5, bb4

bb4、bb5 略过,逻辑和 bb1、bb2 一样。

结论

通过这次简单的分析,可以发现对 weak 增加引用计数是在访问前,访问后就减少了,只对单条指令有效,但是不像 Objective-C 方法参数中的 self__unsafe_unretained,Swift 是强引用过的,相对来说更安全。

Weak-Strong Dance 在 Swift 中同样有意义,考虑如下代码:

var fSpec = { [weak c2] in
    c2?.saveToMemory()
    c2?.saveToDB()
}

由于无法保证两条语句执行时 c2 都有相同的值,所以会有不一致的情况发生。而我们可能希望要么全都执行,全么全都不执行,所以用 Weak-Strong Dance 能符合我们的预期:

var fSpec = { [weak c2] in
    if let c2 = c2 {
        c2.saveToMemory()
        c2.saveToDB()
    }
}

此时产生的 SIL 为:

bb0(%0 : ${ var @sil_weak Optional<aClass> }):
  %1 = project_box %0 : ${ var @sil_weak Optional<aClass> }, 0 // users: %3, %2
  debug_value_addr %1 : $*@sil_weak Optional<aClass>, var, name "c2", argno 1 // id: %2
  %3 = begin_access [read] [dynamic] %1 : $*@sil_weak Optional<aClass> // users: %5, %4
  %4 = load_weak %3 : $*@sil_weak Optional<aClass> // user: %6
  end_access %3 : $*@sil_weak Optional<aClass>    // id: %5
  switch_enum %4 : $Optional<aClass>, case #Optional.some!enumelt.1: bb2, case #Optional.none!enumelt: bb1 // id: %6

bb1:                                              // Preds: bb0
  br bb3                                          // id: %7

// %8                                             // users: %14, %12, %13, %10, %11, %9
bb2(%8 : $aClass):                                // Preds: bb0
  debug_value %8 : $aClass, let, name "c2"        // id: %9
  %10 = class_method %8 : $aClass, #aClass.log!1 : (aClass) -> () -> (), $@convention(method) (@guaranteed aClass) -> () // user: %11
  %11 = apply %10(%8) : $@convention(method) (@guaranteed aClass) -> ()
  %12 = class_method %8 : $aClass, #aClass.log!1 : (aClass) -> () -> (), $@convention(method) (@guaranteed aClass) -> () // user: %13
  %13 = apply %12(%8) : $@convention(method) (@guaranteed aClass) -> ()
  strong_release %8 : $aClass                     // id: %14
  br bb3                                          // id: %15

不仅有了一致性,产生的指令也更少了。