延迟初始化和保留周期
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 中的内存调试工具。
当您使用 blocks 或 closures 时,您可能会意外地创建强保留周期——这与 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
不被视为强参考。以上是关于延迟初始化和保留周期的主要内容,如果未能解决你的问题,请参考以下文章
延迟初始化中的 双重检查模式 和 延迟占位类模式 你都用对了吗?