Swift 中强引用循环的混乱示例
Swift 中强引用循环的混乱示例
这是来自苹果文档的一个例子:
class HTMLElement { let name: String let text: String? lazy var asHTML: Void -> String = { if let text = self.text { return "<\(self.name)>\(text)</\(self.name)>" } else { return "<\(self.name) />" } } init(name: String, text: String? = nil) { self.name = name self.text = text } deinit { print("\(name) is being deinitialized") } }
我理解为什么这个闭包属性会引起强引用循环,并且我知道如何解决它。我不会与此争论。
真正困扰我的是下面的代码:
var heading: HTMLElement? = HTMLElement(name: "h1") let defaultText = "some default text" heading!.asHTML = { // confusing, this closure are supposed to retain heading here, but it does not return "<\(heading!.name)>\(heading!.text ?? defaultText)</\(heading!.name)>" } print(heading!.asHTML()) heading = nil // we can see the deinialization message here, // it turns out that there is not any strong reference cycle in this snippet.
我从Swift文档和我自己对Objective-c的经验中知道,变量heading
将被闭包捕捉,因此应该引起强引用循环。但它并没有,这真的让我困惑。
我还写了一个与此示例相对应的Objective-c,它确实引起了我预期的强引用循环。
typedef NSString* (^TagMaker)(void); @interface HTMLElement : NSObject @property (nonatomic, strong) NSString *name; @property (nonatomic, strong) NSString *text; @property (nonatomic, strong) TagMaker asHTML; @end @implementation HTMLElement - (void)dealloc { NSLog(@"%@", [NSString stringWithFormat:@"%@ is being deinitialized", self.name]); } @end
;
HTMLElement *heading = [[HTMLElement alloc] init]; heading.name = @"h1"; heading.text = @"some default text"; heading.asHTML = ^ { return [NSString stringWithFormat:@"<%@>%@</%@>", heading.name, heading.text, heading.name]; }; NSLog(@"%@", heading.asHTML()); heading = nil; // heading has not been deinitialized here
任何提示或指导都将大大赞赏。
我认为细节决定一切。该文档指出:
...捕获可能发生是因为闭包主体访问实例的某个属性,例如
self.someProperty
,或者因为闭包在实例上调用方法,例如self.someMethod()
。
请注意此处的self
,我认为这是问题的关键。
另一篇文档提到:
闭包可以捕获其定义的周围上下文中的常量和变量。然后闭包可以从其主体中引用和修改这些常量和变量的值,即使定义常量和变量的原始作用域不存在。
换句话说,被捕获的是常量和变量,而不是对象本身。只是self
是一个特殊情况,因为当一个闭包从一个对象中初始化时使用了self
,那么合同规定当闭包被触发时这样的self
总是存在的。换句话说,在任何情况下,一个带有self
的闭包执行,但这个self
指向的对象已经消失是不可能的。考虑以下情况:这样的闭包可以被带到其他地方,例如分配给另一个对象的另一个属性,因此即使原始所有者已经“忘记”被捕获的对象,它也必须能够运行。要求开发人员检查self
是否为nil
是荒谬的,对吧?因此需要保持强引用。
现在,如果你转向另一种情况,闭包中没有使用self
,但是使用了一些(明确强制解包)可选项,那么这是完全不同的事情。这种可选项可能是nil
,开发人员必须接受这个事实,并加以处理。当这样的闭包运行时,它可以是使用的可选属性其实从未被分配具体值的情况!那么保持强引用有什么用呢?
为了说明。这是基本类:
class Foo { let name: String lazy var test: Void -> Void = { print("Running closure from \(self.name)") } init(name: String) { self.name = name } }
这是强引用循环的镜像:
var closure: Void -> Void var captureSelf: Foo? = Foo(name: "captureSelf") closure = captureSelf!.test closure() // Prints "Running closure from captureSelf" captureSelf = nil closure() // Still prints "Running closure from captureSelf"
现在,下一个具有可选属性的情况:
var tryToCaptureOptional: Foo? = Foo(name: "captureSomeOptional") tryToCaptureOptional?.test = { print("Running closure from \(tryToCaptureOptional?.name)") } closure = tryToCaptureOptional!.test closure() // Prints "Running closure from Optional("captureSomeOptional")" tryToCaptureOptional = nil closure() // Prints "Running closure from nil"
...即我们仍然“记得”闭包,但是闭包应该能够处理它使用的属性实际上是nil
的情况。
但是现在"乐趣"才刚刚开始。例如,我们可以做:
var tryToCaptureAnotherOptional: Foo? = Foo(name: "tryToCaptureAnotherOptional") var holdItInNonOptional: Foo = tryToCaptureAnotherOptional! tryToCaptureAnotherOptional?.test = { print("Running closure from \(tryToCaptureAnotherOptional?.name)") } closure = tryToCaptureAnotherOptional!.test closure() // Prints "Running closure from Optional("tryToCaptureAnotherOptional")" tryToCaptureAnotherOptional = nil closure() // Prints "Running closure from nil" print(holdItInNonOptional.name) // Prints "tryToCaptureAnotherOptional" (!!!) holdItInNonOptional.test() // Also prints "Running closure from nil"
换句话说,尽管对象并没有真正"消失",只是某个特定的属性不再指向它,但相关的闭包仍然会适应并反应这个属性不再持有对象的事实(虽然原始对象仍然存在,但它只是移动到了另一个地址)。
归纳起来,我认为区别在于"占位符"属性self
与其他"具体"属性之间。后者附带了隐含的约定,而前者只需要存在或不存在。
因为,在后一种情况下
Swift闭包持有heading
的强引用,而不是指向heading
的实例
图片中看起来像这样
如果我们通过设置heading = nil
来打破红色线,则引用环被打破。
更新开始:
但是,如果你不将heading
设置为nil
,仍然会有一个类似我上面发布的图片中的引用环。你可以这样测试一下
func testCircle(){ var heading: HTMLElement? = HTMLElement(name: "h1") let defaultText = "some default text" heading.asHTML = { return "<\(heading.name)>\(heading.text ?? defaultText)\(heading.name)>" } print(heading.asHTML()) } testCircle()//No dealloc message is printed
更新结束
我还写了下面的测试代码来证明闭包在内存中不持有实例的强引用
var heading: HTMLElement? = HTMLElement(name: "h1") var heading2 = heading let defaultText = "some default text" heading!.asHTML = { // confusing, this closure are supposed to retain heading here, but it does not return "<\(heading!.name)>\(heading!.text ?? defaultText)\(heading!.name)>" } let cloureBackup = heading!.asHTML print(heading!.asHTML()) heading = HTMLElement(name: "h2") print(cloureBackup())//some default text
所以,测试代码的图片是这样的
你将看到Playground的日志
some default text
some default text
没有找到关于这个的任何文档,只是从我的测试和理解中得出,希望它对你有所帮助