2012-12-13 12 views
12

Sto usando UICollectionView in un'app che mostra un bel po 'di foto (50-200) e ho problemi a far sì che sia scattante (per esempio scattante come l'app Foto).Come ottenere un UICollectionView scattante con molte immagini (50-200)?

Ho un UICollectionViewCell personalizzato con un UIImageView come sottoview. Sto caricando immagini dal filesystem, con UIImage.imageWithContentsOfFile:, nelle UIImageViews all'interno delle celle.

Ho provato un paio di approcci ora ma sono stati bug o hanno avuto problemi di prestazioni.

NOTA: Sto usando RubyMotion, quindi scriverò qualsiasi frammento di codice in stile Ruby.

Prima di tutto, ecco la mia personalizzato classe UICollectionViewCell per riferimento ...

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 

Approccio # 1

Mantenere le cose semplici ..

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 

Scorrimento su/giù è orribile usando questo. È nervoso e lento.

Approccio # 2

aggiungere un po 'di caching tramite 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 

Anche in questo caso, jumpy e lenta nel primo rotolo pieno, ma da allora è una navigazione tranquilla.

Approccio # 3

caricare le immagini in modo asincrono ... (e mantenere il caching)

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 

Questo sembrava promettente, ma c'è un bug che non riesco a rintracciare. Nella vista iniziale carica tutte le immagini sono la stessa immagine e mentre scorri blocchi interi carichi con un'altra immagine ma tutti hanno quell'immagine. Ho provato a debuggarlo ma non riesco a capirlo.

Ho anche provato a sostituire NSOperation con Dispatch::Queue.concurrent.async { ... } ma sembra essere lo stesso.

Penso che questo sia probabilmente l'approccio corretto se potessi farlo funzionare correttamente.

Approccio # 4

In frustrazione ho deciso di appena precarico tutte le immagini come 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 

Questo si è rivelato essere un po 'lento e nervosi nel primo scroll completo ma tutto bene dopo.

Sommario

Quindi, se qualcuno mi può puntare nella giusta direzione sarei davvero molto grato. Come funziona l'app Foto così bene?


Nota per chiunque altro: [La accettato] risposta risolto il mio problema di immagini che appaiono nelle cellule sbagliate ma ero ancora ricevendo lo scorrimento instabile anche dopo il ridimensionamento mie immagini per correggere dimensioni. Dopo aver spogliato il mio codice fino all'essenziale, alla fine ho scoperto che UIImage # imageWithContentsOfFile era il colpevole. Anche se stavo pre-riscaldando una cache di UIImages usando questo metodo, UIImage sembra fare una specie di cache pigra internamente. Dopo aver aggiornato il mio codice di riscaldamento della cache per utilizzare la soluzione dettagliata qui - stackoverflow.com/a/10664707/71810 - Finalmente ho avuto uno scrolling super veloce di 60FPS.

risposta

7

Penso che l'approccio # 3 sia il modo migliore per andare, e penso di aver individuato il bug.

Stai assegnando a @image, una variabile privata per l'intera classe di visualizzazione raccolta, nel tuo blocco operativo. Probabilmente si dovrebbe cambiare:

@image = UIImage.imageWithContentsOfFile(image_path) 

a

image = UIImage.imageWithContentsOfFile(image_path) 

e cambiare tutti i riferimenti per @image per utilizzare la variabile locale. Sono disposto a scommettere che il problema è che ogni volta che crei un blocco operativo e assegni alla variabile di istanza, stai sostituendo ciò che è già presente. A causa della casualità del modo in cui i blocchi di operazioni vengono rimossi dalla coda, la richiamata asincrona della coda principale sta recuperando la stessa immagine perché sta accedendo all'ultima volta che è stato assegnato @image.

In pratica, @image si comporta come una variabile globale per l'operazione e i blocchi callback asincroni.

+0

Sì, grazie! Non posso credere di non averlo visto.Al momento la performance è ancora in movimento, ma dai miei test credo che sia solo perché le mie immagini sono troppo grandi per le celle. – alistairholt

+4

Nota per chiunque altro: questa risposta ha risolto il mio problema di immagini che appaiono nelle celle sbagliate, ma continuavo a ricevere scansioni discontinue anche dopo il ridimensionamento delle immagini per correggere le dimensioni. Dopo aver spogliato il mio codice fino all'essenziale, alla fine ho scoperto che UIImage # imageWithContentsOfFile era il colpevole. Anche se stavo pre-riscaldando una cache di UIImages usando questo metodo, UIImage sembra fare una specie di cache pigra internamente. Dopo aver aggiornato il mio codice di riscaldamento della cache per utilizzare la soluzione dettagliata qui - http://stackoverflow.com/a/10664707/71810 - Finalmente ho avuto uno scrolling super veloce di 60FPS. – alistairholt

1

Il modo migliore che ho trovato per risolvere questo problema è stato di mantenere la mia propria cache delle immagini, e pre-riscaldamento su viewWillAppear su un thread in background, in questo modo:

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

È anche possibile utilizzare GCD invece di NSThread, ma ho optato per NSThread, poiché solo uno di questi dovrebbe essere eseguito alla volta, quindi non è necessaria alcuna coda. Inoltre, se si dispone di un numero essenzialmente illimitato di immagini, sarà necessario essere più prudenti di me. Dovrai essere più intelligente nel caricare le immagini intorno alla posizione di scorrimento di un utente, piuttosto che tutte, come faccio qui. Posso farlo perché il numero di immagini, per me, è fisso.

Vorrei anche aggiungere che il vostro CollectionView: cellForItemAtIndexPath: dovrebbe usare la cache, in questo modo:

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

Assicurarsi che la cache cancella quando si ottiene un avviso di memoria, o usare qualcosa come NSCache. Sto usando un framework chiamato Nimbus, che ha qualcosa chiamato NIImageMemoryCache, che mi permette di impostare un numero massimo di pixel per la cache. Abbastanza utile.

Oltre a ciò, una cosa da prendere in considerazione è NON utilizzare il metodo "imageNamed" di UIImage, che fa il proprio caching, e ti darà problemi di memoria. Utilizzare invece "imageWithContentsOfFile".

+0

Grazie per i suggerimenti. – alistairholt

Problemi correlati