Contents


7 foolproof tips for iOS memory management using Swift and Objective-C

Techniques I use to debug apps, find memory issues, and avoid ARC traps

Comments

Managing memory is important and necessary for anyone developing native iOS applications, and that's still true if you're using Apple's new programming language, Swift. I've come across and solved several memory issues here at IBM's Mobile Innovation Lab.

First, a little history

Objective-C has never implemented garbage collection in the way languages like Java have. When developers first started using Objective-C, they had to manage their memory manually under a Reference Counting system. For the most part, this just meant adding the keywords retain, release, and autorelease when allocating and deallocating objects.

A few years ago, Apple released Automatic Reference Counting (ARC) in Xcode 4.2, which took the load off developers having to manually do the reference counting. It accomplished this simply by doing everything a developer would do to manage memory. Even with ARC, though, memory issues can still happen, so we'll look at traps to avoid using ARC and techniques for debugging and finding memory issues in your application.

Common memory issues

Retain cycles

In the most recent Objective-C app I worked on, fixing a retain cycle showed the biggest improvement in memory of all the memory bugs I fixed. A retain cycle is essentially when two objects retain each other. This goes against the standard rules of object ownership, leaving both objects with no authority to release the other, causing a memory leak (the numbers are the retain count):

Diagram showing how                     retain cycles cause memory leaks
Diagram showing how retain cycles cause memory leaks

As the diagram above shows, two objects have a reference to object B, so that when object A is released, object B lives on because object C is referencing it and now there is nothing left to release either object B or C. Thus, we see ARC removed our concerns with retain/release calls, but we still need to think about strong and weak pointers as well as retain cycles. Retain cycles are also possible in Swift , since it also runs on ARC.

How I fixed it

The app I was working on did a lot of image handling. My team became aware of memory issues when, on an iPhone 4S, the app would crash after about 5 minutes of running our UI automated tests. I eventually came across something like the following:

@property (nonatomic) id<MyDelegate> delegate;

In one of my viewcontrollers, we were assigning self to this delegate, which at first glance seems fine. Then I found out properties that don't specify are implicitly strong. So this property was holding a strong reference to my viewcontroller, which prevented it from ever being deallocated, causing memory to climb fairly rapidly. Once I figured this out, I promptly made this property weak:

@property (weak) id<MyDelegate> delegate;

After adding the weak attribute to that property, the app ran for 45 minutes on an iPhone 4S — nine times better by changing just one line of code!

Unnecessary caching

In our app, we were downloading a lot of different images and caching them, which is not the ideal use case for caching. Caching is ideal for storing frequently accessed objects, and we were not frequently accessing these images. Although, NSCache and other caching libraries should discard parts of the cache when you are running low on memory, some developers may want more control.

Issues in caching can also be present with Swift as with Objective-C, because many Apple frameworks (Foundation, UIKit, CoreLocation) are still written in Objective-C. Nevertheless, you as a developer need to decide if it would be beneficial to use a caching system; otherwise, it can cause unnecessary memory growth over time.

How I fixed it

I wanted greater control of the cache for the app we were working on, so I used a caching library that cached the photos for our table view until that view was deallocated; this is when we cleared the cache. This way, our table view scrolling was smooth, but we did not hold images in memory more than necessary.

Not knowing what ARC handles in C

In our Objective-C app, not knowing what ARC handles was not much of an issue, but I did make sure that we were handling C code appropriately. ARC essentially does not apply to C iOS libraries (such as Core Graphics, Core Text) or self-written C code. In Core Graphics, you have to release variables yourself with C function calls:

CGImageRef imageRef = CGImageCreateWithImageInRect([self.cropView.image CGImage], CropRect);
cropped = [UIImage imageWithCGImage:imageRef];
CGImageRelease(imageRef);

We used that piece of code to do the actual cropping in our app, but notice we created a Core Graphics image reference and then promptly released it after we were done.

Note for Swift: This case does not directly correlate to how things work in Swift. For Core Graphics specifically, developers do not have to manually release CGImageRefs; ARC actually does handle this for you in Swift. This is not true for all C libraries in Swift, however, so be sure to check the documentation before you make any assumptions. The fact is, Swift has nice new features and a lot of syntactic changes, but it still has some pretty close ties with Objective-C.

How to debug and avoid memory issues

Override the dealloc method

Overriding the dealloc method on a viewcontroller will help ensure that a viewcontroller is being deallocated when you expect it to be. It can be as simple as the following Objective-C example:

-(void)dealloc
{
 NSLog(@"viewcontroller is being deallocated");
}

Using this is technique helped me find the retain cycle in our app. It showed that a certain viewcontroller was never being deallocated, but more instances of it were being allocated, thus creating the big memory leak. This is one of the first strategies I would follow in debugging memory issues.

Manually create autoreleasepools

The Apple instructions for creating autoreleasepools say that developers typically will not need to do it themselves. There are some cases, however, where creating your own can help reduce the peak memory footprint of your app. Autoreleasepools help because instead of waiting for the system to release whatever objects you have made, you are telling the system to release those objects at the end of the block (or closure in Swift). This example will help explain:

for (int i=0; i<5000; i++)
{
 @autoreleasepool {
 NSNumber *num = [NSNumber numberWithInt:i];
 [num performOperationOnNumber];
  }
}

Memory probably doesn't get too high when we are just dealing with numbers, but the objects are being released as you iterate through the loop and not all at the end. When dealing with objects like UIImages or even base64 NSStrings, memory could climb pretty high before any of those objects are released. I used a few autoreleasepools in our app, but since we were not dealing with a lot of memory heavy data at any one time, I don't think it made a huge difference.

Note for Swift: Few things have changed with memory management in Swift, but using autoreleasepools actually has. When working in a Swift project that also uses Objective-C, there's a chance you might need to use or could use autoreleasepools. If you are working in a purely Swift project, then you should never have to write an autoreleasepool closure. By the way, the syntax is the same for autoreleasepools in Swift, but without the @ symbol at the beginning.

Isolate possible problem areas

If you have been debugging your code and are not sure what is causing your memory problem, it can be helpful to isolate pieces of code you are suspicious of. For example, you can temporarily set up your code to call a particular method many times and see how your memory allocations and deallocations react. Also, in a navigation-based app, it's helpful to continually push and pop the viewcontroller with a potential problem method and see if everything you expect is being released on each pop.

At one point in our app, we weren't sure if an image-cropping view was causing our app's memory to climb, so I isolated the whole viewcontroller inside a new Xcode project. After analyzing the project in Xcode Instruments, I saw nothing that would lead me to believe this viewcontroller was the issue. I performed this practice throughout my debugging, and it was effective in showing where the memory problem was not, saving me a lot of time in meticulous debugging.

Debugging with Xcode Instruments

My last tip for you is using Xcode Instruments for debugging. Instruments an application in the Xcode toolset. It has everything you might need to debug, test, or optimize your app. Within the Instruments app, there are many kinds of instruments such as Allocations, Leaks, Automation, and Time Profiler, to name a few. I'll focus on the Allocations instrument in the Xcode 6 Instruments tool. I used it the most when debugging our memory issue, and I'm sure it can help you now or in the future.

Before we start, you can open Instruments from a spotlight search or straight from Xcode, like so:

Screen capture                     showing Instruments selected from Xcode
Screen capture showing Instruments selected from Xcode

Step 1. Pick the Allocations instrument

  1. Choose the profiling template for Allocations: Screen capture                     showing the Allocations profiling template selected
    Screen capture showing the Allocations profiling template selected
  2. On the main Instruments interface, click VM Tracker, if present, and press the Delete key since you won't be needing that particular instrument: Removing VM Tracker                     from the Instruments
    Removing VM Tracker from the Instruments

By clicking the plus button in the top right, you can add more instruments for different kinds of testing, but I won't be covering them in this tutorial.

Step 2. Set up your Instruments settings

Before running any analysis, there are a few things you need to do. First, you need to plug in an iOS device that has your app installed on it. It must be a physical device because the iOS Simulator is still a simulator and may not accurately represent memory use in your app or how an app might perform under memory pressure.

To pick your target, click My Computer near the top, hover over your device, and then pick your app from the sub-menu:

Selecting your app                     for analysis
Selecting your app for analysis

Next, there is a panel where you can alter the settings for the types of allocations you will be viewing. Besides making sure the Created & Persistent bubble is checked, there is not much you need to do beforehand.

Updating the                     settings
Updating the settings

Optional: I like to zoom in on the allocations view. You can do this by clicking Allocations on the left and pressing Command+.

Step 3. Press record to run the instrument

Once you press the Record button in the top left, your app will start up on your device, and Instruments will begin to chart your allocations. All you need to do here is run through your app, focusing on possible problem areas to see if more memory allocates than deallocates. This could mean doing a lot of repetitive tasks, but you'll thank yourself later.

You should see something like this:

Screen capture of                     Insruments charting your allocations
Screen capture of Insruments charting your allocations

I recommend running through your app once and getting to a stable point in memory so you have a good baseline that will make any increase noticeable. When you are satisfied you have enough data to test, press the stop button in the top left.

Step 4. Analyze

  1. The first thing I do is set my inspection range to measure the total persistent bytes at my baseline. That persistent byte number is located right under the allocation summary.

    Screen capture                     showing total persistent bytes
    Screen capture showing total persistent bytes

    To actually set the inspection range, use the keyboard shortcut Command < for the left inspection range and Command > for the right inspection range. In our app, we have a baseline of about 20MB.

    Screen capture                     showing baseline allocations
    Screen capture showing baseline allocations
  2. Then, I move my right inspection range to a point where I had run through the app again and came back to our root. Here, you can see memory is about the same. So, by doing this a few more times and seeing your memory come back to our baseline, you can assume there are no major memory issues. Screen capture                     showing iterating through your allocations
    Screen capture showing iterating through your allocations

There are different ways to analyze this data that I won't cover here, but be aware that there's a whole drop-down menu of ways to view and analyze your data.

Screen capture                     listing other ways to view and analyze your data
Screen capture listing other ways to view and analyze your data

Step 5. Marking generations

If you prefer not to deal with the inspection ranges as much, there is a feature called Mark Generation. There is a button for it on the right panel of instruments.

Screen capture                     showing the Mark Generation button
Screen capture showing the Mark Generation button

This button will mark points on the timeline of instruments based on where the inspection line is. It does this in order to keep track of all the allocations since the previous mark, or from the beginning if there are no other marks. You can mark generations as you are running the allocations instrument or after you have stopped the run, as in this example:

Screen capture                     showing marked generations
Screen capture showing marked generations

I did another run, and as you can see, little red flags appeared on the timeline as I marked each generation. I can also see the generation view on the bottom and view all the data and how it varies from generation to generation. This is a quick way to see the growth between screens as I navigate through our app. Depending on your situation, this can be very useful, but other times it may just be easier to manually set your inspection ranges after you've recorded your session.

Step 6. Check out the stack trace

The last thing to cover is looking at the stack trace. For this, you want to set your inspection range to highlight all the allocations, and then look at the statistics view, making sure the Created & Persistent bubble is selected on the right panel. In the statistics view, make sure Persistent Bytes is sorted from highest to lowest. There are a lot of allocations here, and it can be hard to understand what is going on, since a lot of them are system allocations.

Going deep

  • Look at the largest allocations and click on the right-facing arrow. A lot of times there will be allocations inside the ones you clicked on and many of them won't have meaning to you.
Screen capture                     showing probing the larger allocations
Screen capture showing probing the larger allocations
  • As you highlight different allocations after clicking an arrow, continue looking at the extended detail on the right panel. Eventually you will come across some bold text that leads to actual code in your project, telling you what the issue might be.
Screen capture                     showing extended detail
Screen capture showing extended detail
  • If you double-click one of the bold items in the stack trace, it will take you to the actual code (assuming you ran allocations on an app you own).
Screen capture                     showing actual app code for analysis
Screen capture showing actual app code for analysis
  • There are a lot of useful things about this view, one being the mostly yellow tags on the right showing you just how much memory each method call is taking up. Every app is different so you, the developer, have to decide if the highlighted method is a problem, something you can optimize, or just an unavoidable part of your app.
  • In my case, that UIColor variable is something that is persistent and used throughout our app and is therefore, acceptable throughout the life of our app.

Conclusion

Things are always changing, with Xcode updates and new developments like Swift. As long as iOS development does not implement a garbage collector, you need to be careful to avoid memory leaks — preferably during the development process and not when your application is almost finished. I hope the tips in this tutorial will help you fix many of your memory issues and build memory-efficient apps.


Downloadable resources


Related topics


Comments

Sign in or register to add and subscribe to comments.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Mobile development
ArticleID=988886
ArticleTitle=7 foolproof tips for iOS memory management using Swift and Objective-C
publish-date=11072014