​How To Reduce Your App’s Memory Footprint

Causes of high memory usage, reducing your app’s footprint and debugging

Photo by freestocks.org on Unsplash

It might sound obvious that we should keep our app’s memory footprint as low as possible, but why actually?

There are several reasons:

  • At some point, high memory usage will raise memory warnings, performance issues, and may terminate your app if you don’t react quickly. And reacting quickly to memory warnings is not as easy as you might think.
  • When your app goes to background, it enters some kind of contest with other apps. When iOS needs memory for other tasks, it’s going to terminate high usage apps, and you don’t want to be on that blacklist.

In the end, inefficient, memory-sucking apps provide poor user experience, so buckle up and squeeze your apps. It’s time to reduce memory usage.


Common Causes of High Memory Usage

1. Retain cycles

Quite a few years ago, we had to manually allocate and release Objective-C objects. These days we have Swift and ARC (Automatic Reference Counting), but memory leaks can still show up. When they do, it’s painful.

The short version: objects can point to other points in several ways. Two basic approaches are weak and strong.

An object is released when it doesn’t have any strong reference. For example, when a UIViewController has a child view controller it has a strong reference for its child, and the child has a weak reference for its parent.

But what if two objects point to each other with strong reference? They won’t get released until the app is terminated. This is called retain cycle.

There are two main use cases for retain cycles we need to be aware of:


  1. Delegates — the basic pattern of delegation in iOS is that there is an object (and some other object holds it with strong reference), and this object holds another object, with strong reference and acts as its delegate. In this case, the other object should have a reference to the delegate somehow, and since the delegate holds it with strong, this reference has to be weak, otherwise we’re going to have a retain cycle here.
  2. Data Structures, such a trie — some data structures require bidirectional references. It can be a trie or a tree with parents and its children (I showed you the example of the UIViewController and child view controller). Remember, that each time you add a return reference, you might create a retain cycle, so be careful.

2. Timers

A timer is actually a private case of the retain cycle. The reason I gave timer its own bullet, is that it’s not obvious as a “regular” retain cycle case.

When you set up a timer, it holds its target as a strong reference and in a lot of cases, this target holds the timer as a strong reference as well.

You can avoid this retain cycle in several ways:

  1. Pass a weak reference as the target for the timer.
  2. Make the object that holds the timer and the target two different objects.
  3. Make sure the timer is invalidated when you exit the screen or when you don’t need it. This is best practice anyway.

3. Big images

There are several things you need to remember about images.

  1. The size of the image you load into the memory isn’t the size of the file, it’s the size of its dimensions. For example, if you have a compressed JPEG of 100 KB, but the image dimensions are 1000 x 1000, you actually load 1 million pixels into the memory (!). Now, since every pixel contains extra information, it’s actually 4 bytes per pixel, meaning around 4 MB in memory usage for a 100 KB file.
  2. Scaled-down images can significantly reduce your memory usage.

4. Cache

We all know that there are cases where we can trade CPU for memory. But when caching storage gets bigger, instead of having CPU issues, we might find ourselves dealing with memory issues.


Tips for Reducing Memory Footprint

1. Lazy loading

Lazy loading means that you load objects into memory only when you need them. Besides saving memory, it also reduces the loading time of the app and the screens.

You can use the traditional method of checking if the object is nil before using it, and if it is nil, allocate it and use it.

if image == nil {
image = UIImage(named : "ocean")
}

Or you can use Swift’s extremely simple lazy keyword:

lazy var image = UIImage(named : "ocean")

2. Implement memory warning methods

Your app is not always the cause of memory warnings, but it should respond to them, otherwise, it will get laggy and finally crash.

The first thing developers do when they receive a memory warning is to release the cache, but it really depends on what type of cache it is.

When iOS notices that you don’t use an object often, it compresses it, so releasing it won’t help much in terms of memory, but it can instead hurt user experience.

What you should do is release non-compressed memory objects. For example, try to dismiss/pop the current screen, or unload big images currently loaded.

3. Use NSCache

Use NSCache instead of a dictionary. It’s amazing how many developers ignore this great API.

NSCache behaves just like a regular dictionary, but it has several advantages. It is thread-safe and it releases objects when the system enters a low memory state. Not just that, it also starts to release objects you use less frequently, so it’s optimized for really good, efficient caching.

4. Use UIGraphicsImageRender instead of UIGraphicsBeginImageContext

We mentioned earlier that loading an image into memory loads four bytes per pixel.

When dealing with images, this can be a big deal. The best thing you can do about it is to replace UIGrahpicsBeginImageContext with UIGraphicsImageRender.

UIGraphicsImageRender actually identifies the type of image you are loading and allocates the optimized bytes per pixel. For example, monochrome (black/white) images will use one pixel or one byte per pixel.

5. Autorelease pool

Remember, just because we are using ARC doesn’t mean we don’t have to retain, autorelease and release commands. It’s all there, added automatically by the compiler.

The autorelease pool still exists, and it is recommended to use it inside loops.

When you’re running code in a loop, objects created inside the loop are only released at the end of the scope. This means that, in large loops, you may experience memory spikes.

The best thing to do here is to wrap the heavy allocations for each loop iteration in the autorelease block, so the objects will be released at the end of the block.


How To Debug Memory Issues

As with every technical problem, you start with an overview of your app.

Fortunately, we have a built-in tool in Xcode, called memory gauge. From this point, you have additional tools to dig in until you find your issue.

1. Memory gauge

The memory gauge is located on the debug navigator in the navigator’s pane on the left. It shows up only when you are running your app in debugging.


The memory gauge shows your app’s usage during its life cycle.

Generally, you should look at the memory gauge when you run your app, and try to pick up any weird behavior.

Examples of weird behavior:

  1. Extremely high memory usage — Apple divides the memory usage into three categories: green, yellow and red. You should always try to make your app on the green part of the scale.
  2. Continuously growing memory — When you’re using your app, you should expect your memory usage to stay constant over time. If you see it grow continuously, then you might have a memory leak or some other issue you need to investigate. Try to present screens and dismiss them, and see if your memory shrinks back in order to identify the specific problem.
  3. Go to the background — When you go to the background, you should see that your memory is reduced. If it’s not, you should think of releasing resources, such as the loading of big images, in order to stay in the background with a small memory footprint.

If you come up with issues in your overview stage, you can easily move on to the instruments tool by tapping on the “Profile in instruments” button.


2. Instruments

I’m not going to explain how to use the instruments, but this is a low-level tool (not the lowest though) to identify both leaks and allocations in your app.

Besides finding bug allocations in your app, it also lets you compare your runs between fixes.

3. Dealloc / deinit method

Every object has dealloc (Obj-c) or deinit (Swift) which is called when the object gets released.

Implement this method and put a log or a breakpoint in — this is a great, preemptive and quick way to verify that the object is being released as you expect it to be.

4. Unit test with memory metric

In iOS 13, we have a great addition to performance testing. Besides time measurement, we also have a memory metric.

If you know that a certain piece of code is generating high memory usage, it is best to measure it using the memory metric. That way you can debug the method on one hand, and on the other, you’ll have another test for that, which always great.

5. Simulate memory warnings

In the simulator, under the debug menu, you have an option “Simulate memory warning”. You can use this to see what happens to your memory usage when it gets the memory warning we talked about.

Simulating this warning on the device itself is a bit tricky. You can do some loading in your app to get to this state, or you can use a private API to simulate it (remember to move this method after you’re done).

UIControl.sendAction(Selector(("_performMemoryWarning")), to:UIApplication.shared, for: nil)

6. Memory debugger


At the bottom of the Xcode window, you’ll find the “debug memory graph” option.

If you haven’t used it, now’s the time to do it. It’s an amazing tool. It lets you see relations between objects in your app, and even retain cycles.

It also lets you count the number of instances per class (just like the instruments tool), so you can quickly examine your memory map.


Summary

As you can see, your app memory is a big deal to observe, debug and maintain.

Following the tips in the article will help you prevent memory leaks by writing proper code. It’ll help you get on top of the issue on time, by observing the memory gauge and fixing them using several tools like instruments, and memory debugger.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s