使用捕获列表中的无主内容导致崩溃,甚至块本身也不会被执行

Whi*_*ind 7 memory-leaks ios automatic-ref-counting sprite-kit swift

在这里,我正在玩泄漏,所以我有意识地做了一个强大的参考周期,看看仪器是否会检测到某些东西,我得到了意想不到的结果.仪器中显示的泄漏确实有意义,但随机崩溃有点神秘(由于我将在后面提到的两个事实).

我这里有一个叫做的课程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")}
}
Run Code Online (Sandbox Code Playgroud)

我还有两个场景,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(){}
} 
Run Code Online (Sandbox Code Playgroud)

最后,MenuScene这是相同的GameScene,只是有一个空的didMoveToView方法(它已只touchesBegan方法实现).

再现崩溃

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

错误信息

还有这个:

错误信息

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

神秘的部分

神秘的部分是这次崩溃发生在iOS 9.1不是iOS9.3上.另一个神秘的事情是,应用程序随机崩溃,但大多数在前十个转换中.此外,奇怪的部分是如果从不执行闭包,或者它没有访问它捕获的实例(至少不是我),它崩溃的原因.

问题的解决方案而不是问题的答案

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

有趣

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

let closure = {[weak self] in
      someInstance.method() 
      self!.method()  
}
Run Code Online (Sandbox Code Playgroud)

这让我觉得......欣赏有关如何调试此内容的任何提示或有关导致崩溃的原因的解释......

编辑:

这是Github 回购

Cas*_*sey 2

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

\n\n

实现此目的的一种方法是将SomeClass对象传递回闭包中。

\n\n
class SomeClass {\n    var closure: (SomeClass->Void)?\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

可以这样使用:

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

更新

\n\n

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

\n\n

尽管官方文档似乎没有说明这一点,但正如本博客文章中简要解释的那样,便利初始化程序实际上会重用相同的对象。

\n\n
\n

文件参考

\n\n

场景编辑器允许您引用不同 .sks(场景)文件之间的内容,这意味着您可以将一堆精灵放在一个场景文件中,然后从另一个场景文件引用该文件。

\n\n

您可能想知道为什么需要多个场景,原因有以下几个:

\n\n

1) 您可以在多个不同的场景中重复使用相同的精灵集合,这意味着您不必一遍又一遍地重新创建它们。

\n\n

2) 如果您需要更改所有场景中引用的内容,您所要做的就是编辑原始场景,引用它的每个场景中的内容都会自动更新。聪明吧?

\n
\n\n

在创建、闭包设置和 deinit 周围添加日志记录会产生一些有趣的输出:

\n\n
GameScene init: 0x00007fe51ed023d0\nGameScene setting closure: 0x00007fe51ed023d0\nMenuScene deinited\nGameScene deinited: 0x00007fe51ed023d0\nGameScene init: 0x00007fe51ed023d0\nGameScene setting closure: 0x00007fe51ed023d0\n
Run Code Online (Sandbox Code Playgroud)\n\n

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

\n\n

可以对 SpriteKit 的内部进行更深入的挖掘,但现在我只需替换[unowned self][weak self].

\n