UIViewControllers won't deallocate memory when they segue to a different UIViewController

Essentially I have a menu with a list of buttons that each present a different SCNView. Press one and it segues (modally) to a new ViewController that is an SCNView. Once there, you can press a "back" button that segues back (modally) to the starting menu where you can select a different button/SCNView and so on. I have an IBAction outlet for each "back" button where I try to dismiss that ViewController(SCNView) to free up memory when it segues back to the menu:


@IBAction func BackButton(_ sender: Any) { self.dismiss(animated: true, completion: nil) }


I run the app while watching the Leaks Instrument (shows no leaks...) and I can see that each time I select one of the ViewControllers/SCNViews, the memory jumps up, when I go back to the menu the memory stays up, then I select a different one and the memory jumps again. It does this until the app crashes, "Terminated due to memory issue".

Somehow I can't get the ViewController/SCNViews to dismiss properly and free up memory when I go back to the menu so the app doesn't eventually crash.


I've even called weak SCNNodes to be shown in the scene (it won't let me set everything as weak, camera, scnView, etc...)


It just seems that trying to dismiss the ViewController is not dismissing everything in it, causing the memory to build up!

Accepted Reply

I don't quite follow all the details of your structure, but I believe you said you're using navigation controllers, which have a stack of view controllers as you go deeper. So, if you have menu1 -> menu2 -> scene, you have all three VCs existing at the same time. Your menu structure may also be retaining each of the view controllers that you visit.


Here's the acid test: If you visit a scene you visited before (via the same path through the menus), that visit should not add any permanent memory usage after the first visit to that scene. To simplify that slightly, exercise your app by visiting every scene once. Then, you should be able to re-visit any scene with no permanent increase in memory. If that's true (and I suspect that might be what you saw), then you don't have a memory management problem.


Note that if all scenes are kept "alive" even when not displayed, and there's any significant memory cost associated with those scenes, you may have a memory usage problem (as distinct from a memory management problem), where the system starts to see your app as a memory hog. That's an entirely different question, if there could be architectural changes to your app that let it minimize its memory use. If you're hogging megabytes, them maybe you should try. If you're only hogging kilobytes, it probably isn't worth bothering about.


Anyway, congratulation of making progress!

Replies

This sounds like a memory management bug, where you have a reference cycle you're not aware of. The most common cause of an "invisible" reference cycle is a closure that that is strongly referenced by "self", but which captures "self" strongly — or something equivalent via a longer chain of references. It may involve the SCNView view controller, or the SCNView itself, or both of them.


If you can't find the cycle by inspecting your code, you can try using the Allocations instrument. Make sure the allocation history option is on, then create the scene view controller and dismiss it. You should then be able to find the view controller or view object's allocation history, and sift through its pairs of retains/releases until you find the one that points to the other object. (Unfortunately, this can be painful if the history is long, but you might be lucky.)

Thank you! After doing a lot more reading on this, I think you're right, there must be some kind of reference that's causing a retain cycle (even with me trying to define everything as weak).

I used the Allocations instrument as you suggested and you're right, the log is long and complicated! I'm not sure I'm quite skilled enough to locate the culprit that way but was wondering if you (or others) might be able to help me pinpoint it from the code I'm using.


Here's how I make my SCNNode (calling from a .dae model I imported):

weak var someobjectscene = SCNScene(named: "art.scnassets/SomeObject.dae")
weak var someobject = someobjectscene?.rootNode.childNode(withName: "SomeObject", recursively: true)


and my scene (calling an 'empty' scene I made in the .scn editor with lighting and a camera):

weak var scene = SCNScene(named: "art.scnassets/EmptyScene.scn")


after adjusting 'someobject' properties, here's how I add it to the scene:

scene?.rootNode.addChildNode(someobject!)


and finally, here's my SCNView:

weak var scnView = self.view as? SCNView
scnView?.scene = scene


In addition to the Navigation Controller I'm now using to move through the VCs, I'm also currently trying to end each file with:

override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(true)
        self.removeFromParentViewController()
        self.dismiss(animated: true, completion: nil)
      }


I don't know if that might help anyone out there spot a potential retain cycle (and hopefully a suggestion on how to fix it!). I can't seem to find it.

Thanks!

I don't know what to make of all of this. This code isn't the right approach (although it likely isn't the cause of any reference cycles, eitther).


When you write code like this (which I assume is inside your view controller class):


weak var someobjectscene = SCNScene(named: "art.scnassets/SomeObject.dae")


you create a SCNScene object, and assign its only existing reference to a weak variable. IOW, nothing is keeping this scene object alive, and it is subject to deallocation at any time. When it does get deallocated, this will cascade down to all of the objects its owns, including 'someobject', which has no other strong reference, so it will deallocate too.


This doesn't fail for you because deallocations (typically) don't happen immediately, for reasons that are basically an implementation detail. In your case, the objects stay alive for "scene" to take a strong reference to "someobject" (when you addChildNode), and for the view to take a strong reference to "scene" (when you set the view's scene). At this point, everything you need is strapped down, and everything you don't need is subject to deallocation.


However, the fact that this (currently) works doesn't mean that the code is correct. You need to think hierarchically for object relationships like this. A parent should strongly reference its children (and therefore keep them alive). A child should weakly reference its parent (and therefore avoid a glaring reference cycle error). In the above code, you are dealing with only "down" (i.e. parent to child) references, so they should typically be strong variables.


There's a secondary issue, too, in that "someobjectscene" and "someobject" probably shouldn't be instance variables at all, since they're both temporary. The references need to be strong, but they don't need to persist after you've set up your real scene.


In regard to "viewDidDisappear", there's not enough context to know exactly what you intend, but on the face of it, your override's code is doing things that need to be done before any sequence of events could lead to viewDidDisappear being called. For example, removing a view controller from its parent view controller will cause its view to be removed from the view hierarchy, which will trigger viewDidDisappear. In addition, you probably shouldn't be calling removeFromParentViewController if you're about to call dismiss, since it seems to me that removeFromParentViewController is part of what dismiss is supposed to do.


So, I think you need to keep honing your skills about how to write code like this (look at sample apps, which will help you gauge when you are writing too much code), and I don't think the cause of your reference cycle is in any of this code.

This helps a lot!! I understand completely about this not being the correct way to code things! This is my first project and I made it entirely by looking at sample apps and by Googling, "How do I ........ in xcode?" 😁


I guess I still don't know how I would go about showing some of my objects any other way (all of my searching got me to this approach!). For example, all of my VCs show 3-D models that I imported to the artassets folder (as .dae files). Some VCs only have to show one model (like the one I've been talking about) while another VC might have to show three. In that case, setting up 'object1scene', then pulling out the only node (3-D model) as 'object1', then repeating for the other two was the only way I could get models from different .dae files to all show up in a single view (after I called my new 'EmptyScene' and added them all to it). Other than that, I really can't figure out how to accomplish it, as well as I think I don't quite understand the difference between calling them as 'instance variables' versus just making references to them. All I can find in the Apple Developer literature is just stuff talking about calling things with 'let' and 'var', then just putting 'weak' in front of it....


I am now going to go back and start re-working everything to try to find a more streamlined way to do it, as you mentioned. I know this is still the wrong way to code (as it is still the 'current' file I have) but I thought I'd just post the whole thing, warts and all, so that maybe it can show a little more of at least what I'm trying to do. Also (fingers crossed!) maybe by seeing the whole thing in context, direct pointers of what kinds of things to do and where might be easier. So, here's ALLof what I've been doing:

import Foundation
import UIKit
import QuartzCore
import SceneKit

weak var someobjectscene = SCNScene(named: "art.scnassets/SomeObject.dae")
weak var someobject = someobjectscene?.rootNode.childNode(withName: "SomeObject", recursively: true)

weak var scene = SCNScene(named: "art.scnassets/EmptyScene.scn")

class OrthoScene: UIViewController {
     override func viewDidLoad() {
       super.viewDidLoad()

  
        someobject?.position = SCNVector3Make(0, 0, 0)
        someobject?.scale.x = 0.005
        someobject?.scale.y = 0.005
        someobject?.scale.z = 0.005
        scene?.rootNode.addChildNode(someobject!)

  
        weak var scnView = self.view as? SCNView

        scnView?.scene = scene

        scnView?.allowsCameraControl = true

        scnView?.autoenablesDefaultLighting = true

        scnView?.backgroundColor = UIColor.black

    }

    override var shouldAutorotate : Bool {
        return true
    }

    override var prefersStatusBarHidden : Bool {
        return true
    }

    override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        /
    }


    override func viewDidDisappear(_ animated: Bool) {

        super.viewDidDisappear(true)

        self.dismiss(animated: true, completion: nil)

    }

}


Just as a reminder, I'm pulling one SCNScene from the 3-D model (.dae) file I imported to the project and the other, "scene", is the 'empty' one I set up with the camera, lighting, etc. so that all my VCs have the same 'base' scene to add the nodes to. Again, I know this isn't the right way to do it, but it's a good example of what can be accomplished from the desire to create an app and access to Google....


Thanks again for the continued help/suggestions and any other insights are most certainly appreciated!!

So, try something like this (replacing lines 6-33 of what you just posted):


class OrthoScene: UIViewController { 
     override func viewDidLoad() {
       super.viewDidLoad()

        let someobjectscene = SCNScene(named: "art.scnassets/SomeObject.dae")! // <- note "!"
        let someobject = someobjectscene.rootNode.childNode(withName: "SomeObject", recursively: true)! // <- note "!"

        let scene = SCNScene(named: "art.scnassets/EmptyScene.scn")! // <- note "!"

        someobject.position = SCNVector3Make(0, 0, 0)
        someobject.scale.x = 0.005
        someobject.scale.y = 0.005
        someobject.scale.z = 0.005
        scene.rootNode.addChildNode(someobject)
  
        let scnView = self.view as! SCNView // <- note "!"

        scnView.scene = scene
        scnView.allowsCameraControl = true
        scnView.autoenablesDefaultLighting = true
        scnView.backgroundColor = UIColor.black
    }


Note that the formerly "weak" variables are now local to "viewDidLoad", since those references don't need to persist. (The persistent referent to the scene you construct is kept by the view's "scene" property, line 19.) Also, since you're providing the various SCN nodes in assets that are in your app bundle's resources, it's a fatal program error if they don't load. So, I put a "!" on those lines to crash your app if the object isn't available, which is the correct thing to do. (If you were, say, loading these scenes from somewhere else, like downloading them, that could easily fail, then you would test for the nil result instead, and do some kind of error handling that notifies the user.) Similar, if the "view" property isn't a SCNView, your app is in big trouble, so crashing is the best choice.


After all that, your optionals ("?") basically all disappear, which is great for reducing the complexity of your code. Escapee optionals are something to be avoided in Swift, because they just propagate bugs away from the original point of the problem.)


Regarding the original memory management problem, there's nothing in this code (your version or mine) that suggests that view controller objects are being leaked. I think your next step is to make an accurate list of which of your objects are being leaked. That is, it view controllers? scenes? node?

The best way to go about this is to use the "Mark Generation" button in the Allocations. After running your app and choosing the first optional view controller, wait a few seconds (for any deferred activities to have time to complete), then press Mark Generation. Then, activate a different view controller, wait a few seconds, then press Mark Generation. Keep doing this a few times. You'll see the object allocations list get an entry for each "generation" which lists the differences in what's allocated but not deallocated in the generation. (Of course, this isn't finalized for one generation until you start the next one. That's why you do this multiple times. Also, there are some things that are genuinely allocated and stay allocated, so there might be genuine increases in memory use the first time you activate each view controller.)


If you see things being left over in older generation, you can look at the list of object classes, and the ones you recognize will likely be the objects locked in a reference cycle. That doesn't tell where the problem is, but it points you closer.

Thank you so much!! I've gone back and updated all my VCs with the changes you've made (as well as going back and also doing some of the earlier ones you mentioned)! After doing a quick check, everything still loads fine.......now I'm just left dealing with the memory issue!


I ran the Allocations and marked the generations like you said. Each time the VCs change (going back and forth to the same two...) I can see consistent 'growth' each time I go into one of the scenes. Go back out and the memory drops a little, but NOT the same amount it went up in the first place. Now turn around and go in again and it grows the same amount it did the first time (leading to the mountain buildup...).


I can see how long the lists are and it will take me a while to go through each item as you said, but I'm willing to do it if it will help!!


In the meantime, I did notice something this time that I hadn't seen before. I don't know if it might help point to the issue but maybe it might. So, I'm using a Navigation Controller in the storyboard to connect up all the VCs so it puts that bar at the top of the view with the name, back 'button', ect. Well, it also lets you see the battery indicater, time, and carrier signal/wifi/etc. I've checked and I'm fairly certain that I've connected them all the same BUT I noticed that SOME views have the battery/time/signal information...but other ones lose that (but keep the title, back 'button', etc.)


On top of that, I noticed that just switching between VCs that both have the battery/time/signal info DOES NOT gradually build up memory! All of the VC's are set up as SCNViews (storyboard) and they're all connected the same way but something about the ones where I'm showing/calling nodes/scenes makes the battery/time/signal disappear....and it's ONLY when I switch back and forth to one of those views when the build up happens! By way of example, here's a chain of three VC's connected through the Navigation controller (all 'Push' segues):


Main Menu --> Secondary Menu --> Scene(with node/model).


Watching the memory allocation, I can go back and forth between the Main Menu <--> Secondary Menu ALL DAY and the memory goes up when I click into the Secondary Menu but goes back down the exact same amountwhen go back to the Main Menu (I sat there and did it over and over, no overall buildup). But, when when I go from Secondary Menu --> Scene, two things happen: the memory goes up again (more than between the two menus...probably because of the nodes/3D models) and the battery/time/signal information collapses/disappers! Now when I go back to the Secondary Menu, the memory drops (a bit) and the battery/time/signal info comes back. The memory drop here is specifically the one that's not equalled out and going back and forth now builds up until the memory crash.


As I said earlier, I'll begin going through those Generation lists but I noticed that discrepancy in the views (in the top bar) and also noticed that's the specific place the memory seems to be building up so I wanted to add it to the discussion!! Maybe there's some kind of connection I made incorrectly that accidentally took the Scenes 'out of the chain' or something like that but at the very least, I was encouraged when I saw that it was that transition that was where the buildup was occuring.


Thank you again for all the time and help on this, I really appreciate it!!

I don't think the status bar has anything directly to do with the problem. Individual view controllers can specify a preference for how the status bar should appear when their views are presented, and I'm guessing that the Scene Kit-based VC you're using says to hide to it because a scene is usually a full screen of graphical content that you don't want to be partially obscured.


What it does suggest, though, is that the problem is specific to the Scene VC part of your app. My guess is that it's something you're doing with the scene, in code we haven't looked at, that's setting up (e.g.) a retain cycle between two objects, and that's keeping the whole thing alive.


Note that when you're looking at the objects "left over" in a generation, there's a display option to hide all the objects not created by your code. That sometimes helps to narrow your focus.


One thing to be careful of, apart from direct strong references between objects, is use of closures that capture 'self'. The closure is a block of code that's executed "later" (usually appearing just after function call syntax, like this:


     let x = fff (a) {
     ... this is a closure ... even if the opening brace is on a separate line ...
     }


If an instance property of some object keeps a closure alive, and the closure refers to ("captures") that object, you have a reference cycle.

Thank you and your tip about the Scenes being the likely culprit for some of the issue paid off big time!! After you mentioned that, I went back and tried to figure out a way to reduce/streamline how many 'scenes' were being called. My old way was to call the scene I imported, 'pull out' the node I wanted, then add it to a blank scene (where I did the lighting, camera, etc.). Reading what you said, I figured I could cut down on the scenes by going through and adjusting the lighting/cameras in the original scenes so I wouldn't need the second 'empty' scene AND I wouldn't even need to mess around calling the nodes because they'd already be in there. It took awhile to make all the original imports match but eventually, I converted them all to .scn (instead of .dae) and was able to eliminate the second 'scene' in each VC!


Doing this made a MAJOR difference in the memory issue....(I say "difference" because it didn't quite fix it but it absolutely has taken a step in that direction!) This is what happens now: I can go through the chain from my earlier post (Main Menu --> Secondary Menu --> Scene(with node/model)) and now going back and forth from the Sec. Menu <--> Scene DOES NOTcontinually build up memory!! Each step in the chain does increase the memory but now NO TWO steps, going back and forth, go into a coninuous build up of memory!!


This seems to be the last remaining memory hurdle: While there's no longer a build up switching between the same VCs, there IS still an overall buildup if you navigate to different VCs. So, going from a menu to a scene jumps the memory. Going back to the menu will drop it (not all the way, but some). Then going to a different scene jumps it PAST where it was for the first scene (creating a new 'memory highpoint'). Going back to the menu drops it some but the good news here is that if you do ANY combination of switching between the first scene, menu, and second scene, you will NEVER raise the 'memory highpoint that was reached. BUT if you navigate to a third scene, the previous highpoint now jumps again (but you can still navigate to any of the three without a runaway memory cycle...)

So, it does seem like it's still retaining something of each scene....but at least it's not running away like it used to!!😁 Since there have been so many changes, I'll go ahead and repost the code I have now for both the Menus and the Scenes. As you pointed out earlier, the problem could be elsewhere in the code so what I'm putting here really is the sum total of code for the project, the entire app is EITHER a menu VC or a scene VC!

Menus: (set to SCNViews in the Storyboard...)

import Foundation
import UIKit
import QuartzCore
import SceneKit
class MainMenu: UIViewController {
       override func viewDidLoad() {
       super.viewDidLoad()
     
        let scene = SCNScene()
     
        let scnView = self.view as! SCNView
     
        scnView.scene = scene
     
        scnView.backgroundColor = UIColor.black
    
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        /
    }

}

Scenes: (also set to SCNViews in the Storyboard...)

i

mport Foundation
import UIKit
import QuartzCore
import SceneKit
class SomeObjectScene: UIViewController {
  
    override func viewDidLoad() {
      
        super.viewDidLoad()
      
        let someobjectscene = SCNScene(named: "art.scnassets/SomeObject.scn")!
      
        let scnView = self.view as! SCNView
      
        scnView.scene = someobjectscene
      
        scnView.allowsCameraControl = true
      
        scnView.autoenablesDefaultLighting = true
      
        scnView.backgroundColor = UIColor.black
      
    }
  
    override var shouldAutorotate : Bool {
        return true
    }
  
    override var prefersStatusBarHidden : Bool {
        return true
    }
  
    override var supportedInterfaceOrientations : UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone {
            return .allButUpsideDown
        } else {
            return .all
        }
    }
  
    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
   
    }
  
}


Maybe the fact that both the Menus and the Scenes are each SCNViews is somehow causing it to retain something? Or somehow because in each they have to call 'self' to set the scnView? Regardless, I can't express how grateful I am and how excited I was to finally see it coming a step closer to working!!

As always, any and all insights are much appreciated!

I don't quite follow all the details of your structure, but I believe you said you're using navigation controllers, which have a stack of view controllers as you go deeper. So, if you have menu1 -> menu2 -> scene, you have all three VCs existing at the same time. Your menu structure may also be retaining each of the view controllers that you visit.


Here's the acid test: If you visit a scene you visited before (via the same path through the menus), that visit should not add any permanent memory usage after the first visit to that scene. To simplify that slightly, exercise your app by visiting every scene once. Then, you should be able to re-visit any scene with no permanent increase in memory. If that's true (and I suspect that might be what you saw), then you don't have a memory management problem.


Note that if all scenes are kept "alive" even when not displayed, and there's any significant memory cost associated with those scenes, you may have a memory usage problem (as distinct from a memory management problem), where the system starts to see your app as a memory hog. That's an entirely different question, if there could be architectural changes to your app that let it minimize its memory use. If you're hogging megabytes, them maybe you should try. If you're only hogging kilobytes, it probably isn't worth bothering about.


Anyway, congratulation of making progress!

You're absolutely right!! As it stands right now, after the initial visit to any scene, NO subsequent visit to that same scene ever pushes the memory higher than whatever it was before. I think you're spot on about now it's just a memory usage issue. I'll go back through and see if there are ways I can 'trim' out some memory heavy pieces. (It would be nice if it was just holding things in the kilobyte range so I wouldn't need to worry but I can see from watching the memory usage that I'm definitely in the megabyte zone!!)


Again, I can't thank you enough for all your help!! My app is in SUCH better shape now and getting that much closer to being ready to submit to Apple!!


Thanks again!!