使用NSInvocationOperation为UITableViewCell导致一些UIImageViews显示错误的图像?可能的重入问题?

时间:2011-11-11 05:15:58

标签: ios multithreading uitableview nsoperation

我正在尝试保持UITableView平滑滚动,同时浏览从互联网下载的约700张图片,缓存(到内部存储)并显示在表格的每个单元格上。到目前为止,我的代码在滚动性能方面似乎很好。但是,我注意到有时候,如果连接很糟糕或者如果我滚动得非常快,一个单元格会显示错误的图片(另一个单元格的图片)大约1/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;
}

2 个答案:

答案 0 :(得分:1)

问题是重复使用单元格,一个单元格同时发出各种图像的请求,并且在每个单元格下载时显示,我知道您正在取消操作队列但是当处理调用方是同步时,操作继续执行。我建议尝试保存请求的indexPath,并在设置UIImage之前将其与单元格的索引路径匹配。

答案 1 :(得分:1)

D33pN16h7是正确的,问题是细胞重用。但是,我没有尝试通过NSURLConnection使indexPath成为线程安全的,而是决定通过将NSOperationQueue移动到UITableViewController代码并使并发的imageView类实际上是NSOperation的正确子类来重新实现整个事情(因为我使用的是NSOperationInvocation)首先尝试避免使用完整的NSOperation子类。

现在,表控制器管理它自己的NSOperationQueue,操作是NSOperation的子类,我可以在表视图滚过它们时从表控制器代码中取消它们。一切都很快,很好。