Two of the biggest problems iOS developers face are leaks or retain cycles. Both can bring several drawbacks to an app such as high memory consumption, random crashes, bad performance, etc.
Because of that at Wolox, we decided to research and develop techniques to avoid these problems, make robust software and ultimately make the developer’s job easier.
What is a leak?
A memory leak occurs when a given memory space cannot be recovered by the system because it is unable to tell if this memory space is actually in use or not.
One of the most common problems that generate memory leaks in iOS is retain cycles. This occurs when we make circular references between two or more objects. A retain cycle prevents that the memory used by these objects be released even if the creator of these objects release them.
For example, if we have a class person and a class Apartment (as shown below)
And if we use it like this:
It will generate retain cycles and neither of the two objects will be released because both have a strong reference to the other as seen in the graphic below.
Swift and Objective-C have a reference counter (ARC) which is responsible for releasing unreferenced memory (in other words the one that is not used). This process works by counting the strong references each object has.
Strong references increment the reference counter by one while weak references don’t increment the counter at all (they set the value to nil when the object’s reference count reaches zero). When an instance has zero references it is released. However, ARC doesn’t detect retain cycles.
In our case, we only have strong and crossed references, therefore, both instances will never be released.
How to fix leaks?
What we can do is declare one reference as weak and the other as strong, therefore, the circular reference is broken.
The two common cases where to find this kind of problem include:
- Case 1: Closures
- Case 2: Delegate patterns
A retain cycle created by a closure can occur when a strong reference to an object (let’s call it ObjectA) has a strong reference to another object ( let’s call it ObjectB) and ObjectB has a strong reference to the closure. In the graphic below we can see a representation of the scene described.
Case 1: Retain cycles caused by closures can be solved using the `unowned` or `weak` keywords that way the circular reference is broken.
On the other hand, retain cycles caused by the use of the delegate pattern (case 2) can happen when the delegate is not declared as weak having a strong reference to the delegate. For example, if we have a View Controller that implements the delegate from another, which is not declared as weak we get a graphic like this:
For more information on memory management check out this link: http://krakendev.io/blog/weak-and-unowned-references-in-swift
At Wolox we use an MVVM pattern to make slimmer view controllers more orderly, smaller and to separate responsibilities.
The binding analyzer was one of the first tools that we used to detect leaks. This process consists in assessing the difference between the amount of binds and amount of unbinds that were realized in the view controllers. By binding we mean connecting the view model’s properties to the controller’s view outlets and actions. With this number we can then distinguish the view controllers and view model that were released from those that were not.
We must be careful not to detect false memory leaks caused by long-lived objects (objects that remain alive during the entire life cycle of the application), like the view model that manages the tab bar. In order to so we have a “registry” that keeps track of this long-lived objects. This registry is implemented using a singleton array that contains identifiers of this long-lived objects.
Additionally, we need to register and remove every time we perform a bind or unbind of our view models. For this, we also have a singleton with an array of structs that contains the data of our registered view models. Furthermore, this class will analyze if we remove a view model that was registered as “long-lived” which should not be released.
After making the registerUnbinding and registerBinding functions, we can register the bind or unbind view model. This method is as follows:
Finally, we need to add a button to trigger this tool.
After implementing a new feature we can use it to run a BindingAnalyzer to check if there are any leaks within the app.
The Binding Analyzer tool is one we use at Wolox that produces excellent results. Even by using this tool there is still a chance for a leak to occur because every time there is a change in the code base, we must run the tool manually.
Automated memory leaks detection with unit tests
The problems that arouse due to the Binding Analyzer tool got us thinking of how to automatically detect leaks using unit tests.
Normally when we add a new feature we test its behavior but do we test when new leaks are added?
At Wolox we use leak testing for features such as view controllers and view models. Using this technique eliminates the process of manually checking for leaks. One way to implement this is by using Nimble and Quick.
As you can see the tests consist of two parts, the first part is responsible for checking if the leak exists in both the view controllers and view models.
- Given a factory of view controllers a new controller is created, referencing previous models.
- Consequently, it must posses a weak reference to the view controller as well as your view model.
- Now, remove the strong reference associated with the view controller and hide its view. This should allow the view controller and view model to be released.
- Finally, check if the view controller and view model were released properly. If the assertion fails, one or more leaks were found in the view controller or view model.
It is advised to use a different Queue with a little delay to be sure that the view controllers and view models have the appropriate amount of time to be released.
How to implement the factories:
The second part of the test consists in detecting if all the classes extend from a UIViewController and comparing them with the amount of factories that are present. In the case that we miss one or more the test will fail (a constant reminder to test the new feature). To detect all the classes that extend from another we can do the following:
The earlier we detect the leaks, the more likely we are able to avoid and fix them in the code base. This is especially important when the project is growing as it will prevent many difficulties later on.
Although this tool is still in the stage of development it has been delivering outstanding results. We are hoping to continue to improve and test this framework with other projects and teams. With the ultimate goal of making it open source, so stay tuned!
Feel free to leave a comment, question, or suggestion below.