13

非常に多くの写真(50〜200)を表示しているアプリで使用UICollectionViewしていますが、写真をスナッピーにするのに問題があります(たとえば、写真アプリのようにスナッピーです)。

サブビューとしての習慣UICollectionViewCellがあります。ファイルシステムから、セル内のUIImageViewsにUIImageView画像を読み込んでいます。UIImage.imageWithContentsOfFile:

私は今かなりの数のアプローチを試しましたが、それらはバグがあるか、パフォーマンスの問題がありました。

注:私は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のスクロールが可能になりました。

4

2 に答える 2

7

アプローチ#3が最善の方法だと思います。そして、バグを見つけたのではないかと思います。

@image操作ブロックで、コレクションビュークラス全体のプライベート変数であるに割り当てています。おそらく変更する必要があります:

@image = UIImage.imageWithContentsOfFile(image_path)

image = UIImage.imageWithContentsOfFile(image_path)

そして@image、ローカル変数を使用するためにすべての参照を変更します。問題は、操作ブロックを作成してインスタンス変数に割り当てるたびに、すでに存在するものを置き換えていることです。操作ブロックのデキュー方法がランダムであるため、メインキューの非同期コールバックは、最後@imageに割り当てられた時刻にアクセスしているため、同じイメージを取り戻します。

本質的@imageに、操作および非同期コールバックブロックのグローバル変数のように機能します。

于 2012-12-13T21:52:30.823 に答える
1

この問題を解決するために私が見つけた最善の方法は、画像の独自のキャッシュを保持し、次のようにバックグラウンドスレッドの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;
    }
}

NSThreadの代わりにGCDを使用することもできますが、一度に実行する必要があるのは1つだけであり、キューは必要ないため、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と呼ばれるものがあります。とても便利です。

それ以外に注意しなければならないことの1つは、UIImageの「imageNamed」メソッドを使用しないことです。これは独自のキャッシュを実行し、メモリの問題を引き起こします。代わりに「imageWithContentsOfFile」を使用してください。

于 2012-12-16T11:19:07.397 に答える