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

2

8

Here, I was playing with leaks, so I've made a strong reference cycle intentionally to see if the Instruments will detect something, and I got unexpected results. The leak shown in Instruments certainly make sense, but the random crash is a bit mysterious (due to two facts I will mention later).

What I have here is a class called 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")}
}

Also I have two scenes, the 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(){}
} 

And finally, the MenuScene which is identical to the GameScene, just with an empty didMoveToView method (it has only touchesBegan method implemented).

Reproducing the Crash

The crash can be reproduce by transitioning between scenes for a few times. By doing that, the leak will happen because someInstance is retained by the closure variable, and the closure variable is retained by the someInstance variable, so we have a cycle. But still, this will not produce the crash (it will just leak). When I actually try to add self.method() inside of a closure, the app crashes and I get this:

error info

and this:

error info

The exact same crash I can produce if I try to access an unowned reference when the object it references is deallocated, eg. when closure outlives the captured instance. That make sense, but that is not the case here (closure is never executed).

The Mysterious Part

The mysterious part is that this crash happens only on iOS 9.1 and not on iOS9.3. And another mysterious thing is, the fact that the app crashes randomly, but mostly within first ten transitions . Also, the weird part is why it crashes if the closure is never executed, or the instance it captures is not accessed (at least not by me).

Solution to the Problem but not the Answer to the Question

Of course the crash can be solved in a few ways by breaking the cycle, and I am aware that I should use unowned only when I am completely sure that captured instance will never become nil after initialization. But still, due to fact that I haven't executed this closure at all, after it outlived the scene, this crash is pretty awkward to me. Also, it might be worth of mentioning that scenes are deinited successfully after each transition.

Interesting

If I use weak self inside of a capture list, the app will not crash (the leak still exists of course). Which make sense, because if the scene becomes nil before block is deallocated, accessing the scene through optional chaining will prevent crash. But the interesting part is that even if I use forced unwrapping like this, it will not crash:

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

Which makes me think...Appreciate any hints about how to debug this or explanation about what causing the crash ...

EDIT:

Here is the Github repo

Sergu answered 26/3, 2016 at 2:46 Comment(3)
If it does not crash on 9.3 then it's probably a bug. Is this your report by the way? SR-1006 It seems like the same problem.Shirting
@Shirting Nope, that is not my bug report ... Well, I started a bounty to see if somebody can really prove that this is a bug or not, and explain what should be the expected behaviour in this situation. Personally, I think it should leak, but shouldn't crash, but we'll see...Sergu
I am pretty sure it should not crash if you don't actually execute the block.Shirting
L
2

the crash is happening because the GameScene object has been released before the animation finishes.

one way to implement this would be to pass the SomeClass object back in the closure.

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

which would be used like this:

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

UPDATE

turns out this is a combination of a nuance in the convenience init?(fileNamed:) + misuse of [unowned self] causing your crash.

although the official documentation doesn't seem to state it, as briefly explained in this blog post the convenience initializer will actually reuse the same object.

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?

adding logging around the creation, setting of the closure and deinit leads to some interesting output:

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

notice how the same memory address is used twice. i'd assume under the hood apple is doing some interesting memory management for optimization which is potentially leading to the stale closure still existing after deinit.

deeper digging could be done into the internals of SpriteKit, but for now i'd just replace [unowned self] with [weak self].

Laos answered 29/3, 2016 at 2:9 Comment(4)
So you think that crash is reasonable, and that behaviour seen on iOS9.1 works correctly, but the results from iOS9.3 are buggy (because the same code doesn't produce the crash) ?Sergu
Also, can you please be more specific and detailed about GameScene instance being released before the animation finishes statement ? Do you mean released before the actual SKTransition finishes or ? I forgot to mention that this is happening only when transitioning from MenuScene -> GameScene, not vice versa.Sergu
Okay, I've tested what you said and you are right about the same addresses usage. The newly allocated scene get the same address as the old scene and that is the point when I get crash (that is why the crash is random, because only sometimes the new scene get the address of the old scene). So this is some clue and is helpful for further investigation. Just one question...Have you actually managed to produce a crash on 9.3 using the repo I've uploaded? Just wondering what is the expected behaviour (what is seen on 9.1 or 9.3).Sergu
And about the convenience fileNamed ... I wouldn't agree that it has something with this crash, because the crash happens even if you don't use fileNamed to load the archive (you can check this by deleting .sks files, and using standard SKScene(size:) initializer). Anyways, thanks for your effort, it was really helpful.Sergu
B
1

From what I can see if seems like the retain cycle would be caused because you are including an object in its own closure and saving it to itself. See if the following works:

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(){}
} 

I moved someInstance to a property of the class because I am pretty sure with a weak reference in the block and without passing someInstance somewhere outside the function, someInstance will deinit at the end of that function. If thats what you want then keep it someInstance inside the function. You can also use unowned if you want but as you know I guess I am just a bigger fan of using weak lol. Let me know if that fixes the leak and crash

Boni answered 16/6, 2016 at 14:36 Comment(2)
Okay thanks for the response and effort... Will look at as soon as I find time.Sergu
No problem. Let me know because from what I saw in the answer above with the same memory addresses being used, I think the strong retains memory leak could be a cause of the issueBoni

© 2022 - 2024 — McMap. All rights reserved.