3

I have my own image downloader class, it holds a queue and downloads images one (or a certain amount) at a time, writes them to the cache folder and retrieves them from the cache folder when necessary. I also have a UIImageView subclass to which I can pass a URL, through the image downloader class it will look if the image already exists on the device and show it if it does, or download and show it after it finished.

After an image finishes downloading I do the following. I create a UIImage from the downloaded NSData, save the downloaded NSData to disk and return the UIImage.

// This is executed in a background thread
downloadedImage = [UIImage imageWithData:downloadedData];
BOOL saved = [fileManager createFileAtPath:filePath contents:downloadedData attributes:attributes];
// Send downloadedImage to the main thread and do something with it

To retrieve an existing image I do this.

// This is executed in a background thread
if ([fileManager fileExistsAtPath:filePath])
{
    NSData* imageData = [fileManager contentsAtPath:filePath];
    retrievedImage = [UIImage imageWithData:imageData];
    // Send retrievedImage to the main thread and do something with it
}

As you can see, I always create a UIImage directly from the downloaded NSData, I never create NSData using UIImagePNGRepresentation so the image never gets compressed. When you create a UIImage from compressed NSData, UIImage will decompress it right before rendering on the main thread and thus block the UI. Since I'm now having a UITableView with a ton of small images in it that have to be downloaded or retrieved from disk, this would be unacceptable as it would slow down my scrolling immensely.

Now my problem. The user is also able to select a photo from the camera roll, save it and it also has to appear in my UITableView. But I can't seem to find a way to turn the UIImage from the camera roll into NSData without using UIImagePNGRepresentation. So here's my question.

How can I convert a UIImage into uncompressed NSData so I can convert it back to a UIImage later using imageWithData so that it doesn't have to be decompressed before rendering?

or

Is there any way I can do the decompression before sending the UIImage to the main thread and cache it so it only has to be decompressed once?

Thanks in advance.

David
  • 1,152
  • 8
  • 13

3 Answers3

4

How can I convert a UIImage into uncompressed NSData so I can convert it back to a UIImage later using imageWithData so that it doesn't have to be decompressed before rendering?

What you're really asking here, I take it, is how to store the UIImage on disk in such a way that you can later read the UIImage from disk as fast as possible. You don't really care whether it is stored as NSData; you just want to be able to read it quickly. I suggest you use the ImageIO framework. Save by way of an image destination and fetch later by way of an image source.

http://developer.apple.com/library/ios/#documentation/GraphicsImaging/Conceptual/ImageIOGuide/ikpg_dest/ikpg_dest.html

Is there any way I can do the decompression before sending the UIImage to the main thread and cache it so it only has to be decompressed once?

Yes, good question. That was going to be my second suggestion: use threading. This is what people have to do with tables all the time. When the table asks for the image, you either have the image already or you don't. If you don't, you supply a filler image and, in the background, fetch the real image. When the real image is ready, you have arranged to get a notification. Back on the main thread, you tell the table view to ask for the data for that row again; this time you've got the image and you supply it. The user will thus see a slight delay before the image appears. I'm sure you've seen lots of apps that behave this way (New York Times is a good example).

I have one further suggestion, and it may be the best of all. You speak of it taking time to decompress the image from disk. But this should take no time at all if the image is small. But the image should be small, because it's going to go into a small place - a table cell. In other words, you should shrink the images beforehand, when you first receive them, so that you are ready with the small version of each image when asked. It is a huge waste of time and memory to supply a large image that is to go into a small space.

ADDED LATER: Of course you do understand that a lot of this worry would be unnecessary if you weren't saving the images to disk. I'm not at all clear on why you need to do that. I hope you have a good reason for it; but it's a heck of a lot faster, obviously, if you just hold the images ready in memory.

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • Hi, thanks for your reply. I'm already using a background thread to load the images. The images are the size that they are displayed in the tableView, so those are as small as they can get. And when they're loaded from disk once I keep them in memory for as long as I can. But all of that still wasn't enough, when scrolling through my tableView the decompression took about 1/3 of the time. I used your suggestion to use ImageIO, in CGImageSourceCreateImageAtIndex I set kCGImageSourceShouldCache to YES. Now it keeps the uncompressed version in memory and it renders way faster. Thanks. – David Mar 29 '13 at 16:54
  • Very nice! I'm still a bit concerned, though; I hope you'll check with Instruments and make sure all is well when the user scrolls the table view on an actual device. – matt Mar 29 '13 at 18:03
  • I did use the time profiler, that's actually how I found out that it was the decompressing that caused such huge delays. copyImageBlockSetPNG and png_read_now were the functions taking up 1/3 of the time. I'm testing on iPhone 3GS and iPod Touch 4G, the two slowest devices our app supports. Even if I turn off caching and load the images from disk every time they become visible, I can only very slightly see the empty imageView before the image is displayed, but the UI doesn't hang at all. And with caching turned on it's completely unnoticeable. It worked out great so thanks again. – David Mar 29 '13 at 21:14
  • Excellent, thanks for that report. An interesting problem, and I'm glad that the ImageIO framework helped; it's really a nice addition to iOS, and is too little-known. – matt Mar 29 '13 at 22:21
2

I found solution:

CGImageRef downloadedImageRef = downloadedImage.CGImage;
CGDataProviderRef provider = CGImageGetDataProvider(downloadedImageRef);
NSData *data = CFBridgingRelease(CGDataProviderCopyData(provider));
// Then you can save the data
kelin
  • 11,323
  • 6
  • 67
  • 104
0

IF you download the data and save it to disk, then the data is compressed in either PNG, JPEG, or GIF format. You are not going to be downloading uncompressed image data. So, the root of your question about doing the decompression first needs to be addressed before you save the file to disk. Decompressing before you save will make the file a lot bigger, but it means that decompression is not needed before the data is read back into a CGImageRef or UIImage. It is the loading and then decompressing a bunch of images that is slowing down your CPU and making scrolling slow. But, it is not a solution to simply hold everything in memory already decompressed, because that will use up all your app memory and crash your phone before long. You might be able to get away with it for some small number of images, but this is a basic design flaw that you need to address when first writing your code. If you like, you can have a look at my blog post on this topic video-and-memory-usage-on-ios-devices, the post deals with video, but you have the exact same issue when dealing with lots of different images. I would suggest that you write your small images to disk in an uncompressed format like TIFF or BMP, that way reading them back in is easy as long as ImageIO supports that specific format.

MoDJ
  • 4,309
  • 2
  • 30
  • 65