Ghostboard pixel

Building Memory Efficient Apps

Building Memory Efficient Apps

The performance of mobile devices got much stronger over the last few years. However, there is and always will be a huge performance deficit compared to desktop computers. At the same time, requirements for the user interface got stronger as well. So there is still the need to write memory efficient applications for mobile devices.

Hint: This post has been updated to Swift 3 and iOS 10

What are memory efficient apps?

Generally speaking, an app is memory efficient, if it uses as much memory as necessary and as little as possible. This also means that the user interface design of the app lays down a framework for the memory consumption. An app that is very high sofisticated will need more memory than one that is not. 

We start with a little bit of history:

ARC – Automatic Reference Counting

In the early days of iOS development, memory management played a major role. Because a traditional garbage collection was too inefficient for a mobile device, iOS transferred the responsibility for memory management to the developers. You needed to increase and decrease the so-called reference counter of an object manually.

This way you were able to write very memory efficient application, because objects were immediately deallocated, when they were not used any more. But on the other hand, managing the memory on your own was also very difficult. It was very common to produce hard to find bugs. So it was clear that this was not the best solution for memory handling.

The solution was has been introduced in iOS 5: Automatic Reference Counting (ARC). From now on reference counting commands were injected at compile time. On the one hand this still leads to very memory efficient code, and on the other hand the developer doesn’t need to take care of it any longer. The solution is so good that in the meantime all Mac OS X applications are using ARC as well.

However, even if you don’t need to count references any more, there is still a lot of responsibility for you.

Choosing the right deployment target

As said above, the requirements of the app imply a certain memory consumption, and that implies a certain deployment target (the minimum required iOS version for the app to run)!  For example, if you are using a deployment target of iOS 5, the first generation iPad is still supported, which has just 256 MB of ram. Although it is very good to support as many devices as possible, it is even better to provide a good user experience on all devices that are supported! So if you want to support older devices, you need to take this into consideration at the design phase.

Here is an overview about the oldest supported devices (iPhone / iPad / iPad Mini) for iOS 5 – iOS  10:

iOS 10: iPhone 5 / iPad 4 / iPad Mini 2
iOS 9: iPhone 4S / iPad 2 / iPad Mini 1
iOS 8: iPhone 4S / iPad 2 / iPad Mini 1
iOS 7: iPhone 4 / iPad 2 / iPad Mini 1
iOS 6: iPhone 3GS / iPad 2/ iPad Mini 1
iOS 5: iPhone 3GS / iPad 1 / –

Because the update rate in the iOS ecosystem is very high, it is good practice to support just the two newest iOS versions. Beside the memory issue, supporting too much iOS versions is generally difficult in terms of both development and testing.

Images

Images are a very important aspect of mobile apps. However, they also have a high memory usage. So there are two important points in dealing with images:

First, images should only be as big as necessary! If you have a table view that needs images with 100 x 100 pixels, it is a very bad idea to use images with 1000×1000 pixels. The performance hit will be very high! If you are consuming images from a server, it is the job of the server to provide you images in the correct size.

Secondly, be sure that only as many images are loaded as needed! For example, a UITableView loads the images just before a cell gets visible. That means cells are recycled. Just imagine you have a table view with 5,000 cells and all of them were loaded, when you enter the screen. Even if the app would not crash due to a heavy memory pressure, the user experience would be very bad. So apply this principle to your own views as well! For example, if you are developing an image gallery, don’t load all images immediately, but only when they are on screen. This is a special case of the so-called lazy loading.

Lazy loading

The idea of lazy loading is to load resources as late as possible. This has two advantages:

  • Loading times are better distributed
  • There is the possibility that the loading of the resource can be avoided completely

So how to do this in iOS development? As mentioned before, table views are a good example for lazy loading. Another good way is to use the keyword lazy for properties. Imagine you want to use an array of products, that is used when certain user interactions happen in the view controller:

var products: [Products] = modelClass.loadProducts()

This array is loaded even if these user interactions never happen! So it is a waste of memory. But if you use the lazy  keyword, the array is first initialized when someone tries to access it:

lazy var products: [Products] = modelClass.loadProducts()

Even if these are just small arrays or variables, the regular usage of lazy loaded properties can sum up to a lot of saved memory.

View controllers and retain cycles

One of the badest things that can happen in terms of memory problems is that a view controller is not deallocated, when it is not longer needed. The most common reason for this scenario is a so-called retain cycle: Imagine you have a view controller A that initializes view controller B and presents it as a child view controller. Furthermore, imagine view controller A give view controller B a reference of itself. Then they have strong references to each other.

Now, if view controller A is dismissed from the screen, both view controllers are not deallocated because they both have a strong reference to each other! You can avoid this by using the Swift keyword weak. For example, if view controller A sets itself as a delegate of view controller B, the corresponding property can be declared in view controller B as follows:

weak var delegate: DelegateType?

A good way to check whether view controllers are deallocated correctly or not is by using log messages in the view controller’s deinit method:

deinit {
     print("deinit")
}

Now it is your job to check whether the message is appearing on the console at the appropriate time or not. For example, if your view controller is presented by a navigation controller, it should be deallocated and the log message should appear if the back button is pressed. For more details take a look at this post.

Control your memory usage

It is not so uncommon that a bad memory management is first recognized at the end of a project. Unfortunately, it is too late then. So it is very important to control the memory usage of your application regularly! To do so, just run the app on a device and click in Xcode’s debug navigator on “Memory”:

Bildschirmfoto 2015-08-01 um 10.23.22

[thrive_text_block color=”blue” headline=”Conclusion”]Memory management is a very important topic in mobile development. If your app uses to much memory, it gets slow and there is a high danger for it to crash. However, if you take care of this topic, you will be able to build very memory efficient apps.[/thrive_text_block]

References

Image: @ amasterphotographer / shutterstock.com