2012-12-13 3 views
12

Ich benutze UICollectionView in einer App, die ziemlich viele Fotos (50-200) anzeigt und ich habe Probleme, es bissig zu bekommen (so schnell wie Fotos App zum Beispiel).Wie bekommt man eine bissige UICollectionView mit vielen Bildern (50-200)?

Ich habe eine benutzerdefinierte UICollectionViewCell mit einer UIImageView als Subview. Ich lade Bilder aus dem Dateisystem mit UIImage.imageWithContentsOfFile: in die UIImageViews innerhalb der Zellen.

Ich habe jetzt einige Ansätze versucht, aber sie waren entweder fehlerhaft oder hatten Leistungsprobleme.

HINWEIS: Ich benutze RubyMotion, damit ich alle Code-Schnipsel im Ruby-Stil schreiben kann.

Zunächst einmal ist hier meine individuelle UICollectionViewCell Klasse als Referenz ...

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 

Ansatz # 1

Dinge halten einfach ..

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 

Bildlauf nach oben/Es ist schrecklich, dies zu benutzen. Es ist nervös und langsam.

Ansatz # 2

ein bisschen Caching über nscache Hinzufügen ...

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 

wieder sprunghaft und langsam auf das erste volle Scroll aber es ist glatt Segeln von da.

Ansatz # 3

laden die Bilder asynchron ... (und hält das 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 

Das sah vielversprechend aus, aber es ist ein Fehler, dass ich nach unten nicht verfolgen kann. In der ersten Ansicht laden alle Bilder das gleiche Bild und wenn Sie ganze Blöcke scrollen, laden Sie ein anderes Bild, aber alle haben dieses Bild. Ich habe versucht, es zu debuggen, aber ich kann es nicht herausfinden.

Ich habe auch versucht, die NSOperation mit Dispatch::Queue.concurrent.async { ... } zu ersetzen, aber das scheint das gleiche zu sein.

Ich denke, das ist wahrscheinlich der richtige Ansatz, wenn ich es richtig funktionieren könnte.

Ansatz # 4

In Frustration habe ich beschlossen, nur Vorbelastung alle Bilder als 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 

Dies erwies ein bisschen langsam und ruckartig in die erste sein aus volle Schriftrolle, aber danach alles gut.

Zusammenfassung

Also, wenn jemand mich in die richtige Richtung zeigen kann ich Ihnen sehr dankbar, in der Tat sein würde. Wie macht Fotos App es so gut?


Hinweis für alle anderen: [Die akzeptierte] Antwort aufgelöst meine Ausgabe von Bildern in den falschen Zellen erscheinen, aber ich war immer noch abgehackt Scrollen immer auch nach meinen Bildern Ändern der Größe Größen zu korrigieren. Nachdem ich meinen Code auf das Wesentliche reduziert hatte, fand ich schließlich, dass UIImage # imageWithContentsOfFile der Schuldige war. Obwohl ich mit dieser Methode einen Cache von UIImages vorerwärmte, scheint UIImage intern eine Art faules Caching durchzuführen. Nach dem Aktualisieren meines Cache-Erwärmungscodes, um die hier beschriebene Lösung zu verwenden - stackoverflow.com/a/10664707/71810 - hatte ich endlich super weiches ~ 60FPS Scrollen.

Antwort

7

Ich denke, dass Ansatz Nr. 3 der beste Weg ist, und ich denke, ich habe den Fehler entdeckt.

Sie ordnen @image eine private Variable für die gesamte Ansichtsklasse der Sammlung in Ihrem Operationsblock zu. Sie sollten wahrscheinlich ändern:

@image = UIImage.imageWithContentsOfFile(image_path) 

zu

image = UIImage.imageWithContentsOfFile(image_path) 

Und alle Verweise ändern für @image die lokale Variable zu verwenden. Ich wette, dass das Problem darin besteht, dass Sie jedes Mal, wenn Sie einen Operationsblock erstellen und der Instanzvariablen zuweisen, das ersetzen, was bereits vorhanden ist. Aufgrund der Zufälligkeit der Ausblendung der Betriebsblöcke erhält der Async-Callback der Hauptwarteschlange das gleiche Image zurück, da er auf das letzte Mal zugreift, dass @image zugewiesen wurde.

Im Wesentlichen funktioniert @image wie eine globale Variable für die Operation und asynchrone Callback-Blöcke.

+0

Ja, danke! Ich kann nicht glauben, dass ich das nicht gesehen habe.Die Performance stottert im Moment noch auf Scroll, aber ich glaube, dass das nur deshalb der Fall ist, weil meine Bilder zu groß für die Zellen sind. – alistairholt

+4

Hinweis für alle anderen: Diese Antwort löste das Problem, dass Bilder in den falschen Zellen angezeigt werden, aber ich bekam immer noch abgehacktes Scrollen, auch nachdem ich meine Bilder auf die richtigen Größen skaliert hatte. Nachdem ich meinen Code auf das Wesentliche reduziert hatte, fand ich schließlich, dass UIImage # imageWithContentsOfFile der Schuldige war. Obwohl ich mit dieser Methode einen Cache von UIImages vorerwärmte, scheint UIImage intern eine Art faules Caching durchzuführen. Nachdem ich meinen Cache-Erwärmungscode aktualisiert hatte, um die hier beschriebene Lösung zu verwenden - http://StackOverflow.com/a/10664707/71810 - hatte ich endlich super glatte ~ 60FPS Scrolling. – alistairholt

1

Die beste Weise, die ich zur Lösung dieses Problems gefunden war von meinem eigenen Cache von Bildern zu halten und Vorwärmen auf viewWillAppear auf einem Hintergrund-Thread, etwa so:

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

Sie können auch stattdessen GCD von NSThread, aber ich entschied mich für NSThread, da nur einer von diesen gleichzeitig ausgeführt werden sollte, also keine Warteschlange erforderlich ist. Auch wenn Sie eine im Wesentlichen unbegrenzte Anzahl von Bildern haben, müssen Sie vorsichtiger sein als ich. Sie müssen schlauer sein über das Laden der Bilder um den Scroll-Ort eines Benutzers, anstatt alle von ihnen, wie ich hier tun. Ich kann das machen, weil die Anzahl der Bilder für mich festgelegt ist.

Ich möchte auch hinzufügen, dass Ihre Kollektion: 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; 
} 

Stellen Sie sicher, Ihre Cache gelöscht, wenn Sie eine Speicher Warnung erhalten, oder verwenden Sie so etwas wie nscache: Cache, wie so verwenden sollten. Ich benutze ein Framework namens Nimbus, das etwas namens NIImageMemoryCache hat, mit dem ich eine maximale Anzahl von Pixeln zum Zwischenspeichern einstellen kann. Ziemlich praktisch.

Andere als das, was man zur Kenntnis nehmen muss, ist es, die "imageNamed" -Methode von UIImage zu verwenden, die ihr eigenes Caching durchführt und Speicherprobleme verursacht. Verwenden Sie stattdessen "imageWithContentsOfFile".

+0

Danke für die Tipps. – alistairholt

Verwandte Themen