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!
You ask:
How to properly allocate/initialize a weak variable?
You should:
Create your object with local variable:
let mapView = MKMapView()
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)
Update your property to maintain a weak
reference to this newly instantiated MKMapView
.
self.mapView = mapView
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:
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:
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.)
See https://stackoverflow.com/a/30993476/1271826 for more information about “Debug memory graph” feature.
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:
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.