Addressing Retain Cycles
Retain cycles in iOS are surprisingly easy to introduce, as I've discovered over a relatively short time learning Swift and iOS developent. Personally I think that there are a few key things that a developer should be aware of to mitigate retain cycles, and honestly, some of them are not as obvious as you might hope.
How Do Retain Cycles Happen?
In order to properly understand retain cycles in the most simple ways that they exist, I think that it is first important to understand how the ViewController
lifecycle works in iOS. In the most simple terms, when a ViewController
is created, it is appended to a stack of active ViewControllers
. When an app navigates backwards off of a ViewController
, that particular instance of the ViewController
is popped off of the stack and deallocated
. When a new ViewController
is added to the stack then the current ViewContoller
is retained the the memory of the app. The problem of retain cycles manifests when a strong reference to the ViewController
is held by another object and prevents it from being deallocated
.
For the sake of future examples, I would like to define a ViewController
as an object in code that exists with components that relate to a view, and a ViewModel
which belongs to a ViewController
and contains both business logic that the ViewController
uses to display information and related to external services in a way that we are going to find irrelevant.
As a simple example, this VC-VM example looks like this.
class ViewController {
let viewModel: ViewModel = ViewModel()
let component:UIButton = UIButton()
private func callbackFunction(stringValue:String) {
//Does something
}
private func toggleComponentVisibility(){
if (self.component.alpha == 0) {
self.component.alpha = 1
} else {
self.component.alpha = 0
}
}
}
class ViewModel {
var toggleVisibility:()->Void = {}
func serviceCall(callBack: String->Void){
self.callService()
callBack()
}
private func callService(){
self.toggleVisibility()
//Does Something
}
}
Easy Ways to Introduce Retain Cycles
There are essentially 3 main ways to introduce a strong reference to self
that will cause a retain cycle. By assigning a function pointer, by passing a function as a parameter, and by wrapping a reference to self in a closure.
Closures
All closures in Swift where self is referenced introduces a strong reference to self
. In all of the issues that are commonly faced with retain cycles in Swift, the solution is usually to wrap the offending code in a closure and explicity define self as unowned
or weak
. Defining a variable outside of a closure, and referencing it within the closure creates a strong reference to self
. This includes instances where self
is not explicitly used. A strong reference to self
is created with the implicit reference to self
. For this reason, and others that I will outline later, I think it is a good idea to get in the habit of explicitly using self
in Swift wherever possible. It makes finding possible retain cycles much easier to find.
self.viewModel.toggleVisiblity = { component.alpha = 0 }
This code introduces a retain cycle through the implicit reference to self
. Even if self was used explicitly, the retain cycle would exist, since the class level object component
was referenced within the scope of the closure.
Solution
self.viewModel.toggleVisibility = {[unowned self] in
self.component.alpha = 0
}
This solution enforces an unowned reference to self
within the closure, so if the ViewController
is deallocated, there is no retain cycle between the class and the closure.
Function Pointers
In our ViewModel
, we have a function pointer property that we will invoke within the viewModel that we need to set in the ViewController
. The simplest way to do this is by writing
self.viewModel.toggleVisibility = self.toggleComponentVisibiliy
in our ViewController
. Unfortunately this introduces a retain cycle, since a strong reference is created between the ViewModel
's property and our function defined in the ViewController
.
Solution
In order to avoid this, we would need to assign this property with
self.viewModel.toggleVisibility = {[unowned self] in
self.toggleComponentVisibility()
}
By defining self
as unowned
, we create a weak reference between self
and our ViewModel
's property. Because of this, if our ViewController
is popped off of the navigation stack and dealloc
is invoked, the reference that the ViewModel
has to the ViewController
will not prevent ARC from deallocating
the ViewController
.
Function Parameters
Now, lets say we want to pass our callbackFunction
from our ViewController
back to our ViewModel
. We can do this simply by passing our function as a parameter which matches the expected function pointer signature of the function in the ViewModel
from within our ViewController
.
self.viewModel.serviceCall(self.callbackFunction)
This works because the function parameter is expecting a void function pointer, and the function being passed is a void function. In Swift, however, function parameters are passed by reference, and this ultimately creates a strong reference to the ViewController
.
Solution
We can force this to be an unowned reference to self by wrapping the parameter in a closure.
self.viewModel.serviceCall({ [unowned self] str in
self.callbackFunction(str)
})
This accomplishes the same result as the Function Pointer solution. The explicit reference to self
that became a strong reference has been wrapped in a closure that has self
forced as unonwned. This allows our ViewController to be properly deallocatted
.
Bonus Round: RxSwift/RxCocoa
For those of you who use RxSwift or RxCocoa in your projects, there is a very common way that retain cycles are introduced.
Observable.just(element)
.subscribeNext{ element in
self.doSomething(element)
}
Subscribe blocks are closures, and therefore, references to self
need to be unowned or weak. This applies for onNext,
onError
, and onCompleted
as well as any other operators that include closures that would reference self
- custom or not. This is one of the main reasons I think that it is important to explicitly reference self
, because with Observable
chains, it's easy to introducde implicit references without much thought.
Solution
Observable.just(element)
.subscribeNext{ [unowned self] element in
self.doSomething(element)
}.addDisposableTo(self.disposeBag)
In this solution, not only do we have to explicitly define self
as unowned
, but we have to add the entire Observable
to a DisposeBag
. The DisposeBag
is an object that belongs to the RxCocoa library that should live as a property on the class that owns the Observable
. The DisposeBag
gets cleaned up with the parent class when it gets deallocated
. If this is not done, the subscription to the Observable
will create a retain cycle, which will cause your subscription to live longer tha the lifetime of the screen it is on.
Conclusion
Unfortunately, all of the solutions offered make the code look really bad. This gets amplified when the code includes lots of Reactive sequences, with function parameters, and callbacks. The simple implementations often introduce retain cycles that can render the code unreadable. This ends up being my biggest problem with Swift as a language. While there can be some refactoring done to make the code more readable, a lot of the potential for clean, readable code actually ends up causing severe issues that can render an app unusable.
Further Reading
For more explicit information on the nature retain cycles, how to avoid or resolve them, or just further reading, I recommend reading this article at KrakenDev.io which I used as a resource while troubleshooting my own issues, and while providing this short writeup. For a more technical explanation of why retain cycles occur at an Obj-C level, read this article by Matt Gallagher.