2013-05-22 10 views
5

Ho bisogno di ridimensionare una grande immagine memorizzata localmente (contenuta in self.optionArray) e quindi mostrarla in collectionView. Se lo mostro, iOS sta cercando di ridimensionare le immagini mentre scorrono rapidamente causando arresti anomali relativi alla memoria.UICollectionView Cell Immagine cambiando come appare con GCD

Nel codice riportato di seguito, collectionView scorrerà senza problemi, ma a volte se si scorre estremamente veloce, l'immagine scorretta verrà visualizzata e quindi modificata rispetto a quella corretta mentre lo scorrimento decelera. Perché non sta impostando il su nil risolvendolo?

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{ 

    CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath]; 
    cell.cellImage.image = nil; 
      dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

      dispatch_async(queue, ^{ 
       cell.cellImage.image = nil; 
       UIImage *test = [self.optionArray objectAtIndex:indexPath.row]; 
       UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)]; 

       dispatch_sync(dispatch_get_main_queue(), ^{ 

        cell.cellImage.image = localImage2 
        cell.cellTextLabel.text = @""; 
        [cell setNeedsLayout]; 
       }); 

      }); 

     } 

    return cell; 
    } 

- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { 
    UIGraphicsBeginImageContextWithOptions(newSize, NO, 0.0); 
    [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; 
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); 
    UIGraphicsEndImageContext(); 
    return newImage; 
} 

EDIT: ho aggiunto un altro asincrone a cache di primo e nil e inizializzato il cell.image. Sto avendo lo stesso problema sullo scorrimento veloce iniziale verso il basso. Tuttavia, sul backup di scorrimento, ora è impeccabile.

ho aggiunto questo:

-(void)createDictionary 
{ 
    for (UIImage *test in self.optionArray) { 
     UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)]; 
     [localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:[self.optionArray indexOfObject:test]]]; 
    } 
} 

- (void)viewDidLoad 
{ 
    [super viewDidLoad]; 
    if (!localImageDict) { 
     localImageDict = [[NSMutableDictionary alloc]initWithCapacity:self.optionArray.count]; 
    } 
    else { 
     [localImageDict removeAllObjects]; 
    } 
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

    dispatch_async(queue, ^{ 
     [self createDictionary]; 
    }); 

} 
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{ 
    CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath]; 
    cell.cellImage.image = nil; 
    cell.cellImage.image = [[UIImage alloc]init]; 

     if ([localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]]) { 
      cell.cellImage.image = [localImageDict objectForKey:[NSNumber numberWithInt:indexPath.row]]; 
      cell.cellTextLabel.text = @""; 
     } 
    else { 

     cell.cellImage.image = nil; 
     cell.cellImage.image = [[UIImage alloc]init]; 
     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

     dispatch_async(queue, ^{ 
      UIImage *test = [self.optionArray objectAtIndex:indexPath.row]; 
      UIImage *shownImage = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)]; 
      [localImageDict setObject:shownImage forKey:[NSNumber numberWithInt:indexPath.row]]; 

      dispatch_sync(dispatch_get_main_queue(), ^{ 

       cell.cellImage.image = shownImage; 

       cell.cellTextLabel.text = @""; 
       [cell setNeedsLayout]; 
      }); 

     }); 
    } 

} 
return cell; 

risposta

6

Dando un'occhiata più da vicino il vostro codice di esempio, posso vedere la causa del problema di memoria. Il problema più significativo che emerge è che sembra che tu stia tenendo tutte le tue immagini in un array. Ciò richiede una quantità straordinaria di memoria (e deduco dal tuo bisogno di ridimensionare le immagini che devono essere grandi).

Per ridurre il footprint dell'app, non è necessario mantenere una serie di oggetti UIImage. Invece, mantieni una serie di URL o percorsi per le tue immagini e poi crea solo gli oggetti UIImage in tempo reale come sono necessari dall'interfaccia utente (un processo che si chiama lazy-loading). E una volta che l'immagine esce dallo schermo, è possibile rilasciarlo (lo UICollectionView, come lo UITableView, fa un sacco di questo lavoro di pulizia per te, purché non mantenga forti riferimenti alle immagini).

Un'app dovrebbe generalmente mantenere solo gli oggetti UIImage per le immagini attualmente visibili. È possibile memorizzare nella cache queste immagini ridimensionate (utilizzando NSCache, ad esempio) per motivi di prestazioni, ma le cache verranno quindi eliminate automaticamente quando si esaurisce la memoria.

La cosa buona è che ovviamente sei già esperto nell'elaborazione asincrona. In ogni caso, l'attuazione potrebbe apparire in questo modo:

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath 
{ 
    CustomTabBarCell *cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CustomTabBarCell" forIndexPath:indexPath]; 

    NSString *filename = [self.filenameArray objectAtIndex:indexPath.row]; // I always use indexPath.item, but if row works, that's great 

    UIImage *image = [self.thumbnailCache objectForKey:filename];   // you can key this on whatever you want, but the filename works 

    cell.cellImage.image = image;           // this will load cached image if found, or `nil` it if not found 

    if (image == nil)              // we only need to retrieve image if not found in our cache 
    { 
     dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul); 

     dispatch_async(queue, ^{ 
      UIImage *test = [UIImage imageWithContentsOfFile:filename]; // load the image here, now that we know we need it 
      if (!test) 
      { 
       NSLog(@"%s: unable to load image", __FUNCTION__); 
       return; 
      } 

      UIImage *localImage2 = [self imageWithImage:test scaledToSize:CGSizeMake(test.size.width/5, test.size.height/5)]; 
      if (!localImage2) 
      { 
       NSLog(@"%s: unable to convert image", __FUNCTION__); 
       return; 
      } 

      [self.thumbnailCache setObject:localImage2 forKey:filename]; // save the image to the cache 

      dispatch_async(dispatch_get_main_queue(), ^{     // async is fine; no need to keep this background operation alive, waiting for the main queue to respond 
       // see if the cell for this indexPath is still onscreen; probably is, but just in case 

       CustomTabBarCell *updateCell = (id)[collectionView cellForItemAtIndexPath:indexPath]; 
       if (updateCell) 
       { 
        updateCell.cellImage.image = localImage2 
        updateCell.cellTextLabel.text = @""; 
        [updateCell setNeedsLayout]; 
       } 
      }); 

     }); 
    } 

    return cell; 
} 

Questo presuppone che si definisce una proprietà di classe di thumbnailCache che è un forte riferimento a una NSCache che si inizializza in viewDidLoad, o dovunque. Il caching è un modo per ottenere il meglio da entrambi i mondi, caricare le immagini in memoria per prestazioni ottimali, ma verrà rilasciato quando si verifica la pressione della memoria.

Chiaramente, sto assumendo allegramente "oh, sostituisci semplicemente la tua serie di immagini con una serie di nomi di file immagine", e so che probabilmente dovrai entrare in una serie di diverse parti del tuo codice per farlo lavoro, ma questa è indubbiamente la fonte del consumo di memoria. Chiaramente, potresti sempre avere altri problemi di memoria (mantenere cicli e simili), ma non c'è niente di simile qui nello snippet che hai postato.

+0

Tutte le immagini sono locali, nessuna viene scaricata da un server. Questo cambia qualcuno dei tuoi consigli? – Eric

+0

@Eric Ah, scusa se non l'ho notato. In tal caso, è improbabile che il mio secondo punto si manifesti (anche se teoricamente potrebbe). E il quarto punto è anche meno di un problema. Ma i punti uno e tre sono decisamente rilevanti e potresti notare un leggero miglioramento delle prestazioni rispetto al quarto punto se usi una cache. Ti inviterei a testare il tuo codice sul dispositivo più lento possibile, perché non vedrai alcuni di questi problemi sul simulatore o su un dispositivo più recente. – Rob

+0

puoi dare un'occhiata alla mia modifica? – Eric

1

Ho avuto un problema simile ma ne ho fatto un altro modo.

Avevo anche il problema di "pop-in" poiché le immagini che erano state caricate asincrone venivano visualizzate mentre arrivavano fino a quando non veniva mostrato quello corretto.

Una ragione per cui questo sta accadendo è che il percorso di indicizzazione corrente per la cella che era stata inizialmente cancellata non corrispondeva all'indice dell'immagine che si sta inserendo.

In pratica, se si scorre rapidamente da 0-19 e la cella che si desidera aggiornare è # 20 e si desidera visualizzare l'immagine # 20, ma sta ancora caricando le immagini 3, 7, 14 in modo asincrono.

Per evitare ciò, ciò che ho fatto è stato tracciare due indici; # 1) il percorso di indicizzazione più recente che riflette la posizione effettiva della cella e # 2) l'indice corrispondente all'immagine che viene effettivamente caricata asincrona (in questo caso dovrebbe essere effettivamente il percorso dell'indice che si sta passando a cellforitematindexpath, viene mantenuto come il processo asincrono funziona attraverso la coda quindi saranno in realtà dati "vecchi" per il caricamento dell'immagine).

Un modo per ottenere il percorso indice più recente può essere quello di creare un metodo semplice che restituisca appena un NSInteger per la posizione corrente della cella. Memorizza come currentIndex.

Poi ho messo una coppia se le affermazioni che hanno verificato che i due erano uguali prima di riempire effettivamente l'immagine.

quindi se (currentIndex == imageIndex), quindi caricare l'immagine.

se si inserisce un NSLog (@ "CORRENTE ...% d ... IMMAGINE ...% d", currentIndex, imageIndex) prima di quelli se le istruzioni si possono vedere abbastanza chiaramente quando i due non corrispondono e le chiamate asincrone dovrebbero uscire.

Spero che questo aiuti.

1

Ho trovato la formulazione di ciò che ha detto chuey101, confondendo. Ho capito un modo e poi ho capito che chuey101 significava lo stesso.

Se aiuta qualcuno, le immagini vengono visualizzate e modificate a causa dei diversi thread in esecuzione. Quindi, quando si genera il thread per le operazioni di immagine, verrà generato per una cella specifica no, diciamo c1. Ma, infine, quando carichi effettivamente l'immagine nella cella, diventerà la cella corrente che stai guardando, quella a cui hai fatto scorrere, ad esempio c2. Quindi, quando si scorre a c2, ci sono stati i thread c2 che sono stati generati, uno davanti a ciascuna cella, mentre si scorreva. Da quello che ho capito, tutti questi thread proveranno a caricare le loro immagini nella cella corrente, c2. Quindi, hai lampi di immagini.

Per evitare questo, è necessario verificare effettivamente che si sta caricando l'immagine che si desidera nella cella che si intende caricare. Quindi, ottieni la collectionviewcell indexpath.row prima di caricare l'immagine in essa (loading_image_into_cell). Inoltre, prendi la cella per la quale hai generato il thread prima di generare il thread, cioè nel thread principale (image_num_to_load). Ora, prima di caricare, controlla che questi due numeri siano uguali.

Problema risolto :)

Problemi correlati