0

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!

jmsapps
  • 388
  • 2
  • 17
  • Did you check deinit is called on view controller that you asume that makes memory leak? Did you check Leaks with Instruments? Is there any other code on that view controller that may create a leak? – Whirlwind Mar 21 '22 at 22:39
  • I've checked instruments as well as the memory graph, but at my level of experience it looks like gibberish to me. Leaks definitely does catch at least six memory leaks. I'm not sure how to implement deinit to be honest. – jmsapps Mar 21 '22 at 23:37
  • Not sure if they are fixed in more recent releases, but back in iOS13 (iirc) days there were documented memory leaks in MKMapView. Not saying these are your problem, and they may have been fixed since then, but something to be aware of. There were some workarounds (which are still in a couple of my apps :-/ ) to address these. – flanker Mar 22 '22 at 01:10
  • @Rob you mentioned when you attempted to test it in your own project that the memory goes down when you dismiss the ViewController, but I only have one ViewController in my project. Could that be the issue? I'll try to make my own test project to see if I can fix it by creating a new ViewController class for the mapView. My initial reasoning for creating my app this way was that I figured it would be easier to create any kind of custom segue that I needed, not thinking about any implications of memory leakage. – jmsapps Mar 22 '22 at 18:14
  • Okay, I’m going to have to deep dive into it. I’m not using any weak variables for the mapView’s subviews so that could be the issue right there. One thing I did notice: when I put mapView.removeFromSuperview() in the completion block of the UIView.animate function, the memory does not go down, but when I put it inside the animation block itself it does by about 30MB, but only sometimes (weird). Doing that however, negates the animation so regardless its not an option. – jmsapps Mar 22 '22 at 18:51
  • No offence taken, I was reluctant to even make a post about this because I’m not really even sure what the right question is to ask, hence I’m all over the place and unable to give a proper MCVE. Please forgive me, I’m trying to be as concise as possible. As you say, I did omit the function which adds the subviews. You can see it being called in segueToMapView() (though you’re correct, its not a proper segue.) – jmsapps Mar 22 '22 at 19:49
  • Also, I’d just like to thank you for being so helpful, I’m learning a lot. – jmsapps Mar 22 '22 at 19:50
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/243196/discussion-between-rob-and-jmsapps). – Rob Mar 22 '22 at 19:57
  • Yeah, I understand that it automatically gets deallocated. I think, if we simplify everything I've asked down to one thing, is how do I initialize a weak variable? Right now in my code I do not have it declared as weak, so going back to my question I have let mapView = MKMapView(), and in viewDidLoad I have setupMapViews() and in setupMapViews you can see i setup the initial frame for the mapView. So yeah, how do I declare it as weak and then initialize it properly? – jmsapps Mar 22 '22 at 19:57

1 Answers1

1

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 https://stackoverflow.com/a/30993476/1271826 for more information about “Debug memory graph” feature.

  3. You asked whether the issue was the fact that you are not using view controllers or storyboards. No.

    For example, I repeated the above example, but this time, called mapView.removeFromParent (and made sure the mapView reference in my code was weak) and you can see that within a second of the map view having its last strong reference removed (i.e., the purple signpost labeled “remove”, where I removed the map view from the view hierarchy, with only a final weak reference to it), memory is recovered:

    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.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • I'm looking through my memory graph, and it seems that almost every reference in my project is strong. I wasn't really aware of the concept of memory management when I started this project. One thing I'm still not understanding is how to declare the mapView as weak. As I mentioned in the question, when I try to declare it as weak, the object is never initialized when I attempt to segue to it. I'll add my code for the segue to the mapView in my original post. – jmsapps Mar 22 '22 at 19:18