延迟初始化和保留周期

Posted

技术标签:

【中文标题】延迟初始化和保留周期【英文标题】:Lazy initialisation and retain cycle 【发布时间】:2016-11-03 14:57:08 【问题描述】:

在使用惰性初始化器时,是否有可能出现保留周期?

在blog post 和许多其他地方看到[unowned self]

class Person 

    var name: String

    lazy var personalizedGreeting: String = 
        [unowned self] in
        return "Hello, \(self.name)!"
        ()

    init(name: String) 
        self.name = name
    

我试过了

class Person 

    var name: String

    lazy var personalizedGreeting: String = 
        //[unowned self] in
        return "Hello, \(self.name)!"
        ()

    init(name: String) 
        print("person init")
        self.name = name
    

    deinit 
        print("person deinit")
    

这样用过

//...
let person = Person(name: "name")
print(person.personalizedGreeting)
//..

发现“person deinit”被记录了。

所以似乎没有保留周期。 据我所知,当一个块捕获自我以及当这个块被自我强烈保留时,存在一个保留周期。这种情况看起来类似于保留循环,但实际上并非如此。

【问题讨论】:

你试过了吗?添加一个deinit 方法并检查它是否在您期望对象被释放时被调用。或者使用 Xcode/Instruments 中的内存调试工具。 当您使用 blocksclosures 时,您可能会意外地创建强保留周期——这与 lazy 初始化器无关。 hello @MartinR deinit 即使没有捕获列表也被调用。 @holex 似乎块内存管理在惰性属性方面有所不同。正如答案中所指出的,惰性属性的闭包是隐式的。这改变了此类闭包的内存管理规则。 【参考方案1】:

我试过这个[...]

lazy var personalizedGreeting: String =  return self.name ()

似乎没有保留周期

正确。

原因是立即应用的闭包() 被认为是@noescape。它不保留捕获的self

供参考:Joe Groff's tweet。

【讨论】:

另一种思考方式是编译器可以安全地决定不在惰性 var 的闭包中为 self 应用 ARC,因为闭包只能由仍然保留类实例的代码调用(在此示例中为 Person 实例)。所以不需要在实例(又名自我)上保留另一个级别。我也喜欢这个答案中的@noescape 参考。【参考方案2】:

在这种情况下,您不需要捕获列表,因为在 personalizedGreeting 实例化后没有引用 self

正如 MartinR 在他的评论中所写,您可以通过在删除捕获列表时记录 Person 对象是否被取消初始化来轻松测试您的假设。

例如

class Person 
    var name: String

    lazy var personalizedGreeting: String = 
        _ in
        return "Hello, \(self.name)!"
        ()

    init(name: String) 
        self.name = name
    

    deinit  print("deinitialized!") 


func foo() 
    let p = Person(name: "Foo")
    print(p.personalizedGreeting) // Hello Foo!


foo() // deinitialized!

显然在这种情况下不存在强引用循环的风险,因此在惰性闭包中不需要unowned self 的捕获列表。这样做的原因是惰性闭包只执行一次,并且只使用闭包的返回值来(惰性)实例化personalizedGreeting,而对self的引用没有,在在这种情况下,比闭包的执行时间更长。

但是,如果我们要在 Person 的类属性中存储类似的闭包,我们将创建一个强引用循环,因为 self 的属性将保持对 self 的强引用。例如:

class Person 
    var name: String

    var personalizedGreeting: (() -> String)?

    init(name: String) 
        self.name = name

        personalizedGreeting = 
            () -> String in return "Hello, \(self.name)!"
        
    

    deinit  print("deinitialized!") 


func foo() 
    let p = Person(name: "Foo")


foo() // ... nothing : strong reference cycle

假设:惰性实例化闭包默认自动将self 捕获为weak(或unowned

当我们考虑下面的例子时,我们意识到这个假设是错误的。

/* Test 1: execute lazy instantiation closure */
class Bar 
    var foo: Foo? = nil


class Foo 
    let bar = Bar()
    lazy var dummy: String = 
        _ in
        print("executed")
        self.bar.foo = self 
            /* if self is captured as strong, the deinit
               will never be reached, given that this
               closure is executed */
        return "dummy"
    ()

    deinit  print("deinitialized!") 


func foo() 
    let f = Foo()
    // Test 1: execute closure
    print(f.dummy) // executed, dummy


foo() // ... nothing: strong reference cycle

即,foo() 中的 f 没有被取消初始化,鉴于这个强引用循环,我们可以得出结论,self 在惰性变量 dummy 的实例化闭包中被强烈捕获。

我们还可以看到,如果我们从不实例化 dummy,我们将永远不会创建强引用循环,这将支持最多一次惰性实例化闭包可以被视为运行时范围(很像 never达到如果) 是 a) 从未达到(未初始化)或 b) 达到、完全执行并“丢弃”(范围结束)。

/* Test 2: don't execute lazy instantiation closure */
class Bar 
    var foo: Foo? = nil


class Foo 
    let bar = Bar()
    lazy var dummy: String = 
        _ in
        print("executed")
        self.bar.foo = self
        return "dummy"
    ()

    deinit  print("deinitialized!") 


func foo() 
    let p = Foo()
    // Test 2: don't execute closure
    // print(p.dummy)


foo() // deinitialized!

有关强参考周期的更多信息,请参阅例如

"Weak, strong, snowned, oh my!" - A guide to references in Swift

【讨论】:

“很明显,在这种情况下没有强引用循环的风险”:嗯,至少对我来说这并不明显。如果惰性属性从未被访问过,则初始化闭包将永远存在。为什么它不阻止实例释放?延迟初始化闭包有什么魔力,总是将 self 的引用解释为 weak 我指的是通过实验明显(也许明显是一个不好的词选择)。无论如何,personalizedGreeting 本身只是一个简单的值类型(String),它本身不能持有对self 的引用。用于(可能)实例化p...Greeting 的 at-most-on-the-fly-executed-once 闭包本身不是对象,因此它不能保存对self 的引用。如果我们要求实例化p...Greeting,它只会执行一次。如果我们从不访问p...Greeting,则此非实例化值类型将在超出范围时与类对象一起被释放。 我同意你的说法,但它没有回答问题。在您的示例中,闭包引用了self。如果这是一个强引用,只要闭包没有被释放(这在属性初始化之前不会发生),它就会使实例保持活动状态。所以我能想到的唯一解释是:惰性属性初始化中的闭包总是自动捕获self弱(或者,更有可能是unowned)。这完全说得通,并解释了观察到的“缺失”参考循环的行为。 @NikolaiRuhe 我会在午饭后回到办公室再回顾一下。我的理论是 1. 如您所描述的那样(weak 默认捕获),或者 2. 最多一次的惰性实例化闭包可以被视为运行时范围(就像从未达到的 if ) 是 a) 从未达到(未初始化)或 b) 达到、完全执行并“丢弃”(范围结束),后者的结果只是闭包的返回类型,在这种情况下只是一个值类型。 我找不到支持自动弱理论的文档。我对 swift 源代码的简短浏览也没有显示任何提示。无论如何,进一步的测试似乎支持惰性初始化器是正常闭包的理论,但 self 不被视为强参考。

以上是关于延迟初始化和保留周期的主要内容,如果未能解决你的问题,请参考以下文章

延迟初始化中的 双重检查模式 和 延迟占位类模式 你都用对了吗?

延迟初始化

单例模式的实现——延迟初始化占位类替代双重检验加锁以达到延迟初始化和线程安全的目的。

JAVA 双重检查锁定和延迟初始化

延迟初始化、ORM 和 lambda

延迟初始化:未能延迟初始化集合