在捕获列表中使用 unowned 导致崩溃,即使块本身没有执行

Using unowned inside of a capture list causing a crash even the block itself isn't executed

在这里,我正在玩泄漏,所以我故意做了一个强大的参考循环,看看 Instruments 是否会检测到一些东西,我得到了意想不到的结果。 Instruments 中显示的泄漏当然是有道理的,但是 随机崩溃 有点神秘(由于我稍后会提到的两个事实)。

我这里有一个 class 叫做 SomeClass:

class SomeClass{

    //As you can guess, I will use this shady property to make a strong cycle :)
    var closure:(()->())?
    init(){}
    func method(){}
    deinit {print("SomeClass deinited")}
}

另外我有两个场景,GameScene:

class GameScene: SKScene {

    override func didMoveToView(view: SKView) {

        backgroundColor = .blackColor()

        let someInstance = SomeClass()

        let closure = {[unowned self] in

            someInstance.method() //This causes the strong reference cycle...
            self.method()  
        }
        someInstance.closure = closure
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

        if let nextScene = MenuScene(fileNamed: "MenuScene"){
            nextScene.scaleMode = .AspectFill
            let transition = SKTransition.fadeWithDuration(1)
            view?.presentScene(nextScene, transition: transition)
        }
    }

    deinit {print("GameScene deinited")}

    func method(){}
} 

最后,MenuSceneGameScene 相同,只是有一个空的 didMoveToView 方法(它只实现了 touchesBegan 方法)。

重现崩溃

可以通过在场景之间转换几次来重现崩溃。通过这样做,泄漏将会发生,因为 someInstanceclosure 变量保留,并且 closure 变量被 someInstance 变量保留,所以我们有一个循环。但是,这仍然不会产生崩溃(它只会泄漏)。当我实际尝试在闭包内添加 self.method() 时,应用程序崩溃了,我得到了这个:

还有这个:

如果我在 unowned 引用的对象被释放时尝试访问它,我可能会产生完全相同的崩溃,例如。当闭包超过捕获的实例时。这是有道理的,但这里不是这种情况(永远不会执行闭包)。

神秘部分

神秘的部分是这次崩溃 只发生在 iOS 9.1 不发生在 iOS9.3 .另一个神秘的事情是,应用程序崩溃 随机 ,但主要是在前十个转换中。此外,奇怪的是,如果从未执行闭包,或者它捕获的实例未被访问(至少不是我),它为什么会崩溃。

解决问题但不是问题的答案

当然,崩溃可以通过打破循环的几种方式来解决,我知道只有在我完全确定捕获的实例在初始化后永远不会变为 nil 时,我才应该使用 unowned。但是,由于事实上我根本没有执行过这个关闭,在它超过场景之后,这个崩溃对我来说还是很尴尬的。此外,可能值得一提的是,场景在每次转换后都会成功取消。

有趣

如果我在捕获列表中使用 weak self,应用程序将不会崩溃(当然泄漏仍然存在)。这是有道理的,因为如果场景在块被释放之前变为 nil,通过可选链接访问场景将防止崩溃。但有趣的是,即使我这样使用forced unwrapping,它也不会崩溃:

let closure = {[weak self] in
      someInstance.method() 
      self!.method()  
}

这让我觉得...感谢有关如何调试此问题的任何提示或有关导致崩溃的原因的解释...

编辑:

这里是Githubrepo

发生崩溃是因为 GameScene 对象在动画结束前已被释放。

实现这一点的一种方法是将 SomeClass 对象传回闭包。

class SomeClass {
    var closure: (SomeClass->Void)?
}

可以这样使用:

override func didMoveToView(view: SKView) {
    let someInstance = SomeClass()
    someInstance.closure = { [unowned self] someClass in
        someClass.method() // no more retain cycle
        self.method()
    }
}

更新

事实证明,这是便利性 init?(fileNamed:) + 误用 [unowned self] 导致崩溃的细微差别。

虽然官方文档似乎没有说明,但正如这篇 blog post 中简要解释的那样,便利初始化程序实际上会重用同一个对象。

File References

The scene editor allows you to reference content between different .sks (scene) files, meaning you can put together a bunch of sprites in a single scene file and then reference the file from another scene file.

You might wonder why you would need more than one scene, and there a couple of reasons:

1) You can reuse the same collection of sprites in multiple different scenes, meaning you don’t have to recreate them over and over again.

2) If you need to change the referenced content in all of your scenes, all you have to do is edit the original scene and the content automatically updates in every scene that references it. Smart, right?

围绕创建、设置闭包和 deinit 添加日志记录会导致一些有趣的输出:

GameScene init: 0x00007fe51ed023d0
GameScene setting closure: 0x00007fe51ed023d0
MenuScene deinited
GameScene deinited: 0x00007fe51ed023d0
GameScene init: 0x00007fe51ed023d0
GameScene setting closure: 0x00007fe51ed023d0

注意同一个内存地址是如何被使用两次的。我假设在幕后苹果正在做一些有趣的内存管理优化,这可能导致陈旧的闭包在 deinit 后仍然存在。

可以深入挖掘 SpriteKit 的内部结构,但现在我只是将 [unowned self] 替换为 [weak self]

据我所知,是否会导致保留循环,因为您将一个对象包含在它自己的闭包中并将其保存到自身。查看以下是否有效:

class GameScene: SKScene {

    let someInstance = SomeClass()

    override func didMoveToView(view: SKView) {

        backgroundColor = .blackColor()

        let closure = {[weak self, weak someInstance] in

            someInstance?.method() //This causes the strong reference cycle...
            self?.method()  
        }
        someInstance.closure = closure
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {

        if let nextScene = MenuScene(fileNamed: "MenuScene"){
            nextScene.scaleMode = .AspectFill
            let transition = SKTransition.fadeWithDuration(1)
            view?.presentScene(nextScene, transition: transition)
        }
    }

    deinit {print("GameScene deinited")}

    func method(){}
} 

我将 someInstance 移动到 class 的 属性 因为我很确定块中的弱引用并且没有在函数外部的某个地方传递 someInstance , someInstance 将在结束时取消初始化那个功能。如果那是你想要的,那么将它保存在函数中的 someInstance 中。如果你愿意,你也可以使用 unowned,但如你所知,我想我只是更喜欢使用 weak lol。让我知道这是否解决了泄漏和崩溃问题