Представим таблицу UITableView
, которая выполняет роль содержания иллюстрированной книги для «Айпада». В каждой ячейке таблицы находится по несколько уменьшенных изображений страниц книги. Эти изображения довольно крупные, потому что каждая страница — это комикс и важно, чтобы люди по уменьшенным картинкам могли легко найти нужную.
Когда изображение достаточно большое, его декодирование занимает довольно много времени. Это наблюдается и с PNG, который еще на этапе сборки проекта переводится в удобный для устройства формат и рекомендован «Эпплом» для использовании в UIKit-программах. Если не предпринимать специальных действий, то не получится добиться плавности прокрутки таблицы, ячейки которой содержат такие большие изображения.
Вначале может показаться, что проблема в чтении файла с накопителя, но профайлер говорит, что это не так. Как -[UIImage imageNamed:]
, так и -[UIImage imageWithContentsOfFile:]
не декодируют изображения сразу, а откладывают это на потом. Декодирование происходит, например, при вызове -[UIImageView setImage:]
. Что самое неприятное, этот метод нельзя вызывать в неглавном потоке. До iOS 4 никакие методы UIKit нельзя было вызывать в потоках, отличающихся от главного. В iOS 4 ситуация немного изменилась, но касательно -[UIImageView setImage:]
правила остались те же. Получается, находясь на уровне UIKit запустить дорогостоящую операцию декодирования изображения можно только в драгоценном главном потоке, который занят прокруткой таблицы в тот самый момент, когда это изображение как раз нужно.
Иногда, конечно, можно заранее декодировать все изображения, а потом только лишь отдавать их. Но такое решение ненадежно, потому что изображений может быть очень много и они могут не вместиться в память. К тому же время главного потока все равно придется потратить в какой-то момент. Если, например, это сделать на этапе запуска приложения, то время запуска увеличится, а это непозволительная роскошь в iOS.
К счастью, нам доступен не только UIKit с его UIImage
, но и Core Graphics с CGImage
. И функции CGImage
можно вызывать в фоне, в неглавном потоке. Более того, там же изображение можно и декодировать. Вот как это делается:
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);
|
Изображение, полученное от CGImageCreate()
, уже декодировано, а это то, что нам и нужно. Осталось только завернуть этот код в NSOperation
, чтобы выполнить его в фоне, и в конце выполнения передать результаты в главный поток. Из полученного CGImage
с помощью +[UIImage imageWithCGImage:]
получаем UIImage
и назначаем его нужному UIImageView
прямо в процессе прокрутки. Анимация при этом больше не страдает.
Пример использования:
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];
}
}
|