iOS Animation and Tuning for Efficiency

Building a great app is not all about looks or functionality, it’s also about how well it performs. Although hardware specifications of mobile devices are improving at a rapid pace, apps that perform poorly, stutter at every screen transition or scrolls like a slideshow can ruin the experience of its user and become a cause of frustration. In this article we will see how to measure performance of an iOS app and tune it for efficiency. For the purpose of this article, we will build a simple app with a long list of images and texts.
toptal-blog-image-1450440672735-a4b4710b3b9bf3959e14f09c061b93b0
For the purposes of testing performance, I would recommend the use of real devices. If you’re serious about building apps, and optimizing them for smooth iOS animation, simulators simply don’t cut it. Simulations can sometimes be out of step with reality. For example, the simulator may be running on your Mac which probably means the CPU (Central Processing Unit) is far more powerful than the CPU on your iPhone. Conversely, the GPU (Graphics Processing Unit) is so different between your device and your Mac that your Mac actually emulates the device’s GPU. As a result, CPU-bound operations tend to be faster on your simulator while GPU-bound operations tend to be slower.

Animating at 60 FPS

One key aspect of perceived performance is making sure your animations run at 60 FPS (frames per second), which is the refresh rate of your screen. There are some timer based animations, which we won’t discuss here. Generally speaking, if you’re running at anything greater than 50 FPS your app will look smooth and performant. If your animations are stuck between 20 and 40 FPS there will be a noticeable stutter and the user will detect a “roughness” in transitions. Anything below 20 FPS will severely affect the usability of your app.

Before we start, it’s probably worth discussing the difference between CPU bound and GPU bound operations. The GPU is a specialized chip that is optimized for drawing graphics. While the CPU can too, it’s far slower. This is why we want to offload as much of our graphics rendering, the process of generating an image from a 2D or 3D model, to the GPU. But we need to be careful, as when the GPU runs out of processing power, graphics related performance will degrade even if the CPU is relatively free.

Core Animation is a powerful framework that handles animation both inside your app, and outside it. It breaks down the process into 6 key steps:

        Layout: Where you arrange your layers and set their properties, such as color and their relative position
        Display: This is where the backing images are drawn onto a context. Any routine you wrote in

drawRect:

        or

drawLayer:inContext:

      is accessed here.
      Prepare: At this stage Core Animation, as it is about to send context to the renderer to draw on, performs some necessary tasks such as decompress images.
      Commit: Here Core Animation sends all this data to the render server.
      Deserialization: The previous 4 steps were all within your app, now the animation is being processed outside your app, the packaged layers are deserialized into a tree that the render server can understand. Everything is converted into OpenGL geometry.
      Draw: Renders the shapes (actually triangles).

You might have guessed that processes 1-4 are CPU operations and 5-6 are GPU operations. In reality you only have control over the first 2 steps. The biggest killer of the GPU is semi-transparent layers where the GPU has to fill the same pixel multiple times per frame. Also any offscreen drawing (several layer effects such as shadows, masks, rounded corners, or layer rasterization will force Core Animation to draw offscreen) will also affect performance. Images that are too large to be processed by the GPU will be processed by the much slower CPU instead. While shadows can be easily achieved by setting two properties directly on the layer, they can easily kill performance if you have many objects onscreen with shadows. Sometimes it’s worth considering adding these shadows as images.

Measuring iOS Animation Performance

We will begin with a simple app with 5 PNG images, and a table view. In this app, we will essentially load 5 images, but will repeat it over 10,000 rows. We will add shadows to both the images and to labels next to the images:

-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    CustomTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:[CustomTableViewCell getCustomCellIdentifier] forIndexPath:indexPath];
    
    NSInteger index = (indexPath.row % [self.images count]);
    NSString *imageName = [self.images objectAtIndex:index];
    
    NSString *filePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"png"];
    UIImage *image = [UIImage imageWithContentsOfFile:filePath];
    
    cell.customCellImageView.image = image;
    
    
    cell.customCellImageView.layer.shadowOffset = CGSizeMake(0, 5);
    cell.customCellImageView.layer.shadowOpacity = 0.8f;
    
    cell.customCellMainLabel.text = [NSString stringWithFormat:@"Row %li", (long)(indexPath.row + 1)];
    cell.customCellMainLabel.layer.shadowOffset = CGSizeMake(0, 3);
    cell.customCellMainLabel.layer.shadowOpacity = 0.5f;
    
    return cell;
}

Images are simply recycled while labels are always different. The result is:
toptal-blog-image-1450440888864-8b5d563353f1ec12b1fcdfd844a19910
Upon swiping vertically, you are very likely to notice stutter as the view scrolls. At this point you may be thinking that the fact we are loading images in the main thread is the problem. May be if we moved this to the background thread all our problems would be solved.

Instead of making blind guesses, let’s try it out and measure the performance. It’s time for Instruments.

To use Instruments, you need to change from “Run” to “Profile”. And you should also be connected to your real device, not all instruments are available on the simulator (another reason why you shouldn’t be optimizing for performance on the simulator!). We will be primarily using “GPU Driver”, “Core Animation” and “Time Profiler” templates. A little known fact is that instead of stopping and running on a different instrument, you can drag and drop multiple instruments and run several at the same time.

Now that we have our instruments set-up, let’s measure. First let’s see if we really have a problem with our FPS.
toptal-blog-image-1450440951441-b43a8b5b9fc56c481044561c550a9ad3
Yikes, I think we’re getting 18 FPS here. Is loading images from the bundle on the main thread really that expensive and costly? Notice our renderer utilization is almost maxed out. So is our tiler utilization. Both are above 95%. And that has nothing to do with loading an image from the bundle on the main thread so let’s not look for solutions here.

iOS Animation and Tuning for Efficiency

facebooktwittergoogle_plusredditpinterestlinkedinmail