Let’s take a UITableView
table which is used as a contents page for an illustrated iPad book. Each table cell contains several scaled-down pages of the book. These images are fairly large since it’s a comic book and users will be using the thumbnails to find a specific page.
When the image is large, decoding it takes a lot of time. This is also the case with PNG which at compilation stage is being converted to a format convenient for the device and is recommended by Apple for use in UIKit-based programs. Without special considerations there is no way to achieve smooth scrolling of a table that contains such large images.
At first it might seem that the problem is in reading the file from media, but the profiler confirms it isn’t so. Both -[UIImage imageNamed:]
and -[UIImage imageWithContentsOfFile:]
do not decode images instantly and postpone it for later. Decoding can happen for example when -[UIImageView setImage:]
is called. And the worst thing is that there is no way to call this method outside of the main thread. Before iOS 4 no UIKit methods could be called in non-main threads. In iOS 4 the situation is slightly different, but in relation to -[UIImageView setImage:]
the rules remain the same. It turns out that while operating on the UIKit level the only way to run a resource-consuming image decode operation is to do it in the precious main thread which is busy scrolling the table right when this image is required.
Of course, sometimes it’s possible to decode all the images before serving them. But such a solution is unreliable since there can be lots of images and they all might not fit into memory. And all the same, we would still have to spend the main thread time at one point or another. If we decide to do it at app launch, it will lead to increased app start time which is not something we can afford in the case of iOS.
Thankfully, we have at our disposal not only UIKit with UIImage
but also Core Graphics with CGImage
, and CGImage
functions can be called in background, outside of the main thread. Moreover, this is also where we can decode an image. Here is how it’s done:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
UIImage *anImage = ...
CGImageRef originalImage = (CGImageRef)anImage;
assert(originalImage != NULL);
CFDataRef imageData = CGDataProviderCopyData(CGImageGetDataProvider(originalImage));
CGDataProviderRef imageDataProvider = CGDataProviderCreateWithCFData(imageData);
if (imageData != NULL) {
CFRelease(imageData);
}
CGImageRef image = CGImageCreate(CGImageGetWidth(originalImage),
CGImageGetHeight(originalImage),
CGImageGetBitsPerComponent(originalImage),
CGImageGetBitsPerPixel(originalImage),
CGImageGetBytesPerRow(originalImage),
CGImageGetColorSpace(originalImage),
CGImageGetBitmapInfo(originalImage),
imageDataProvider,
CGImageGetDecode(originalImage),
CGImageGetShouldInterpolate(originalImage),
CGImageGetRenderingIntent(originalImage));
if (imageDataProvider != NULL) {
CGDataProviderRelease(imageDataProvider);
}
CGImageRelease(image);
|
The image received from CGImageCreate()
is already decoded, which is exactly what we need. All that’s left is to wrap this code in NSOperation
to execute it in background and finally send the results to the main thread. Using +[UIImage imageWithCGImage:]
we get UIImage
from the resulting CGImage
and assign it to the required UIImageView
right during scrolling. The animation works flawlessly.
Usage example:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSUInteger index = ...;
NSNumber *indexNumber = [NSNumber numberWithUnsignedInteger:index];
UIImage *thumbnailImage = [self thumbnailForPageAtIndex:index];
CGImageRef imageRef = [thumbnailImage CGImage];
NSDictionary *thumbnailDict = [NSDictionary dictionaryWithObjectsAndKeys:
indexNumber, kThumbnailIndexKey,
imageRef, kThumbnailImageKey,
nil];
NSInvocationOperation *thumbnailOperation
= [[[NSInvocationOperation alloc]
initWithTarget:self
selector:@selector(decodeThumbnail:)
object:thumbnailDict]
autorelease];
[[self operationQueue] addOperation:thumbnailOperation];
}
- (void)decodeThumbnail:(NSDictionary *)thumbnailDict {
CGImageRef originalImage = (CGImageRef)[thumbnailDict objectForKey:kThumbnailImageKey];
NSNumber *indexNumber = [thumbnailDict objectForKey:kThumbnailIndexKey];
if (originalImage == NULL || indexNumber == nil) {
return;
}
CFDataRef imageData = CGDataProviderCopyData(CGImageGetDataProvider(originalImage));
CGDataProviderRef imageDataProvider = CGDataProviderCreateWithCFData(imageData);
if (imageData != NULL) {
CFRelease(imageData);
}
CGImageRef image = CGImageCreate(CGImageGetWidth(originalImage),
CGImageGetHeight(originalImage),
CGImageGetBitsPerComponent(originalImage),
CGImageGetBitsPerPixel(originalImage),
CGImageGetBytesPerRow(originalImage),
CGImageGetColorSpace(originalImage),
CGImageGetBitmapInfo(originalImage),
imageDataProvider,
CGImageGetDecode(originalImage),
CGImageGetShouldInterpolate(originalImage),
CGImageGetRenderingIntent(originalImage));
if (imageDataProvider != NULL) {
CGDataProviderRelease(imageDataProvider);
}
NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
indexNumber, kThumbnailIndexKey,
image, kThumbnailImageKey,
nil];
CGImageRelease(image);
[self performSelectorOnMainThread:@selector(didDecodeThumbnail:)
withObject:dict
waitUntilDone:NO];
}
- (void)didDecodeThumbnail:(NSDictionary *)thumbnailDict {
NSNumber *indexNumber = [thumbnailDict objectForKey:kThumbnailIndexKey];
NSUInteger index = [indexNumber unsignedIntegerValue];
CGImageRef imageRef = (CGImageRef)[thumbnailDict objectForKey:kThumbnailImageKey];
UIImage *image = [UIImage imageWithCGImage:imageRef];
NSUInteger row = index / kThumbnailsInRow;
NSIndexPath *path = [NSIndexPath indexPathForRow:row inSection:0];
NSArray *visiblePaths = [[self tableView] indexPathsForVisibleRows];
if ([visiblePaths containsObject:path]) {
UITableViewCell *cell = [[self tableView] cellForRowAtIndexPath:path];
NSInteger tag = index - row * kThumbnailsInRow + 1;
UIButton *button = (UIButton *)[cell viewWithTag:tag];
[button setImage:image forState:UIControlStateNormal];
}
}
|