использование NSInvocationOperation для UITableViewCell приводит к тому, что некоторые UIImageViews отображают неправильное изображение? Возможная проблема с повторным входом?

#ios #multithreading #uitableview #nsoperation

#iOS #многопоточность #uitableview #nsoperation

Вопрос:

Я пытаюсь обеспечить плавную UITableView прокрутку при просмотре около 700 изображений, которые загружены из Интернета, кэшированы (во внутреннюю память) и отображаются в каждой ячейке таблицы. Мой код пока выглядит нормально с точки зрения производительности прокрутки. Однако я заметил, что иногда, если соединение дрянное или если я прокручиваю очень быстро, ячейка будет отображать неправильное изображение (изображение другой ячейки), возможно, около 1/2 секунды, а затем обновится до изображения, которое она должна отображать.

Пока я подозреваю 2 вещи:

A- У меня может возникнуть проблема с повторным входом с момента, когда мои NSInvocationOperation вызовы возвращаются в основной поток с [self performSelectorOnMainThread:] до момента, когда выполняется селектор в главном потоке. Хотя на самом деле я не вижу никаких общих переменных.

B- Какая-то гонка между основным потоком и NSInvocationOperation ? Нравится:

1 вызовы основного потока cacheImageFromURL

2 внутри этого вызова UIImage охватывает рабочий поток

3 рабочий поток почти завершен и переходит к вызову performSelectorOnMainThread

4 на данный момент рассматриваемая ячейка исключена из очереди для повторного использования, поэтому основной поток снова вызывает cahceImageFromURL новое изображение.

5 внутри этого вызова UIImage останавливает, NSOPerationQueue что приводит к завершению предыдущего NSInvocationOperation потока.

6 НО поток уже вызвал performSelectorOnMainThread

7 таким образом, селектор возбуждается, вызывая загрузку старого изображения.

8 сразу после этого недавно созданный поток завершает выборку нового изображения и вызывает performSelectorOnMainThread снова, вызывая обновление до правильного изображения.

Если это так, я думаю, мне нужно было бы установить флаг при входе в cacheImageFromURL метод, чтобы код рабочего потока не вызывался performSelectorOnMainThread , если внутри уже есть другой поток (основной) cacheImageFromURL ?

Вот мой код для моего UIImageView подкласса, который использует каждая ячейка в таблице:

 @implementation UIImageSmartView

//----------------------------------------------------------------------------------------------------------------------
@synthesize defaultNotFoundImagePath;
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - init
//----------------------------------------------------------------------------------------------------------------------
- (void)dealloc
{ 
  if(!opQueue)
  {
    [opQueue cancelAllOperations];
    [opQueue release];
  }
  [super dealloc];
}
//----------------------------------------------------------------------------------------------------------------------
#pragma mark - functionality
//----------------------------------------------------------------------------------------------------------------------
- (bool)cacheImageFromURL:(NSString*)imageURL
{
  /*  If using for the first time, create the thread queue and keep it 
      around until the object goes out of scope*/
  if(!opQueue)
    opQueue = [[NSOperationQueue alloc] init];
  else
    [opQueue cancelAllOperations];

  NSString *imageName = [[imageURL pathComponents] lastObject];
  NSString* cachePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject];
  NSString *cachedImagePath = [cachePath stringByAppendingPathComponent:imageName];

  /*  If the image is already cached, load it from the local cache dir.
      Else span a thread and go get it from the internets.*/
  if([[NSFileManager defaultManager] fileExistsAtPath:cachedImagePath])
    [self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
  else
  {
    [self setImage:[UIImage imageWithContentsOfFile:self.defaultNotFoundImagePath]];
    NSMutableArray *payload = [NSMutableArray arrayWithObjects:imageURL, cachedImagePath, nil];

    /*  Dispatch thread*/
    concurrentOp = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(loadURI:) object:payload];
    [opQueue addOperation: concurrentOp];
    [concurrentOp release];
  }

  return YES;
}
//----------------------------------------------------------------------------------------------------------------------
/*  Thread code*/
-(void)loadURI:(id)package
{
  NSArray *payload = (NSArray*)package;
  NSString *imageURL = [payload objectAtIndex:0];  
  NSString *cachedImagePath = [payload objectAtIndex:2];

  /*  Try fetching the image from the internets.
      If we got it, write it to disk. If fail, set the path to the not found again.*/
  UIImage *newThumbnail = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:imageURL]]];  
  if(!newThumbnail)
    cachedImagePath = defaultNotFoundImagePath;
  else  
    [UIImagePNGRepresentation(newThumbnail) writeToFile:cachedImagePath atomically:YES];

  /*  Call to the main thread - load the image from the cache directory
      at this point it'll be the recently downloaded one or the NOT FOUND one.*/
  [self performSelectorOnMainThread:@selector(updateImage:) withObject:cachedImagePath waitUntilDone:NO];
}
//----------------------------------------------------------------------------------------------------------------------
- (void)updateImage:(NSString*)cachedImagePath
{
  [self setImage:[UIImage imageWithContentsOfFile:cachedImagePath]];
}
//----------------------------------------------------------------------------------------------------------------------
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
  // Return YES for supported orientations
  return (interfaceOrientation == UIInterfaceOrientationPortrait);
}

@end
  

И способ использования этого UIImage находится в контексте cellForRowAtIndexPath , вот так:

 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
  UIImageSmartView *cachedImage;
  // and some other stuff...

  static NSString *CellIdentifier = @"Cell";
  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
  if (cell == nil) 
  {
    cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
                                   reuseIdentifier:CellIdentifier] autorelease];

    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
    cell.selectionStyle = UITableViewCellSelectionStyleGray;

    // some labels and tags stuff..

    cachedImage = [[UIImageSmartView alloc] initWithFrame:CGRectMake(5, 5, 57, 80)];
    cachedImage.contentMode = UIViewContentModeCenter;
    cachedImage.defaultNotFoundImagePath = [[NSBundle mainBundle] pathForResource:@"DefaultNotFound" ofType:@"png"];
    cachedImage.tag = PHOTO_TAG;

    [cell.contentView addSubview:cachedImage];
    [cell.contentView addSubview:mainLabel];
    [cell.contentView addSubview:secondLabel];

  }
  else
  {
    cachedImage = (UIImageSmartView*)[cell.contentView viewWithTag:PHOTO_TAG];
    mainLabel = (UILabel*)[cell.contentView viewWithTag:MAINLABEL_TAG]; 
  }

  // Configure the cell...

  NSString *ImageName = [[[self.dbData objectAtIndex:indexPath.row] objectAtIndex:2] 
                         stringByReplacingOccurrencesOfString:@".jpg" 
                         withString:@"@57X80.png"];

  NSString *imageURL = [NSString stringWithFormat:@"www.aServerAddress.com/%@/thumbnail5780/%@", 
                        self.referencingTable, 
                        ImageName];

  [cachedImage cacheImageFromURL:imageURL];

  mainLabel.text = [[self.dbData objectAtIndex:indexPath.row] objectAtIndex:0]; 
  return cell;
}
  

Ответ №1:

Проблема заключается в повторном использовании ячейки, одна ячейка запрашивает различные изображения одновременно и отображается при загрузке каждого из них, я знаю, что вы отменяете очередь операций, но поскольку вызывающий объект обработки синхронен, операция продолжает выполнение. Я предлагаю попытаться сохранить indexPath запроса и сопоставить его с индексным путем ячейки, прежде чем устанавливать UIImage.

Комментарии:

1. пытаюсь понять ваше предложение. Вы имеете в виду, сохранить indexPath при вводе cacheImageFromURL: , затем передать его в NSInvocationOperation и попросить NSInvocation передать его, в свою очередь, обратно в код основного потока, чтобы при updateImage, если текущий путь к индексу не совпадает с переданным, не загружать «обновленное» изображение?

2. было бы лучше просто забыть весь NSOperationQueue и, возможно, вместо этого перейти к обычной NSOperation? значит, несколько запросов не могут встать в очередь и не отменяются вовремя?

3. Хммм, позвольте мне объяснить, попробуйте добавить свойство с именем indexPath к вашему объекту UIImageSmartView и устанавливать его каждый раз в cellForRowAtIndexPath, создайте пользовательское синхронное соединение NSRLConnection (подкласс) со свойством indexPath, и когда вы выполняете запрос (NSOperationInvocation), установите для него текущее свойство indexPath UIImageSmartView, таким образом, у вас будет уникальное соединение для каждого индекса. Перед установкой изображения проверьте, совпадает ли indexPath UIImageSmartView с indexPath пользовательского NSURLConnection.

Ответ №2:

D33pN16h7 прав в том, что проблема заключалась в повторном использовании ячейки. Однако, вместо того, чтобы пытаться сделать indexPath потокобезопасным с помощью NSURLConnection, я решил переопределить все это, переместив NSOperationQueue в код UITableViewController и сделав параллельный класс ImageView фактически надлежащим подклассом NSOperation (поскольку я использовал NSOperationInvocation в первую очередь, чтобы попытаться избежать полноценного подкласса NSOperation ).

Итак, теперь контроллер таблицы управляет своим собственным NSOperationQueue, операции являются подклассами NSOperation, и я могу отменить их из кода контроллера таблицы, когда представление таблицы прокручивается мимо них. И все работает быстро и красиво.