Background Rendering Bottleneck

In iOS (and, actually, in almost any user interface) it’s best to do as much work in background threads as possible, so your main thread is free to keep the UI smooth. To this end, I’ve been loading images from PDF files in the background using this sort of pattern (in pseudocode – this isn’t a working example):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)loadImageInBackground
{
    // Use Grand Central Dispatch to queue up loading an image on a thread
    dispatch_async(renderQueue, ^{
        // Open the PDF, render the page into an UIImage
        UIImage *image = [self loadImage];

        // Transition back to main thread to update the UI
        dispatch_async(async_get_main_queue(), ^{
            // Assign the UIImage to a UIImageView
            imageView.source = image;
        });
    });
}

This works pretty well, and made for a very responsive UI, but when the user was rapidly scrolling through a document, the app was running out of memory. After some time in Instruments looking at where the memory was going, I found out that while scrolling, the main thread could wind up being too busy to dispatch the second part of the rendering, the part that passes the bitmap from the secondary thread back to the main thread.

The line above that says “Transition back to main thread” is where the problem starts. At this point, we’ve loaded an image, and we’re waiting for a chance to update the UI – but that chance isn’t coming soon enough, and we end up with an image taking up memory and nothing to do with it but wait. Meanwhile, the main thread, where the user is scrolling madly, is queueing up more background image processing. Eventually, it’s too much and the app dies.

The fix? Wait for the render queue to clear out before queueing up any more work. Here’s what that looks like:

1
2
// Wait for any current background render to complete
dispatch_sync(renderQueue, ^{});

The dispatch_sync call ensures that the renderQueue is empty before we add anything else to it, because it synchronously queues up a job at the end of the queue. Once the job runs, the queue is empty and we can put more work on it.

Leave a Reply