2012-12-13 7 views
12

Używam UICollectionView w aplikacji, która wyświetla dość dużo zdjęć (50-200) i mam problemy z jej pobieraniem (tak efektowne, jak aplikacja Zdjęcia na przykład).Jak uzyskać efektowny obraz UICollectionView z dużą ilością obrazów (50-200)?

Mam niestandardowy UICollectionViewCell z UIImageView jako jego wydział. Ładuję obrazy z systemu plików, z UIImage.imageWithContentsOfFile:, do UIImageViews wewnątrz komórek.

Próbowałem już kilku podejść, ale były albo błędne albo miały problemy z wydajnością.

UWAGA: Używam RubyMotion, więc napiszę wszystkie fragmenty kodu w stylu Ruby.

Przede wszystkim, tu jest mój zwyczaj klasa UICollectionViewCell odsyłające ...

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 

Podejście nr 1

Utrzymanie rzeczy proste ..

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 

Przewijanie w górę/dół jest straszny przy użyciu tego. Jest szybki i powolny.

Podejście nr 2

dodać trochę buforowanie poprzez 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 

Znowu jumpy i powolny w pierwszym pełnym zwoju, ale od tej pory to gładko.

Podejście nr 3

Załaduj obrazy asynchronicznie ... (i zachować buforowanie)

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 

Ten wyglądał obiecująco, ale nie jest to problem, że nie mogę wyśledzić. Po załadowaniu początkowego widoku wszystkie obrazy są tym samym obrazem, a podczas przewijania całe bloki ładują się z innym obrazem, ale wszystkie mają ten obraz. Próbowałem debugowania go, ale nie mogę tego rozgryźć.

Próbowałem również podstawiania NSOperation z Dispatch::Queue.concurrent.async { ... }, ale wydaje się, że to samo.

Myślę, że to prawdopodobnie prawidłowe podejście, gdybym mógł sprawić, by działał prawidłowo.

Podejście # 4

W frustracji postanowiłem właśnie pre-load wszystkie obrazy jako 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 

ten okazał się być nieco powolna i nerwowy w pierwszym pełne przewijanie, ale wszystko dobrze po tym.

Podsumowanie

Tak więc, jeśli ktoś może wskazać mi w dobrym kierunku byłbym rzeczywiście najbardziej wdzięczny. Jak działa aplikacja Photos tak dobrze?


Uwaga dla nikogo innego: [Przyjęty] odpowiedź rozwiązać mój problem obrazów pojawiających się w niewłaściwych komórkach, ale nadal był coraz niepewny przewijanie nawet po zmianie rozmiaru moich obrazów w celu skorygowania wielkości. Po zepchnięciu mojego kodu do najpotrzebniejszych rzeczy w końcu stwierdziłem, że winowajcą jest UIImage # imageWithContentsOfFile. Mimo że wcześniej podgrzewałem pamięć podręczną obiektów UII za pomocą tej metody, UIImage wydaje się leniwym buforowaniem wewnętrznie. Po aktualizacji kodu ocieplania pamięci podręcznej, aby skorzystać z rozwiązania opisanego tutaj - stackoverflow.com/a/10664707/71810 - W końcu miałem super płynne przewijanie ~ 60FPS.

Odpowiedz

7

Myślę, że podejście nr 3 jest najlepszym sposobem, aby przejść, i myślę, że mogłem zauważyć błąd.

Przypisujesz do @image, zmienną prywatną dla całej klasy widoku kolekcji, w bloku operacyjnym. powinieneś zmienić:

@image = UIImage.imageWithContentsOfFile(image_path) 

do

image = UIImage.imageWithContentsOfFile(image_path) 

I zmienić wszystkie odniesienia do @image użyć zmiennej lokalnej. Mogę się założyć, że problem polega na tym, że za każdym razem, gdy tworzysz blok operacyjny i przypisujesz do zmiennej instancji, zastępujesz to, co już tam jest. Ze względu na losowość blokowania operacji bloki operacyjne, wywołanie zwrotne asynchroniczne głównej kolejki przywraca ten sam obraz, ponieważ uzyskuje dostęp do ostatniego przypisania @image.

W istocie, @image zachowuje się jak zmienna globalna dla bloków wywołań funkcji i asynchronicznych.

+0

Tak, dziękuję! Nie mogę uwierzyć, że tego nie widziałem.W tej chwili wydajność wciąż się zacina, ale z moich testów wynika, że ​​moje obrazy są zbyt duże dla komórek. – alistairholt

+4

Uwaga dla kogokolwiek innego: ta odpowiedź rozwiązała problem z obrazami pojawiającymi się w niewłaściwych komórkach, ale wciąż otrzymywałem przerywane przewijanie nawet po zmianie rozmiaru obrazów do prawidłowych rozmiarów. Po zepchnięciu mojego kodu do najpotrzebniejszych rzeczy w końcu stwierdziłem, że winowajcą jest UIImage # imageWithContentsOfFile. Mimo że wcześniej podgrzewałem pamięć podręczną obiektów UII za pomocą tej metody, UIImage wydaje się leniwym buforowaniem wewnętrznie. Po aktualizacji kodu ocieplania pamięci podręcznej, aby skorzystać z rozwiązania opisanego tutaj - http://stackoverflow.com/a/10664707/71810 - W końcu miałem super płynne przewijanie ~ 60FPS. – alistairholt

1

Najlepszym sposobem znalazłem rozwiązania tego problemu było utrzymując własną pamięć podręczną obrazów i wstępnie ogrzewając go na viewWillAppear na wątek w tle, tak jak poniżej:

- (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; 
    } 
} 

Można również użyć GCD zamiast NSThread, ale wybrałem NSThread, ponieważ tylko jeden z nich powinien działać jednocześnie, więc nie trzeba kolejki. Ponadto, jeśli masz w zasadzie nieograniczoną liczbę obrazów, będziesz musiał być ostrożniejszy niż I. Będziesz musiał być bardziej sprytny, jeśli chodzi o ładowanie obrazów wokół lokalizacji przewijania użytkownika, a nie wszystkich z nich, tak jak robię to tutaj. Mogę to zrobić, ponieważ liczba zdjęć jest dla mnie ustalona.

Powinienem też dodać, że CollectionView: cellForItemAtIndexPath: Należy korzystać z pamięci podręcznej, tak:

- (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; 
} 

Upewnij się, że czyści cache, gdy pojawi się ostrzeżenie pamięci lub użyć czegoś podobnego NSCache. Korzystam z frameworka Nimbus, który ma coś o nazwie NIImageMemoryCache, co pozwala ustawić maksymalną liczbę pikseli w pamięci podręcznej. Dość przydatny.

Oprócz tego, jedną uwagę należy zwrócić na to, że NIE należy używać metody "imageNamed" UIImage, która wykonuje własne buforowanie i daje problemy z pamięcią. Zamiast tego użyj "imageWithContentsOfFile".

+0

Dzięki za porady. – alistairholt

Powiązane problemy