Unfortunately, the iPhone system, the Objective-C base is hopelessly stuck in the early 80s and doesn't have a garbage collector. It has something called Automatic Reference Counting(ARC). The ARC came about a couple of years ago and was hailed as a major break through. Really? We really are in the 80s.
So, what is ARC? Before ARC you used to have to do RC, i.e. reference counting, which was a tool to handle deallocations in the heap. If you allocated an object you could deallocate it when you didn't need it anymore, by calling .release() on it. Conversely, if you needed it held onto in multiple places you could call .retain() on it. These calls would lower and increase the reference count on that object respectively. When the count would hit zero, then magically, the space the object was taking up would be given back to the heap. In order to deallocate space that the object may have references to, you would have to override a function called dealloc() and call .release() on some references, or you used some other strategy. Some strange strategies did emerge, but that is not the subject here.
Apple discovered, or worked hard at, noticing that the approach developers were doing was pretty boilerplate for deallocating objects. At least that what I surmise, since that is the way garbage collectors have been working for decades. Seriously, we tackled the issue of garbage collection a long time ago in the 70s with LISP and probably before, seriously enhancing garbage collection in the 80s and 90s to be really efficient and effective, yielding smarter, and also less stressed programming.
Enter ARC, that is "Automatic Reference Counting". Voila, It's much like not having to manually shift gears in your car, which is something we developed in the 40s, but I digress. You get my point. The 90s are over.
So, now with the ARC, Apple will take care of this reference counting for you. Apparently, the compiler would be able to notice when ".retain()" or ".release()" needed to be called, based on syntax. Also, at deallocation, much like my 'dealloc()" override strategy mentioned above, one would assume that it would "automatically" reduce all the reference counts of space which was being held by the object being deallocated. So, one would think. Think again Einstein.
One other supreme <sarcasm> benefit </sarcasm> with ARC is that it is NOT a garbage collector. "It's much more efficient than a garbage collector," they say. So, unlike a garbage collector, you cannot just ditch the top pointer of an object graph and have the whole object graph go away. No, uhh unnh. no nada, no god damn way. If you have any reference cycles in the object graph, you are hosed because a cycle chain will never reduce its reference counts to zero, and therefore will never be freed. I'll leave that thesis to the reader as an exercise.
So, Apple developed, at least in Swift, two different types of references, "weak" and "unowned". Pointers to objects declared with these keywords are said not to increment or decrement reference counts on the object for which they are references. For example, the following declarations:
class A {
var myChild : B
}
class B {
weak var myParent : A?
}
var a : A? = A()
var b : B? = B()
b.myParent = a!
a.myChild = b!
Now, if you got rid of the reference "a" by the following:
a = nil
Then, both objects would automatically get deallocated. Magic, eh? My god, what an accomplishment!!! If myParent were not a "weak" reference to A, then none of it would get deallocated. Okay, you say. That seems logical.
So, what is the problem with this approach?
YOU HAVE NO FUCKING CLUE whether any 3rd party software actually declared myParent a weak or unowned reference.
In fact, the above example is quite simple for illustration, but if the object graph is complicated you have to do a whole bunch of this stuff, just to MANAGE the GARBAGE! You have to MANAGE the GARBAGE.
Let me say that one more fucking time.
YOU have to MANAGE the FUCKING GARBAGE!!!!!So, back to my point. My problem arose from doing this:
class MapViewController : UIViewController, MKMapViewDelegate {
init() {
mapView = MKMapView()
mapView.delegate = self
.....
mapView.addOverlay(MYOverlay())
}
func mapView(mapView: MapView, rendererForOverlay: MKOverlay) -> MKOverlayRenderer {
return MyOverlayView(rendererForOverlay as MyOverlay)
}
}
MyOverlay would get a MyOverLayView and go on with its business. So, one would think that when MapViewController got deallocated, it would also deallocate the MyOverlayView, which it does. One would also think that the mapView would in turn deallocate the overlay, or at least reduce its reference count. Apparently, after painstaking investigation, that is NOT the logical case. WTF?
It took me more than a fucking week with debug statements and the almost usless Xcode debugger to figure out what I now call an undocumented requirement. It's not documented anywhere probably because, why would you want to deallocate a mapView? Fuck me.
So, as I labored for weeks, I find this solution. You have to keep a reference to the overlay you added, and also remove it from the mapView when the ViewController is getting deallocated, Otherwise, I can only surmise that it is holding onto a lot of closures, or something that isn't even referenced in the overlay object itself, but to things in the MyOverlayView.
This was the hard part. The matching MyOverlayView was getting deallocated, however, a lot of stuff that was actually referenced in the MyOverlayView was not. The MyOverlay was NOT getting deallocated, and it actually contained references to anything that was still being held. But that stuff in question did have some indirect references in the MyOverlayView. But let me say this again. the MYOverlayView object was deallocated. So, that lead me to look for something else, possibly stupid, that *I* was doing. Think again Einstein.
After trying everything else that was related to the objects/memory that should have been getting released and was not, this approach magically seemed to do the trick:
class MapViewController : UIController, MKMapViewDelegate {
var myOverlay : MyOverlay?
init() {
mapView = MKMapView()
mapView.delegate = self
.....
myOverlay = MyOverlay()
mapView.addOverlay(myOverlay)
}
deinit {
mapView.removeOverlay(myOverlay)
}
}
Then, BINGO, WTF? Really? My memory leak from releasing and creating difference instances of the MapViewController disappeared, well, as far as I can tell.
One would think, that if the MapViewController only carries a reference to the MapView that when it gets deallocated, it should also deallocate the MapView object, and therefore AUTOMATICALLY remove the overlays. Obviously, that is not the case, and that is not documented anywhere.
I am kind of thinking that the MKOverlay class actually holds an undocumented non-weak reference to the MKMapView object, and the MKMapView object obviously holds a reference to the MKOverlay in a list somewhere. So, neither the mapView nor overlay object get deallocated due to their reference cycle. I'm still at a loss for why all the rest of the stuff was being held onto as they never had references to the mapview except within executed function through the MyOverlayView, which had been deallocated. Who the fuck knows?
This situation does not seem to be documented probably because nobody would probably think about deallocating a ViewController containing a mapView. In my app, this situation may occur as much as the user wants it, so I've gotten burned.
Finally, though, I now know how to solve the memory leak with this mapView, and hopefully this blog post helps the next poor schmuck who has to do MapKit shit in iPhone development.
Now maybe I can finish this fucking iPhone App that took a significantly less time (an order of magnitude less time) to implement in Java and Android.
Thanks Apple.
No comments:
Post a Comment