Mobile

Managing Memory on iOS

2. Use smaller resources.

Each time an iOS loads a resource, that resource has to take up memory. The resource may stay resident for the duration of a view or task or, in some cases, for the entire app session. Since loaded resources take up precious memory space, we must try to keep their sizes as small as possible.

Consider graphics files, for instance. They hold the icons and logos that appear on an iOS view. The preferred format is a 32-bit PNG, usually no larger than 64x64 square pixels (for an iPhone target). To prepare the graphics file, use a preprocess tool such as pngcrush or ImageOptim. Both resample and recompress the file for the smallest possible size and load speed. Usage notes for both tools are available on their respective websites.

Now consider text files. These hold text localized for a specific region. Their data should be rendered in serialized form, as opposed to Unicode or UTF-8. This can cut down the extraneous bytes needed for each text character.

Listing Three shows one way to handle serialized text. First, the method loadSerialText gets the path to the text resource foobar.text (line 13). Having a valid path, the method loads the resource into an NSData object (line 18). It then extracts the raw character bytes and uses them to create an NSString object (lines 22-28).

The method saveSerialText gets an NSString for input and extracts the raw character bytes (lines 49-54). It locates the resource file foobar.text (line 60) and stores the raw bytes into an NSData object (lines 61-62). Finally, it writes the object itself to the resource (line 63).

3. Avoid pre-fetching.

Some developers let their applications load their resource at launch time. Their intent is to avoid reloading often-used resources and to make those resources readily available. While this practice may be sensible with OS X apps, it will make an iOS app a memory hog.

A better way is to load those resources on demand. Then discard the resource when it is no longer needed. Doing so cuts down the memory needed by the app, and avoids inducing a low-memory state.

4. Optimize for size.

Executable size can also influence the overall memory footprint. When an iOS app launches, it loads as much of its executable code into memory as possible. The code stays resident until the application terminates.

Thus, it is important that we keep the executable size as small as possible. The best way to achieve this is to compile the project with an mthumb option. This option directs GCC to optimize the final executable by size. We get a smaller executable size, at the cost of a slight decrease in performance.

Figure 3 shows how to prepare the compile settings. First, choose "Edit Project Settings" from Xcode's Project menu to get the settings dialog. Locate the group "GCC 4.2 - Code Generation," and check the option "Compile for Thumb." If the option is not set, click to set the adjacent checkbox.

Figure 3.

Most iOS projects benefit greatly with size optimization, but there is a small group of projects where the costs of optimization may outweigh its benefits. These projects perform a large number of floating-point tasks while using arm6 opcodes. If their executables are optimized for size, their runtime performance may be severely reduced. In this situation, switching off the mthumb option may be a better choice.

Managing Memory

Apple also provides three guidelines on how an iOS app should manage its share of memory. These guidelines take into account the lack of garbage collection and the lack of volatile paging. So let us go through them, shall we?

1. Reduce the number of autoreleased objects.

Naturally, objects marked for autorelease linger longer that those with normal retain/release cycles. When there are too many autoreleased objects, the autorelease pool will need more memory to maintain its collection of objects.

Consider the sample method back in Listing Two. This method uses three autoreleased objects to perform its task. Two of those objects, tNom and tTmp, (lines 9,12) are valid only with the method scope and are disposed of on exit. The third object, also named tNom, (line 20) stays valid outside scope due to being a return value. Its disposal is then left to the calling method.

At first, this one appears to use only two autoreleased objects (lines 15, 23). Object tSet is valid only within the method scope, while object tWrd stays valid outside scope. What is not obvious is that tWrd is an array of NSString objects. Since those objects are made by the factory method componentsSeparatedByCharactersInSet (line 19), they are all marked for autorelease. So if the input NSString argument aTxt has a particularly long string, the autorelease pool may end up using more memory to hold that array.

2. Place limits on resource loading.

Most iOS apps load their resources in their entirety. This is not a problem when the resource itself is reasonably small. But suppose the resource is at least a quarter the size of available memory. Then loading it will take up more space, leaving less memory for other tasks.

A better way to deal with large resources is to load the data piecemeal, again on demand. Use the library routines mmap() and munmap() to copy only the needed portions into memory. Alternatively, you can prepare the resource as a database. Then use Core Data to access each resource portion as a record.

3. Avoid unbound problem sets.

Unbound sets are sets with no discernible limits. They could be data structures that kept growing with every use. They could be routines that use repetition to perform their tasks.

Listing Four was a good example of an unbound set as a data structure. There, the data supplied by the NSString argument aTxt dictates the size of the NSArray object tWrd. If the string data consists of hundreds of words, then the array may get hundreds of entries. If the data has just a dozen or so words, the same array will have less entries.

Thus, there is no easy way to determine the final array size by just examining the input string. Perhaps a better solution is to rewrite the routine so it extracts each word in situ.

Listing Five contains an example of an unbound set in the form of a routine. This one gives the root value nearest to the argument aX. It uses a basic Newton-Raphson algorithm (lines 7-9) to compute a possible root for a given polynomial. If the root does not converge, the routine calls itself, passing along the computed value as input (lines 12-14).

Since the routine uses recursion, its stack grows with each computation. This becomes a problem when the stack needs more memory than what is available. Perhaps a way to avoid this problem is to rewrite the routine to use iteration, not recursion.

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task.
However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

Video

This month's Dr. Dobb's Journal

This month,
Dr. Dobb's Journal is devoted to mobile programming. We introduce you to Apple's new Swift programming language, discuss the perils of being the third-most-popular mobile platform, revisit SQLite on Android
, and much more!