iOS Core Animation: Advanced Techniques (2014)

Part III. The Performance of a Lifetime

Chapter 12. Tuning for Speed

Code should run as fast as necessary, but no faster.

Richard E. Pattis

In Parts I and II, we learned about the awesome drawing and animation features that Core Animation has to offer. Core Animation is powerful and fast, but it’s also easy to use inefficiently if you’re not clear what’s going on behind the scenes. There is an art to getting the best performance out of it. In this chapter, we explore some of the reasons why your animations might run slowly and how you can diagnose and fix problems.

CPU Versus GPU

There are two types of processor involved in drawing and animation: The CPU (central processing unit) and GPU (graphics processing unit). On modern iOS devices, these are both programmable chips that can run (more or less) arbitrary software, but for historical reasons, we tend to say that the part of the work that is performed by the CPU is done “in software” and the part handled by the GPU is done “in hardware.”

Generally speaking, we can do anything in software (using the CPU), but for graphics processing, it’s usually much faster to use hardware because the GPU is optimized for the sort of highly parallel floating point math used in graphics. For this reason, we ideally want to offload as much of our screen rendering to hardware as possible. The problem is that the GPU does not have unlimited processing power, and once it’s already being used to full capacity, performance will start to degrade (even if the CPU is not being fully utilized).

Most animation performance optimization is about intelligently utilizing the GPU and CPU so that neither is overstretched. To do that, we first have to understand how Core Animation divides the work between these processors.

The Stages of an Animation

Core Animation lies at the heart of iOS: It is used not just within applications but between them. A single animation may actually display content from multiple apps simultaneously, such as when you use gestures on an iPad to switch between apps, causing views from both to briefly appear onscreen at once. It wouldn’t make sense for this animation to be performed inside the code of any particular app because then it wouldn’t be possible for iOS to implement these kinds of effects. (Apps are sandboxed and cannot access each others’ views.)

Animating and compositing layers onscreen is actually handled by a separate process, outside of your application. This process is known as the render server. On iOS 5 and earlier, this is the SpringBoard process (which also runs the iOS home screen). On iOS 6 and later, this is handled by a new process called BackBoard.

When you perform an animation, the work breaks down into four discrete phases:

Image Layout—This is the phase where you prepare your view/layer hierarchy and set up the properties of the layers (frame, background color, border, and so on).

Image Display—This is where the backing images of layers are drawn. That drawing may involve calling routines that you have written in your -drawRect: or -drawLayer:inContext: methods.

Image Prepare—This is the phase where Core Animation gets ready to send the animation data to the render server. This is also the point at which Core Animation will perform other duties such as decompressing images that will be displayed during the animation (more on this later).

Image Commit—This is the final phase, where Core animation packages up the layers and animation properties and sends them over IPC (Inter-Process Communication) to the render server for display.

But those are just the phases that take place inside your application—there is still more work to be done before the animation appears onscreen. Once the packaged layers and animations arrive in the render server process, they are deserialized to form another layer tree called the render tree(mentioned in Chapter 1, “The Layer Tree”). Using this tree, the render server does the following for each frame of the animation:

Image Calculates the intermediate values for all the layer properties and sets up the OpenGL geometry (textured triangles) to perform the rendering

Image Renders the visible triangles to the screen

So that’s six phases in total; the last two are repeated over and over for the duration of the animation. The first five of these phases are handled in software (by the CPU), only the last is handled by the GPU. Furthermore, you only really have direct control of the first two phases: layout and display. The Core Animation framework handles the rest internally and you have no control over it.

That’s not really a problem, because in the layout and display phases, you get to decide what work will be done upfront on the CPU and what will get passed down to the GPU. So how do you make that decision?

GPU-Bound Operations

The GPU is optimized for a specific task: It takes images and geometry (triangles), performs transforms, applies texturing and blending, and then puts them on the screen. The programmable GPUs in modern iOS devices allow for a lot of flexibility in how those operations are performed, but Core Animation does not expose a direct interface to any of that. Unless you are prepared to bypass Core Animation and start writing your own OpenGL shaders, you are basically stuck with a fixed set of things that are hardware accelerated, and everything else must be done in software on the CPU.

Broadly speaking, most properties of CALayer are drawn using the GPU. If you set the layer background or border colors, for example, those can be drawn really efficiently using colored triangles. If you assign an image to the contents property—even if you scale and crop it—it gets drawn using textured triangles, without the need for any software drawing.

A few things can slow down this (predominantly GPU-based) layer drawing, however:

Image Too much geometry—This is where too many triangles need to be transformed and rasterized (turned into pixels) for the processor to cope. The graphics chip on modern iOS devices can handle millions of triangles, so geometry is actually unlikely to be a GPU bottleneck when it comes to Core Animation. But because of the way that layers must be preprocessed and sent to the render server via IPC before display (layers are fairly heavy objects, composed of several subobjects), too many layers will cause a CPU bottleneck. This limits the practical number of layers that you can display at once (see the section “CPU-Bound Operations,” later in this chapter).

Image Too much overdraw—This is predominantly caused by overlapping semitransparent layers. GPU’s have a finite fill-rate (the rate at which they can fill pixels with color), so overdraw (filling the same pixel multiple times per frame) is something to be avoided. That said, modern iOS device GPUs are fairly good at coping with overdraw; even an iPhone 3GS can handle an overdraw ratio of 2.5 over the whole screen without dropping below 60 FPS (that means that you can draw one-and-a-half whole screenfuls of redundant information without impacting performance), and newer devices can handle more.

Image Offscreen drawing—This occurs when a particular effect cannot be achieved by drawing directly onto the screen, but must instead be first drawn into an offscreen image context. Offscreen drawing is a generic term that may apply to either CPU or GPU-based drawing, but either way it involves allocating additional memory for the offscreen image and switching between drawing contexts, both of which will slow down the GPU performance. The use of certain layer effects, such as rounded corners, layer masks, drop shadows, or layer rasterization will force Core Animation to prerender the layer offscreen. That doesn’t mean that you need to avoid these effects altogether, just that you need to be aware that they can have a performance impact.

Image Too-large images—If you attempt to draw an image that is larger than the maximum texture size supported by the GPU (usually 2048×2048 or 4096×4096, depending on the device), the CPU must be used to preprocess the image each time it is displayed, slowing the performance right down.

CPU-Bound Operations

Most of the CPU work in Core Animation happens upfront before the animation begins. This is good because it means that it generally doesn’t impact the frame rate, but it’s bad because it can delay the start of an animation, making your interface appear unresponsive.

The following CPU operations can all slow down the start of your animation:

Image Layout calculations—If your view hierarchy is complex, it might take a while to calculate all the layer frames when a view is presented or modified. This is especially true if you are using iOS 6’s new autolayout mechanism, which is more CPU-intensive than the old autoresizing logic.

Image Lazy view loading—iOS only loads a view controller’s view when it is first displayed onscreen. This is good for memory usage and application startup time, but can be bad for responsiveness if a button tap suddenly results in a lot of work having to be done before anything can appear onscreen. If the controller gets its data from a database, or the view is loaded from a nib file, or contains images, this can also lead to IO work, which can be orders of magnitude slower than ordinary CPU operations (see the “IO-Bound Operations” section, later in this chapter).

Image Core Graphics drawing—If you implement the -drawRect: method of your view, or the -drawLayer:inContext: method of your CALayerDelegate, you introduce a significant performance overhead even before you’ve actually drawn anything. To support arbitrary drawing into a layer’s contents, Core Animation must create a backing image in memory equal in size to the view dimensions. Then, once the drawing is finished, it must transmit this image data via IPC to the render server. On top of that overhead, Core Graphics drawing is very slow anyway, and not something that you want to be doing in a performance critical situation.

Image Image decompression—Compressed image files such as PNGs or JPEGs are much smaller than the equivalent uncompressed bitmaps. But before an image can be drawn onscreen, it must be expanded to its full, uncompressed size (usually this equates to image width × height × four bytes). To conserve memory, iOS often defers decompression of an image until it is drawn (more on this in Chapter 14, “Image IO”). Depending on how you load an image, the first time you assign it to a layer’s contents (either directly or indirectly by using a UIImageView) or try to draw it into a Core Graphics context, it may need to be decompressed, which can take a considerable time for a large image.

After the layers have been successfully packaged up and sent to the render server, the CPU still has work to do: To display the layers on screen, Core Animation must loop through every visible layer in the render tree and convert it into a pair of textured triangles for consumption by OpenGL. The CPU has to do this conversion work because the GPU knows nothing about the structure of Core Animation layers. The amount of CPU work involved here scales proportionally with the number of layers, so too many layers in your hierarchy will indirectly cause additional CPU slowdown every frame, even though this work takes place outside of your application.

IO-Bound Operations

Something we haven’t mentioned yet is IO-bound work. IO (Input/Output) in this context refers to accessing hardware such as flash storage or the network interface. Certain animations may require data to be loaded from flash (or even from a remote URL). A typical example would be a transition between two view controllers, which may lazily load a nib file and its contents, or a carousel of images, which may be too large to store in memory and so need to be loaded dynamically as the carousel scrolls.

IO is much slower than normal memory access, so if your animation is IO-bound that can be a big problem. Generally, this must be addressed using clever-but-awkward-to-get-right techniques such as threading, caching, and speculative loading (loading things in advance that you don’t need right now but predict you will need in the future). These techniques are discussed in depth in Chapter 14.

Measure, Don’t Guess

So, now that you know where the slowdown points might be in your animation, how do you go about fixing them? Well, first of all, you don’t. There are many tricks to optimize an animation, but if applied blindly, these tricks often have as much chance of causing performance problems as correcting them.

It’s important to always measure rather than guess why your animation is running slowly. There is a big difference between using your knowledge of performance to write code in an efficient way, and engaging in premature optimization. The former is good practice, but the latter is a waste of time and may actually be counterproductive.

So, how can you measure what’s slowing down your app? Well, the first step is to make sure you that are testing under real-world conditions.

Test Reality, Not a Simulation

When you start doing any sort of performance tuning, test on a real iOS device, not on the simulator. The simulator is a brilliant tool that speeds up the development process, but it does not provide an accurate reflection of a real device’s performance.

The simulator is running on your Mac, and the CPU in your Mac is most likely much faster than the one in your iOS device. Conversely, the GPU in your Mac is so different from the one in your iOS device that the simulator has to emulate the device’s GPU entirely in software (on the CPU), which means that GPU-bound operations usually run slower on the simulator than they do on an iOS device, especially if you are writing bespoke OpenGL code using CAEAGLLayer.

This means that testing on the simulator gives a highly distorted view of performance. If an animation runs smoothly on the simulator, it may be terrible on a device. If it runs badly on the simulator, it may be fine on a device. You just can’t be sure.

Another important thing when performance testing is to use the Release configuration, not Debug mode. When building for release, the compiler includes a number of optimizations that improve performance, such as stripping debugging symbols or removing and reorganizing code. You may also have implemented optimizations of your own, such as disabling NSLog statements when in release mode. You only care about release performance, so that’s what you should test.

Finally, it is good practice to test on the slowest device that you support: If your base target is iOS 6, that probably means an iPhone 3GS or iPad 2. If at all possible, test on multiple devices and iOS versions, though, because Apple makes changes to the internals of iOS and iOS devices in major releases and these may actually reduce performance in some cases. For example, the iPad 3 is noticeably slower than the iPad 2 for a lot of animation and rendering tasks because of the challenge of rendering four times as many pixels (to support the Retina display).

Maintaining a Consistent Frame Rate

For smooth animation, you really want to be running at 60 FPS (frames per second), in sync with the refresh rate of the screen. With NSTimer or CADisplayLink-based animations you can get away with reducing your frame rate to 30 FPS, and you will still get a reasonable result, but there is no way to set the frame rate for Core Animation itself. If you don’t hit 60 FPS consistently, there will be randomly skipped frames, which will look and feel bad for the user.

You can tell immediately if the frame rate is skipping badly just by using the app, but it’s difficult to see the extent of the problem just by looking at the screen, and hard to tell whether your changes are making it slightly better or slightly worse. What you really want is an accurate numeric display of the frame rate.

You can put a frame rate counter in your app by using CADisplayLink to measure the frame period (as we did in Chapter 11, “Timer-Based Animation”) and then display the equivalent FPS value onscreen, but an in-app FPS display cannot ever be completely accurate when measuring Core Animation performance because it only measures the frame rate inside the app. We know that a lot of the animation happens outside the app (in the render server process), so while an in-app FPS counter can be a useful tool for certain types of performance issues, once you’ve identified a problem area, you will need to get some more accurate and detailed metrics to narrow down the cause. Apple provides the powerful Instruments toolset to help us with this.

Instruments

Instruments is an underutilized feature of the Xcode suite. Many iOS developers have either never used Instruments, or have only made use of the Leaks tool to check for retain cycles. But there are many other Instruments tools, including ones that are specifically designed to help us to tune our animation performance.

You can invoke Instruments by selecting the Profile option in the Product menu. (Before you do this, remember to target an actual iOS device, not the simulator.) This will present the screen shown in Figure 12.1. (If you don’t see all of these options, you might be testing on the simulator by mistake.)

Image

Figure 12.1 The Instruments tool selection window

As mentioned earlier, you should only ever profile the Release configuration of your app. Fortunately, the profiling option is set up to use the Release configuration by default, so you don’t need to adjust your build scheme when you are profiling.

The tools we are primarily interested in are as follows:

Image Time Profiler—Used to measure CPU usage, broken down by method/function.

Image Core Animation—Used to debug all kinds of Core Animation performance issues.

Image OpenGL ES Driver—Used to debug GPU performance issues. This tool is more useful if you are writing your own OpenGL code, but occasionally still handy for Core Animation work.

A nice feature of Instruments is that it lets us create our own groupings of tools. Regardless of which tool you select initially, if you open the Library window in Instruments, you can drag additional tools into the left sidebar. We’ll create a group of the three tools that we are interested in so that we can use them all in parallel (see Figure 12.2).

Image

Figure 12.2 Adding additional tools to the Instruments sidebar

Time Profiler

The Time Profiler tool is used to monitor CPU usage. It gives us a breakdown of which methods in our application are consuming most CPU time. Using up a lot of CPU isn’t necessarily a problem—you would expect your animation routines to be very CPU intensive because animation tends to be one of the most demanding tasks on an iOS device. But if you are having performance problems, looking at the CPU time is a good way to determine if your performance is CPU-bound, and if so which methods need to be optimized (see Figure 12.3).

Image

Figure 12.3 The Time Profiler tool

Time Profiler has some options to help narrow down the display to show only methods we care about. These can be toggled using checkboxes in the left sidebar. Of these, the most useful for our purposes are as follows:

Image Separate by Thread—This groups methods by the thread in which they are executing. If our code is split between multiple threads, this will help to identify which ones are causing the problem.

Image Hide System Libraries—This hides all methods and functions that are part of Apple’s frameworks. This helps us to identify which of our own methods contain bottlenecks. Since we can’t optimize framework methods, it is often useful to toggle this method to help narrow down the problem to something we can actually fix.

Image Show Obj-C Only—This allows us to hide everything except Objective-C method calls. Most of the internal Core Animation code uses C or C++ functions, so this is a good way to eliminate noise and help us focus on the methods that we are calling explicitly from our code.

Core Animation

The Core Animation tool is used to monitor Core Animation performance. It gives a breakdown of FPS sampled periodically, taking into account the parts of the animation that happen outside of our application (see Figure 12.4).

Image

Figure 12.4 The Core Animation tool, with visual debug options

The Core Animation tool also provides a number of checkbox options to aid in debugging rendering bottlenecks:

Image Color Blended Layers—This option highlights any areas of the screen where blending is happening (that is, where multiple semitransparent layers overlap one another), shaded from green to red based on severity. Blending can be bad for GPU performance because it leads to overdraw, and is a common cause of poor scrolling or animation frame rate.

Image Color Hits Green and Misses Red—When using the shouldRasterize property, expensive layer drawing is cached and rendered as a single flattened image. This option highlights rasterized layers in red when the cache has to be regenerated. If the cache is regenerated frequently this is an indication that the rasterization may have a negative performance impact. (See Chapter 15, “Layer Performance,” for more detail about the implications of using the shouldRasterize property.)

Image Color Copied Images—Sometimes the way that backing images are created means that Core Animation is forced to make a copy of the image and send it to the render server instead of just sending a pointer to the original. This option colors such images blue in the interface. Copying images is very expensive in terms of memory and CPU usage and should be avoided if possible.

Image Color Immediately—Normally the Core Animation Instrument only updates the layer debug colors once every 10 milliseconds. For some effects, this may be too slow to detect an issue. This option sets it to update every frame (which may impact rendering performance and throw off the accuracy of frame rate measurements, so it should not be left on all the time).

Image Color Misaligned Images—This highlights images that have been scaled or stretched for display, or which have not been correctly aligned to a pixel boundary (that is, they have nonintegral coordinates). Most of these will be false positives because it is common to deliberately scale images in an app, but if you are accidentally displaying a large image as a thumbnail, or making your graphics blurry by not aligning them correctly, this will help you to spot it.

Image Color Offscreen-Rendered Yellow—This highlights any layer that requires offscreen rendering in yellow. Such layers may be candidates for using optimizations such as shadowPath or shouldRasterize.

Image Color OpenGL Fast Path Blue—This highlights anywhere that you are drawing directly to the screen using OpenGL. If you are only using UIKit or Core Animation APIs, then this won’t have any effect. If you are using a GLKView or CAEAGLLayer, then if it doesn’t show in blue it may mean that you are doing more work than necessary by forcing the GPU to render to a texture instead of drawing directly to the screen.

Image Flash Updated Regions—This will briefly highlight in yellow any content that is redrawn (that is, any layer that is doing software drawing using Core Graphics). Such drawing is slow. If it’s happening frequently, it may indicate a bug or an opportunity to improve performance by adding caching or using an alternative approach to generate your graphics.

Several of these layer coloring options are also available in the iOS Simulator Debug menu (see Figure 12.5). We stated earlier that it’s a bad idea to test performance in the simulator, but if you have identified that the cause of your performance issues can be highlighted using these debugging options, using the iOS Simulator to verify that you’ve resolved the problem may provide a quicker fix/test cycle than tethering to a device.

Image

Figure 12.5 Core Animation visual debugging options in the iOS Simulator

OpenGL ES Driver

The OpenGL ES Driver tool can help you measure the GPU utilization, which is a good indicator as to whether your animation performance might be GPU-bound. It also provides the same FPS display as the Core Animation tool (see Figure 12.6).

Image

Figure 12.6 The OpenGL ES Driver tool

In the right sidebar are a number of useful metrics. Of these, the most relevant for Core Animation performance are as follows:

Image Renderer Utilization—If this value is higher than ~50%, it suggests that your animation may be fill-rate limited, possibly due to offscreen rendering or overdraw caused by excessive blending.

Image Tiler Utilization—If this value is higher than ~50%, it suggests that your animation may be geometry limited, meaning that there may be too many layers onscreen.

A Worked Example

Now that we are familiar with the animation performance tools in Instruments, let’s use them to diagnose and solve a real-world performance problem.

We’ll create a simple app that displays a mock contacts list in a table view using fake friend names and avatars. Note that even though the avatar images are stored in our app bundle, to make the app behave more realistically we’re loading the avatar images individually in real time rather than preloading them using the –imageNamed: method. We’ve also added some layer shadows to make our list a bit more visually appealing. Listing 12.1 shows the initial, naive implementation.

Listing 12.1 A Simple Contacts List Application with Mock Data


#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>

@interface ViewController () <UITableViewDataSource>

@property (nonatomic, strong) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;

@end

@implementation ViewController

- (NSString *)randomName
{
    NSArray *first = @[@"Alice", @"Bob", @"Bill", @"Charles",
                       @"Dan", @"Dave", @"Ethan", @"Frank"];
    NSArray *last = @[@"Appleseed", @"Bandicoot", @"Caravan",
                      @"Dabble", @"Ernest", @"Fortune"];
    NSUInteger index1 = (rand()/(double)INT_MAX) * [first count];
    NSUInteger index2 = (rand()/(double)INT_MAX) * [last count];
    return [NSString stringWithFormat:@"%@ %@", first[index1], last[index2]];
}

- (NSString *)randomAvatar
{
    NSArray *images = @[@"Snowman", @"Igloo", @"Cone",
                        @"Spaceship", @"Anchor", @"Key"];
    NSUInteger index = (rand()/(double)INT_MAX) * [images count];
    return images[index];
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    //set up data
    NSMutableArray *array = [NSMutableArray array];
    for (int i = 0; i < 1000; i++)
    {
        //add name
        [array addObject:@{@"name": [self randomName],
                           @"image": [self randomAvatar]}];
    }
    self.items = array;

    //register cell class
    [self.tableView registerClass:[UITableViewCell class]
                                   forCellReuseIdentifier:@"Cell"];
}

- (NSInteger)tableView:(UITableView *)tableView
 numberOfRowsInSection:(NSInteger)section
{
    return [self.items count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //dequeue cell
    UITableViewCell *cell =
        [self.tableView dequeueReusableCellWithIdentifier:@"Cell"
                                             forIndexPath:indexPath];

    //load image
    NSDictionary *item = self.items[indexPath.row];
    NSString *filePath = [[NSBundle mainBundle] pathForResource:item[@"image"]
                                                         ofType:@"png"];

    //set image and text
    cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];
    cell.textLabel.text = item[@"name"];

    //set image shadow
    cell.imageView.layer.shadowOffset = CGSizeMake(0, 5);
    cell.imageView.layer.shadowOpacity = 0.75;
    cell.clipsToBounds = YES;

    //set text shadow
    cell.textLabel.backgroundColor = [UIColor clearColor];
    cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
    cell.textLabel.layer.shadowOpacity = 0.5;


return cell;
}

@end


When we scroll quickly, we get significant stuttering (see the FPS counter in Figure 12.7).

Image

Figure 12.7 The frame rate falls to 15 FPS when scrolling in our app.

Instinctively, we might assume that the bottleneck is image loading. We’re loading images from the flash drive in real time and not caching them, so that’s probably why it’s slow, right? We can fix that with some awesome code that will speculatively load our images asynchronously using GCD and then cache them using....

Stop. Wait.

Before we start writing code, let’s test our hypothesis. We’ll profile the app using our three Instruments tools to identify the problem. We suspect that the issue might be related to image loading, so let’s start with the Time Profiler tool (see Figure 12.8).

Image

Figure 12.8 The timing profile for our contacts list app

The total percentage of CPU time spent in the -tableView:cellForRowAtIndexPath: method (which is where we load the avatar images) is only ~28%. That isn’t really all that high. That would suggest that CPU/IO is not the limiting factor here.

Let’s see if it’s a GPU issue instead: We’ll check the GPU utilization in the OpenGL ES Driver tool (see Figure 12.9).

Image

Figure 12.9 GPU utilization displayed in the OpenGL ES Driver tool

The tiler and renderer utilization values are at 51% and 63%, respectively. It looks like the GPU is having to work pretty hard to render our contacts list.

Why is our GPU usage so high? Let’s inspect the screen using the Core Animation tool debugging options. Enable the Color Blended Layers option first (see Figure 12.10).

Image

Figure 12.10 Debugging our app with the Color Blended Layers option

All that red on the screen indicates a high level of blending on the text labels, which is not surprising because we had to make the background transparent to apply our shadow effect. That explains why the renderer utilization is high.

What about offscreen drawing? Enable the Core Animation tool’s Color Offscreen-Rendered Yellow option (see Figure 12.11).

Image

Figure 12.11 The Color Offscreen–Rendered Yellow option

We see now that all our table cell content is being rendered offscreen. That must be because of the shadows we applied to the image and label views. Let’s disable the shadows by commenting them out in our code and see whether that helps with our performance problem (see Figure 12.12).

Image

Figure 12.12 Our app running at close-to 60 FPS with shadows disabled

Problem solved. Without the shadows, we get a smooth scroll. Our contacts list doesn’t look as interesting as before, though. How can we keep our layer shadows and still have good performance?

Well, the text and avatar for each row don’t need to change every frame, so it seems like the UITableViewCell layer is an excellent candidate for caching. We can cache our layer contents using the shouldRasterize property. This will render the layer once offscreen and then keep the result until it needs to be updated. Let’s try that (see Listing 12.2).

Listing 12.2 Improving Performance with the shouldRasterize Option


- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //dequeue cell
    UITableViewCell *cell =
        [self.tableView dequeueReusableCellWithIdentifier:@"Cell"
                                             forIndexPath:indexPath];

    ...

    //set text shadow
    cell.textLabel.backgroundColor = [UIColor clearColor];
    cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
    cell.textLabel.layer.shadowOpacity = 0.5;

    //rasterize
    cell.layer.shouldRasterize = YES;
    cell.layer.rasterizationScale = [UIScreen mainScreen].scale;

    return cell;
}


We are still drawing the layer content offscreen, but because we have explicitly enabled rasterization, Core Animation is now caching the result of that drawing, so it has less of a performance impact. We can verify that the caching is working correctly by using the Color Hits Green and Misses Red option in the Core Animation tool (see Figure 12.13).

Image

Figure 12.13 Color Hits Green and Misses Red indicates caching works.

We see that—as expected—most rows are green, only flashing red briefly as they move onto the screen. Consequently, our frame rate is now much smoother.

So, our initial instinct was wrong. The loading of our images turns out not to have been a bottleneck after all, and any effort put into a complex, multithreaded loading and caching implementation would have been wasted. It’s a good thing we verified what the problem was before we tried to fix it!

Summary

In this chapter, you learned about how the Core Animation rendering pipeline works and where the bottlenecks are likely to occur. You also learned how to use Instruments to diagnose and fix performance problems.

In the next three chapters, we look at each of the common app performance pitfalls in detail and learn how to fix them.