Benjohn
Benjohn

Reputation: 13887

Off-screen UIView thread safety

In my app I am rendering lots (200 to 400) UIImages on a background thread and then installing them inside on-screen UIImageView instances by dispatching a UI update block to the main thread.

Some code to show roughly what I'm doing…

dispatch_async( redrawQueue, ^{
  // An array to stuff images in to for the views that have one.
  NSMutableArray *const images = [NSMutableArray arrayWithCount: [activeViews count] value: [NSNull null]];

  for(NSUInteger i=0; i<activeCount; ++i)
  {
    // Rendered content comes from a block in myState.
    UIImage *const image = contentBlock(i);
    if(image)
    {
      images[i] = image;
    }
  }
  else
  {
    return;
  }

  // Update the UI now…
  dispatch_async( dispatch_get_main_queue(), ^{
    [images enumerateObjectsUsingBlock:^(UIImage *image, NSUInteger i, BOOL *stop) {
      UIImageView *const view = [activeViews objectAtIndex:i];
      [view setImage: [NSNull isNotNull: image] ? image : nil];
      layoutBlock(view, i);
    }];
  });
});

This is working well, but I'm still getting dropped frames during rapid scrolling. It seems like this is happening because the work of setting the images in the views is overwhelming the main thread. My evidence for this is that if I take out just the code to actually set the rendered images in the views, scrolling is much smoother.

I'm wondering if an approach to solving this might be to also create the views in a background thread, assign images to them, and place them in to a container view. Then in the main thread, I would simply need to swap the container in to the on-screen scene. The result is a bit like a double buffered graphics context, I guess – update one while the other is displayed.

Can anyone suggest if this is unlikely to be thread safe?

I've done a small test of allocating off screen UIViews on a background thread and nesting them inside each other. It hasn't crashed yet :-) "It hasn't crashed yet" isn't a great "thread safety" guarantee though! It also doesn't say anything about what might happen in a future version of iOS.


An obvious answer to this is "Hey, you fool, why are you using hundreds of little views? Composite them to one a big image and have a single view you swap it in to." Unfortunately I need lots of little views because I need to move the individual little pieces about independently.

Another answer might be "Use sprite kit, dude", and you're probably right, but the little views have dynamic size and content and I'm not sure how optimal sprite kit is when there are lots of sprite updates occurring.

A third approach could be to throttle the UI updates on the main thread to prevent frames getting dropped. Is there a mechanism that does this? Some kind of dispatch queue run by the main thread that only calls stuff while it's got plenty of time left?

Upvotes: 1

Views: 488

Answers (1)

ipmcc
ipmcc

Reputation: 29886

You asked:

A third approach could be to throttle the UI updates on the main thread to prevent frames getting dropped. Is there a mechanism that does this? Some kind of dispatch queue run by the main thread that only calls stuff while it's got plenty of time left?

This is not something that's built in, but it's not that hard to envision how you might do this, but it's also non-trivial. You will probably need to manage your own array of images to deliver (including some means of protecting it from concurrent access), then add a CFRunLoopObserver (probably in the kCFRunLoopBeforeWaiting activity, since that's when the run loop is about to go to sleep) that, every time it's triggered, marks the start time, and then processes items from your array of images until some amount of time has passed (10ms is probably a decent amount of time).

Another thing you might consider would be rendering many of these little images into one CGImage (or some small number of images), and then setting the view's layer's contents to the big image, while setting the bounds such that each instance is clipped to just the portion corresponding to that view. This might reduce the number of GPU texture uploads (and hence overall overhead), since all the CALayers backing the views will have the same image as their contents. This would probably be my first stop.

Upvotes: 2

Related Questions