Swift 中强引用循环的混乱示例

11 浏览
0 Comments

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

任何提示或指导都将大大赞赏。

admin 更改状态以发布 2023年5月21日
0
0 Comments

我认为细节决定一切。该文档指出:

...捕获可能发生是因为闭包主体访问实例的某个属性,例如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与其他"具体"属性之间。后者附带了隐含的约定,而前者只需要存在或不存在。

0
0 Comments

因为,在后一种情况下

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)"
    }
    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)"
}
let cloureBackup = heading!.asHTML
print(heading!.asHTML())
heading = HTMLElement(name: "h2")
print(cloureBackup())//

some default text

所以,测试代码的图片是这样的

enter image description here

你将看到Playground的日志

some default text

some default text

没有找到关于这个的任何文档,只是从我的测试和理解中得出,希望它对你有所帮助

0