Home > Software engineering >  How to properly allocate/initialize a weak variable? (Swift)
How to properly allocate/initialize a weak variable? (Swift)

Time:03-23

I'm trying to get rid of a memory leak associated with an MKMapView. I think the main problem is that I created my entire project without using storyboard as a series of views which I manage by either setting the alpha to 0 or shrinking the view to a height of zero. I have a mapView initialized in ViewController.swift as such:

class ViewController: UIViewController{

//Properties
let mapView = MKMapView()

} 

and then in another .swift file an extension of ViewController, something like:

extension ViewController {

private func setupMapViews(){

    //MAPVIEW:
    mapView.frame = CGRect(x: 0, y: view.frame.height, width: view.frame.width, height: 0)
    mapView.overrideUserInterfaceStyle = .dark
    mapView.mapType = .hybrid
    mapView.delegate = self

}

On my app's main view I have a button which segues to this mapView using an animation. Checking the debug navigator as I run the simulator I see that the memory usage jumps from something like 90 MB to 160 MB when the segue occurs. When I press the Done button I have added to the mapView, the memory usage remains at around 160 MB, telling me there is a memory leak. I tried at first to simply remove it from the superview when the segue back to the main view controller occurs, but the app stays at around 160 MB, telling me that there is a retain cycle. I tried instead to change the declaration of the mapView to a weak var as such:

class ViewController: UIViewController{

//Properties
weak var mapView: MKMapView?

} 

and then to initialize it in viewDidLoad()

override func viewDidLoad() {
    super.viewDidLoad()
    let mapV: MKMapView? = MKMapView()
    self.mapView = mapV
}

but now when I attempt to segue to the mapView from my main view, it does not initialize, and the view does not appear. What am I doing wrong?

Any help is appreciated, thank you!

CodePudding user response:

You ask:

How to properly allocate/initialize a weak variable?

You should:

  1. Create your object with local variable:

    let mapView = MKMapView()
    
  2. Before your local variable falls out of scope (i.e., within the current method), add it to your view hierarchy (so that your view hierarchy keeps a strong reference to it):

    view.addSubview(mapView)
    
  3. Update your property to maintain a weak reference to this newly instantiated MKMapView.

    self.mapView = mapView
    
  4. You can later remove the map view from the view hierarchy with:

    mapView?.removeFromSuperview()
    

    When you do that, the map view will no longer have any strong references, and the weak property will automatically get set to nil and your memory will be recovered.

So, if you want to add your MKMapView when the user taps the button, move the above steps to your button handler (or methods that this calls) rather than viewDidLoad. If you leave this code in viewDidLoad without step 2, the MKMapView will be immediately deallocated.


A few observations:

  1. When diagnosing memory consumption, do not rely on the result of the first iteration (which will include memory used during caching), but rather focus on subsequent iterations.

    In a quick test, in an app running at 50mb as shown in the Xcode “Debug Navigator”, it jumped to 120mb when a map was presented, and dropped to 85mb when the view controller with the map was dismissed. Then, presenting the view controller with the map a second time jumped up to 120mb again, and dismissals brought it back to 85mb. And it continued with this pattern for subsequent presentation and dismissal of the view controller with the map. So, there’s lots of caching going on (~35mb worth), but the lack of continued memory growth in subsequent launches means that there is no (substantive) leak.

    Allocations tool (“Product” » “Profile” » “Allocations”) illustrates this even more clearly, here where I presented and dismissed the map view three times:

    enter image description here

  2. Let us assume that you did the above diagnostics and confirmed that you were losing memory for every iteration, not just experiencing cache behavior.

    I would then run the app in the debugger, dismiss the map view within the app, and then use Xcode’s “Debug memory graph” feature to figure out what was keeping a strong reference. Here, I searched for MKMapView in the debug navigator, and it shows me the graph, with a white line going back to the view controller in question. (I ignore the lines in gray, as those show weak/unowned references.)

    enter image description here

    See enter image description here

    Now, I have signposts where I presented the view controller and dismissed it, but you can see that this was not relevant. When I removed all references to the map view (even before dismissing the view controller), memory is recovered correctly.

Bottom line, the problem does not rest in the code that you have shared with us thus far. You have something keeping a strong reference to the map view, and we will not be able to help you in the absence of a MCVE. But hopefully the above provides some diagnostic tools (especially the “Debug memory graph” feature).

The problem likely rests in either (a) how you're removing the map view from the view hierarchy; or (b) some strong reference cycle between the map view and its child objects (e.g. annotations, annotation views, etc.). But we do not have enough information to diagnose that.

  • Related