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]; }); }
Thank you!!!!
ReplyDeleteAwesome . Very nice article
ReplyDeleteThanks, glad you liked it. Feel free to spread on social media, I've added some buttons for that...
DeleteHave 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.
ReplyDeleteNo 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.
DeleteAny link to the iOS7 API changes specific to this issue would be appreciated.