Last week's blog was about how to write unit tests to minimize bugs from the start. But let's admit it, writing unit tests alone is just not enough. That's why I want to talk today about Allocations, one of the tools available in XCode instruments toolbox.
Allocations is about memory usage. Memory management is based on managing reference counters. When an object is created it starts out with a reference count of 1. Strong references increase the reference counter, and weak ones don't. When a reference counter drops to zero an object is freed from memory.
Although memory management is handled by LLVM using automatic reference counting (ARC), this doesn't mean that memory issues belong in the past. Sometimes objects keep hanging around. To explain why, let's take a look at different types of memory bug classes.
Classes of memory bugs
- retain cycles
2 objects reference each other via a strong reference. Object A references object B and B references A. But often retain cycles are a little harder to spot since they stretch across multiple classes e.g. A references B, B references C aso until Z references A again. One way to discover retain cycles is with Leaks, another Instrument available with XCode. Today however I am gonna talk about Allocations, which can help tracking down this class of bugs as well.
- abandoned memory
Abandoned memory is memory that is still referenced but not used any more. Heapshots or Generations as they are called now can help to track down this class of bugs.
With that now out of the way, let's talk about the different ways of tracking down memory issues with Allocations. To use Allocations, start Instruments and choose the Allocations tool. Then select your app from the drop down menu and tap the record button.
Heapshots / Generations
Generations are one of the more prominent features of Allocations and are based on repetitive tasks. So to start with, we initially need to choose a view controller in our app with a certain state to which we are able to return to. This can be for example the start screen in the app that allows a user to login and that serves as a segue to other viewcontrollers. Those usually enable users to perform various other tasks a, b, c. So an example journeys could look like this:
- Log in - Log out
- Log in - Task a - log out
- Log in - Task a - Task b - log out
Whenever you return to the start of your journey, you tap the Generations button (titled 'Mark Generation'), which makes Allocations compare each heapshot n against the previous heapshot n-1 and list their differences in their respective drop down menu. For this purpose, I recommend to prefix your classes and restrict the output to those only via the filter menu. Ideally you shouldn't see any of your classes in those diffs. If you do however see some that belong to you, it could mean that there is potential memory issue. I say 'potential' since it doesn't necessarily always mean that there is one. You might have to consider a few other things as well.
Your app might take some time to establish its initial memory requirements. So after you start up your app, it might still be in the process of allocating memory. Furthermore, sometimes classes are not fully initialized from the start and will be so only after their first reference (e.g. lazy loading). You can see that for example with UITabBarControllers. If you use a tabbarcontroller right from the start in your app, the viewcontrollers in the other tabs only become fully initialized after their first selection. So to counteract this initial memory allocation, you might want to start with a "warmup" journey.
Apart from that you might want to ignore all references to coredata objects. Those objects are managed by the system and will be released over time. This means that even after you have done x iterations, the heapshot of your first iteration might still change i.e. objects will still be removed from the memory diff.
Last but not least, I'd like to point out that with Allocations you don't necessarily have to tap the Generations button after each journey. Since XCode 6 you can add the Generations marker to the graph later. To do so just add a marker to the graph followed by a tap of the Generations button. Allocations will insert the new heapshot in the list at the right position in your current heapshot list. Ideally adding those markers should be fairly easy, since the resulting graph of each journey should more or less result in a staircase pattern.
Allocations Statistics pane
I use this more often to track down memory issues than anything else. It's kind of a variation of the Generations method with having only one heapshot. Like with Generations it is better to restrict the output just only to your classes because not everything in here is probably of your interest. To use this method, switch from the generations pane in Allocations to the statistics pane. The statistics pane lists all current objects in memory along with some information of how often they are instantiated, had been instantiated or how much memory they occupy.
Again, choose a viewcontroller with a certain state in your app you are able to return to and then move around in your app for some time. This time however we dont't take a heapshot but check instead which classes have been instantiated when we return to the start of our user journey. As the programmer of your app you should have a healthy understanding of what's going on in your app. Take a look at the Persistent column. What is the counter saying? A high value usually indicates that something fishy is going on and you should take a closer look at that object and its use cases. But check low persistent value as well e.g. a Singleton should always have a counter of one.
Whereas heapshots show differences of various states of your app in the past, the statistics pane provides you with an overview of what is currently going on in your app. It doesn't have the overhead of having to perform certain journeys over and over again to see results but requires a better understanding of the app under observation.
Allocations is one of the most used tools in Instruments toolbox. A good understanding of how to use it helps you to gain insight into the memory consumption of your app. It's essential in the app development since as elaborated above helps you
- track down memory issues
- flatten memory spikes that might trigger the iOS watchdog timer to kill negligent apps in this matter and last but not least helps
- reduce the memory footprint of your app.
This is an important point to remember since any app is in constant competition with other apps for memory. Don't forget memory on a mobile device is a less abundant resource than memory on a desktop computer. Treating it as a scarce resource increases the performance of your app and enables iOS to provide a better overall user experience.