Wednesday, August 29, 2012

On iOS UIImage decompression nightmare

Yesterday I tried to polish a Gallery class that I work on which I'll be posting to Github soon. This class is intended to be used as the ultimate gallery view ;) It's really easy to understand and thanks to blocks it is really easy to use. But anyways I will make a post when it's available on github.

The problem I encountered while using my new gallery class to display images was that even though the pages creation was separated from the data sourcing using dispatch queues the first time an image was loaded, the scrolling would be really bad.



Here is the simplified culprit code:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%u.png", pageIndex]];
        
  dispatch_async(dispatch_get_main_queue(), ^{ 
    [((UIImageView*)page)setImage:image];
  });
}


I did a little bit of research on any known issue concerning -[UIImageView setImage:] being slow. And found some interesting posts like this one where the author benchmarks the UIImage decompression for different image formats (png and jpg) on different iOS hardwares.

The problem we have when we use the -[UIImageView setImage:] is that if the image was never decompressed, the framework (UIKit) will decompress it in a lazy manner, that is, when the image data really needs to be accessed and in our case it would be done in the block dispatched to the main queue.

To improve our image sourcing block we would need to force the decompression of the image in the background queue and leave the main queue block as it is.

Also, as the article pointed out, it is best for full screen images to use jpg format since the decompression of the image data is much quicker on all iOS platforms.

Ok, so changing the images format is just a bit of mouse work... done.
Now, how do we force the image to decompress it's data? Well there are many examples around on the web and I made my own by tearing parts of them.

NS_INLINE void forceImageDecompression(UIImage *image) {
  CGImageRef imageRef = [image CGImage];
  CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  CGContextRef context = CGBitmapContextCreate(NULL, CGImageGetWidth(imageRef), CGImageGetHeight(imageRef), 8, CGImageGetWidth(imageRef) * 4, colorSpace,kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
  CGColorSpaceRelease(colorSpace);
  if (!context) { NSLog(@"Could not create context for image decompression"); return; }
  CGContextDrawImage(context, (CGRect){{0.0f, 0.0f}, {CGImageGetWidth(imageRef), CGImageGetHeight(imageRef)}}, imageRef);
  CFRelease(context);
}

Basically, this inlined function allows you to force the image data decompression by drawing the image in a bitmap context. This can be done in a background thread since iOS 4.0 IIRC.

So now the overall sequence becomes:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
  UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%u.jpg", pageIndex]];
  forceImageDecompression(image);
        
  dispatch_async(dispatch_get_main_queue(), ^{ 
    [((UIImageView*)page)setImage:image];
  });
}

5 comments:

  1. Awesome . Very nice article

    ReplyDelete
    Replies
    1. Thanks, glad you liked it. Feel free to spread on social media, I've added some buttons for that...

      Delete
  2. Have you tried this on iOS7? During iOS6 days I remember that this trick was very helpful. Right now I can't see an obvious difference.

    ReplyDelete
    Replies
    1. No I haven't measured the difference with iOS7. But most of my apps are back compatible all the way down to iOS 5 so it still makes sense for me to use this.
      Any link to the iOS7 API changes specific to this issue would be appreciated.

      Delete