Подтвердить что ты не робот

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

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

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

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

ПРИМЕЧАНИЕ. Я использую 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

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

Резюме

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


Примечание для кого-либо еще: [Принятый] ответ разрешил мою проблему с изображениями, появляющимися в неправильных ячейках, но я все еще получал прерывистую прокрутку даже после изменения размера моих изображений для корректировки размеров. После того, как я снял свой код с самого начала, я в конечном итоге обнаружил, что виновником является UIImage # imageWithContentsOfFile. Несмотря на то, что я использовал pre-warming кеш UIImages, используя этот метод, UIImage, похоже, делает какое-то ленивое кэширование внутри. После обновления кода кэширования кеша, чтобы использовать решение, подробно описанное здесь, qaru.site/info/441990/... - У меня наконец была суперглавная прокрутка ~ 60FPS.

4b9b3361

Ответ 1

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

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

@image = UIImage.imageWithContentsOfFile(image_path)

к

image = UIImage.imageWithContentsOfFile(image_path)

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

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

Ответ 2

Лучший способ решить эту проблему состоял в том, что я сохранил свой собственный кеш изображений и предварительно нагрел его на 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, что позволяет мне установить максимальное количество пикселей в кеш. Очень удобно.

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