懒加载和保留环问题

37

使用lazy initialisers,有可能会存在保留循环吗?

在一篇博客文章和许多其他地方,我们可以看到[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”。

因此似乎没有保留循环。据我所知,当一个块捕获self并且该块被self强烈保留时,会出现保留循环。这种情况看起来类似于保留循环,但实际上不是。


2
你试过了吗?添加一个 deinit 方法并检查当你期望对象被释放时是否被调用。或者使用 Xcode/Instruments 中的内存调试工具。 - Martin R
当你使用_blocks_或_closures_时,可能会意外地创建强引用循环 - 这与lazy初始化程序无关。 - holex
你好 @MartinR,即使没有捕获列表,deinit 也被调用了。 - BangOperator
@holex 看起来当涉及到懒加载属性时,块内存管理有所不同。正如答案中指出的那样,对于惰性属性的闭包是隐式的 noescaping。这会改变此类闭包的内存管理规则。 - BangOperator
3个回答

74

我尝试了这个[...]

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

看起来没有保留环

正确。

原因是立即应用的闭包{}()被视为@noescape,它不会保留捕获的self

参考:Joe Groff的推文


1
另一种思考方式是编译器可以安全地决定不为lazy var的闭包中的self应用ARC,因为该闭包只能由仍然保留类实例(在此示例中为Person实例)的代码调用。因此,不需要对实例(即self)进行另一级保留。我也喜欢这个答案中的@noescape引用。 - WeakPointer

6

在这种情况下,您不需要捕获列表,因为在实例化 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未被取消初始化,考虑到这种强引用循环,我们可以得出结论:实例闭包中的惰性变量dummy捕获了self
另外,如果我们永远不实例化dummy,就不会创建强引用循环,这表明最多一次的惰性实例化闭包可以看作是运行时作用域(类似于永远不会到达的if语句),即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!

关于强引用循环的更多阅读材料,例如:


1
显然,在这种情况下不存在强引用循环的风险。至少对我来说,这并不明显。如果懒加载属性从未被访问,初始化闭包将永远存在。为什么它没有阻止实例被释放?懒加载初始化闭包中是否有一些魔法,总是将对self的引用解释为weak - Nikolai Ruhe
2
我同意你的说法,但它并没有回答问题。在你的例子中,闭包需要一个对 self 的引用。如果这是一个强引用,那么只要闭包没有被释放(在属性初始化之前不可能发生),它就会保持实例的存活。所以我能想到的唯一解释是:懒加载属性初始化中的闭包自动弱引用捕获 self(或者更可能是 unowned)。这完全有道理,并解释了“缺失”的引用循环的行为。 - Nikolai Ruhe
可能测试1中的强循环是与self被捕获的方式无关的,但另一方面,测试2显示,如果我们从未实例化lazy变量,则闭包将永远不会生效(也没有需要被解除分配的必要)。然而,在我完成这篇帖子的编辑时,您提供了有关@noescape的解决方案,所以所有这些都变得不相关了。也许我会删除这个答案,稍后再查看一下,看看是否还有任何价值。 - dfrib
谢谢你的有趣讨论 :) - Nikolai Ruhe
@NikolaiRuhe 同样! - dfrib
显示剩余8条评论

0
在我看来,事情可能是这样的。该块肯定会捕捉到自身引用。但请记住,如果完成保留循环,则前提是self必须保留该块。但是正如您所见,延迟属性仅保留块的返回值。因此,如果未初始化延迟属性,则外部上下文会保留惰性块,使其一致,并且没有完成保留循环。但是,有一件事情我仍然不清楚,那就是惰性块何时被释放。如果已经初始化了惰性属性,则显而易见地执行惰性块并立即释放它,也不会产生保留循环。问题的主要在于谁保留了惰性块,我想。如果在块内捕获self时没有完成保留循环,则可能不是self。至于@noescape,则不是这样的。@noescape并不意味着不捕获,而是指临时存在,并且任何对象都不应该对该块具有持久引用,或者换句话说,保留该块。该块不能异步使用 查看此主题。如果@noescape是事实,那么惰性块怎么能持续到惰性属性被初始化?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接