• Техногрет
  • Прокрутка таблицы с большими изображениями в iOS

    HTML и CSSXSLTJavaScriptИзображенияСофтEtc
    Алексей Кузнецов

    26 апреля 2011


    Задача.

    Добиться плавности прокрутки таблицы с большими изображениями.

    Представим таблицу 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 = ...  // Assume this exists.
    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);
    }
    
    // Do something with the image.
    
    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 = ...;  // Page index, assume it exists.
      
      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 {
      // thumbnailDict contains thumbnail and its index. Thumbnail
      // is represented by a UIImage object with key kThumbnailImageKey,
      // Thumbnail index is represented by an NSNumber object with key
      // kThumbnailIndexKey.
      
      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 {
      // Unpack thumbnail and its index.
      NSNumber *indexNumber = [thumbnailDict objectForKey:kThumbnailIndexKey];
      NSUInteger index = [indexNumber unsignedIntegerValue];
      CGImageRef imageRef = (CGImageRef)[thumbnailDict objectForKey:kThumbnailImageKey];
      UIImage *image = [UIImage imageWithCGImage:imageRef];
      
      // Set thumbnail as an image for the button that opens book page at a given index.
      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];
      }
    }