Как получить быстрый UICollectionView с большим количеством изображений (50-200)?

Я использую UICollectionView в приложении, которое отображает довольно много фотографий (50-200), и у меня возникают проблемы с его быстрой работой (например, такой же быстрой, как приложение «Фотографии»).

У меня есть пользовательский UICollectionViewCell с UIImageView в качестве подвида. Я загружаю изображения из файловой системы с помощью UIImage.imageWithContentsOfFile: в UIImageViews внутри ячеек.

Я пробовал довольно много подходов, но они либо содержали ошибки, либо имели проблемы с производительностью.

ПРИМЕЧАНИЕ. Я использую RubyMotion, поэтому любые фрагменты кода буду писать в стиле Ruby.

Прежде всего, вот мой собственный класс UICollectionViewCell для справки...

class CustomCell < UICollectionViewCell
  def initWithFrame(rect)
    super

    @image_view = UIImageView.alloc.initWithFrame(self.bounds).tap do |iv|
      iv.contentMode = UIViewContentModeScaleAspectFill
      iv.clipsToBounds = true
      iv.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth
      self.contentView.addSubview(iv)
    end

    self
  end

  def prepareForReuse
    super
    @image_view.image = nil
  end

  def image=(image)
    @image_view.image = image
  end
end

Подход №1

Все просто..

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  image_path = @image_paths[index_path.row]
  cell.image = UIImage.imageWithContentsOfFile(image_path)
end

Прокрутка вверх/вниз при этом ужасна. Это нервно и медленно.

Подход № 2

Добавьте немного кэширования через NSCache...

def viewDidLoad
  ...
  @images_cache = NSCache.alloc.init
  ...
end

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  image_path = @image_paths[index_path.row]

  if cached_image = @images_cache.objectForKey(image_path)
    cell.image = cached_image
  else
    image = UIImage.imageWithContentsOfFile(image_path)
    @images_cache.setObject(image, forKey: image_path)
    cell.image = image
  end
end

Опять же, нервный и медленный при первой полной прокрутке, но с тех пор все плавно.

Подход №3

Загружайте изображения асинхронно... (и сохраняйте кеширование)

def viewDidLoad
  ...
  @images_cache = NSCache.alloc.init
  @image_loading_queue = NSOperationQueue.alloc.init
  @image_loading_queue.maxConcurrentOperationCount = 3
  ...
end

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  image_path = @image_paths[index_path.row]

  if cached_image = @images_cache.objectForKey(image_path)
    cell.image = cached_image
  else
    @operation = NSBlockOperation.blockOperationWithBlock lambda {
      @image = UIImage.imageWithContentsOfFile(image_path)
      Dispatch::Queue.main.async do
        return unless collectionView.indexPathsForVisibleItems.containsObject(index_path)
        @images_cache.setObject(@image, forKey: image_path)
        cell = collectionView.cellForItemAtIndexPath(index_path)
        cell.image = @image
      end
    }
    @image_loading_queue.addOperation(@operation)
  end
end

Это выглядело многообещающе, но есть ошибка, которую я не могу отследить. При начальной загрузке просмотра все изображения являются одним и тем же изображением, и при прокрутке целых блоков загружается другое изображение, но у всех есть это изображение. Я пытался отладить его, но я не могу понять это.

Я также пытался заменить NSOperation на Dispatch::Queue.concurrent.async { ... }, но это похоже на то же самое.

Я думаю, что это, вероятно, правильный подход, если бы я мог заставить его работать правильно.

Подход №4

В отчаянии я решил просто предварительно загрузить все изображения как UIImages...

def viewDidLoad
  ...
  @preloaded_images = @image_paths.map do |path|
    UIImage.imageWithContentsOfFile(path)
  end
  ...
end

def collectionView(collection_view, cellForItemAtIndexPath: index_path)
  cell = collection_view.dequeueReusableCellWithReuseIdentifier(CELL_IDENTIFIER, forIndexPath: index_path)
  cell.image = @preloaded_images[index_path.row]
end

Это оказалось немного медленным и нервным в первой полной прокрутке, но после этого все было хорошо.

Сводка

Итак, если кто-нибудь может указать мне в правильном направлении, я был бы очень благодарен. Как приложение «Фото» справляется с этим так хорошо?


Примечание для всех остальных: [Принятый] ответ решил мою проблему с изображениями, появляющимися в неправильных ячейках, но у меня все еще была прерывистая прокрутка даже после изменения размера моих изображений до правильных размеров. Сократив свой код до самого необходимого, я в конце концов обнаружил, что виновником был UIImage#imageWithContentsOfFile. Несмотря на то, что я предварительно прогревал кеш UIImages, используя этот метод, UIImage, похоже, выполняет какое-то ленивое внутреннее кэширование. После обновления моего кода подогрева кеша, чтобы использовать решение, подробно описанное здесь, - > stackoverflow.com/a/10664707/71810 - наконец-то у меня была супер плавная прокрутка ~ 60FPS.


person Alistair Holt    schedule 13.12.2012    source источник


Ответы (2)


Я думаю, что подход № 3 — лучший способ, и я думаю, что, возможно, заметил ошибку.

Вы назначаете @image, закрытую переменную для всего класса представления коллекции, в своем операционном блоке. Вероятно, вам следует изменить:

@image = UIImage.imageWithContentsOfFile(image_path)

to

image = UIImage.imageWithContentsOfFile(image_path)

И измените все ссылки для @image, чтобы использовать локальную переменную. Я готов поспорить, что проблема в том, что каждый раз, когда вы создаете блок операции и присваиваете значение переменной экземпляра, вы заменяете то, что уже есть. Из-за некоторой случайности того, как операционные блоки удаляются из очереди, асинхронный обратный вызов основной очереди возвращает то же изображение, поскольку он обращается к последнему времени, когда было назначено @image.

По сути, @image действует как глобальная переменная для операций и блоков асинхронного обратного вызова.

person Jonathan Penn    schedule 13.12.2012
comment
Да спасибо! Не могу поверить, что я этого не видел. На данный момент производительность при прокрутке все еще колеблется, но из моего тестирования я считаю, что это просто потому, что мои изображения слишком велики для ячеек. - person Alistair Holt; 18.12.2012
comment
Примечание для всех остальных: этот ответ решил мою проблему с изображениями, появляющимися в неправильных ячейках, но у меня все еще была прерывистая прокрутка даже после изменения размера моих изображений до правильных размеров. Сократив свой код до самого необходимого, я в конце концов обнаружил, что виновником был UIImage#imageWithContentsOfFile. Несмотря на то, что я предварительно прогревал кеш UIImages, используя этот метод, UIImage, похоже, выполняет какое-то ленивое внутреннее кэширование. После обновления моего кода подогрева кеша, чтобы использовать решение, подробно описанное здесь - stackoverflow.com/a/10664707/71810 - у меня, наконец, супер плавная прокрутка ~ 60 кадров в секунду. - person Alistair Holt; 04.01.2013

Лучшим способом решения этой проблемы, который я нашел, было сохранение моего собственного кеша изображений и его предварительное прогревание в viewWillAppear в фоновом потоке, например так:

- (void) warmThubnailCache
{
    if (self.isWarmingThumbCache) {
        return;
    }

    if ([NSThread isMainThread])
    {
        // do it on a background thread
        [NSThread detachNewThreadSelector:@selector(warmThubnailCache) toTarget:self withObject:nil];
    }
    else
    {
        self.isWarmingThumbCache = YES;
        NIImageMemoryCache *thumbCache = self.thumbnailImageCache;
        for (GalleryImage *galleryImage in _galleryImages) {
            NSString *cacheKey = galleryImage.thumbImageName;
            UIImage *thumbnail = [thumbCache objectWithName:cacheKey];

            if (thumbnail == nil) {
                // populate cache
                thumbnail = [UIImage imageWithContentsOfFile:galleryImage.thumbImageName];

                [thumbCache storeObject:thumbnail withName:cacheKey];
            }
        }
        self.isWarmingThumbCache = NO;
    }
}

Вы также можете использовать GCD вместо NSThread, но я выбрал NSThread, поскольку одновременно должен выполняться только один из них, поэтому очередь не требуется. Кроме того, если у вас практически неограниченное количество изображений, вам придется быть более осторожным, чем мне. Вам придется быть более умным при загрузке изображений вокруг местоположения прокрутки пользователя, а не всех их, как я делаю здесь. Я могу это сделать, потому что количество изображений для меня фиксировано.

Я также должен добавить, что ваш collectionView:cellForItemAtIndexPath: должен использовать ваш кеш, например:

- (PSUICollectionViewCell *)collectionView:(PSUICollectionView *)collectionView
                  cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *CellIdentifier = @"GalleryCell";

    GalleryCollectionViewCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:CellIdentifier
                                                                                forIndexPath:indexPath];

    GalleryImage *galleryImage = [_galleryManager.galleryImages objectAtIndex:indexPath.row];

    // use cached images first
    NIImageMemoryCache *thumbCache = self.galleryManager.thumbnailImageCache;
    NSString *cacheKey = galleryImage.thumbImageName;
    UIImage *thumbnail = [thumbCache objectWithName:cacheKey];

    if (thumbnail != nil) {
        cell.imageView.image = thumbnail;
    } else {
        UIImage *image = [UIImage imageWithContentsOfFile:galleryImage.thumbImageName];

        cell.imageView.image = image;

        // store image in cache
        [thumbCache storeObject:image withName:cacheKey];
    }

    return cell;
}

Убедитесь, что ваш кеш очищается, когда вы получаете предупреждение о памяти, или используйте что-то вроде NSCache. Я использую фреймворк под названием Nimbus, в котором есть что-то под названием NIImageMemoryCache, которое позволяет мне установить максимальное количество пикселей для кэширования. Очень удобно.

Кроме этого, следует обратить внимание на то, что НЕ используйте метод UIImage «imageNamed», который выполняет собственное кэширование и вызовет проблемы с памятью. Вместо этого используйте «imageWithContentsOfFile».

person elsurudo    schedule 16.12.2012