iOS:如何在主线程中更新UI的后台线程?

问题描述:

我实现了UIImageView单元的UITableView,每个单元通过NSTimer每隔5秒定期刷新一次。每个图像在后台线程中从服务器加载,并且从后台线程中,我还通过调用performSelectorOnMainThread来更新UI,显示新图像。到现在为止还挺好。iOS:如何在主线程中更新UI的后台线程?

当我不等到一个图像加载到当前单元格并且我快速向下滚动到一个新的单元格时,我目前遇到了一个问题。然而,这个新单元显示的是前一个单元的图像,而不是新单元的图像。虽然下一轮的NSTimer会正确显示图像,但这可能会首先让用户感到困惑。

如果我不重用UITableView的单元格,问题就会消失,但如果在我的应用程序中显示单元格的数量,这不是一个选项。

所以我能想到的唯一解决方案是取消(或杀死)后台线程,如果我知道用户执行滚动操作,将显示旧图像。

我想知道这可能不是最佳实践,因此,寻求您的建议。

// In MyViewController.m 
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{ 
    ... 
    NSTimer* timer=[NSTimer scheduledTimerWithTimeInterval:ANIMATION_SCHEDULED_AT_TIME_INTERVAL 
               target:self 
               selector:@selector(updateImageInBackground:) 
               userInfo:cell.imageView 
               repeats:YES]; 
    ... 
} 

- (void) updateImageInBackground:(NSTimer*)aTimer 
{ 
    [self performSelectorInBackground:@selector(updateImage:) 
         withObject:[aTimer userInfo]]; 
} 

- (void) updateImage:(AnimatedImageView*)animatedImageView 
{  
    @autoreleasepool { 
     [animatedImageView refresh]; 
    } 
} 

// In AnimatedImageView.m 
-(void)refresh 
{ 
    if(self.currentIndex>=self.urls.count) 
     self.currentIndex=0; 

    ASIHTTPRequest *request=[[ASIHTTPRequest alloc] initWithURL:[self.urls objectAtIndex:self.currentIndex]]; 
    [request startSynchronous]; 

    UIImage *image = [UIImage imageWithData:[request responseData]]; 

    // How do I cancel this operation if I know that a user performs a scrolling action, therefore departing from this cell. 
    [self performSelectorOnMainThread:@selector(performTransition:) 
         withObject:image 
        waitUntilDone:YES]; 
} 

-(void)performTransition:(UIImage*)anImage 
{ 
    [UIView transitionWithView:self duration:1.0 options:(UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction) animations:^{ 
     self.image=anImage; 
     currentIndex++; 
    } completion:^(BOOL finished) { 
    }]; 
} 
+1

WWDC 2012会话211详细介绍了这一点,其中包括不同级别的队列取消,优化等。这是一个很棒的视频。 'cellForRow'中的 – jrturton 2012-07-23 09:54:58

+0

,为什么不立即设置图像为零?这样,它会暂时空白,而不是错误的图像。 – user102008 2012-07-23 17:55:35

+0

@ user102008我这样做了,但它不起作用。错误的图像已经在主线程中排队(刷新处于执行过程中),等待显示。 – 2012-07-23 22:05:49

好(我,因为我的要求就是显示一组从服务器中加载循环图片也不能使用SDWebImage)... 另一种方式来做到这将是把您的“形象请求代码”到你的AnimatedImageView,并失去每次设定一个新的图像URL时挂起的请求

// your controller 
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
    { 
    ... 
    [cell.imageView setDistantImage:imageURL]; 
    ...  
    } 

然后,在你的手机类,“setDistantImage”创建ASIHttpRequest和无效前一个

//your ImageView class 

@property (...) ASIHttpRequest *distantImageRequest; 

- (void) setDistantImageUrl:(NSUrl *)imageUrl 
{ 
    //clear previous request 
    [distantImageRequest clearDelegatesAndCancel]; 
    //set a new request, with the callBack embedded directly in this ImageView class 
    ... 
} 

//animation and callBacks methods in this ImageView class too... 

我的CollectionView & TableView中有重用的小区覆盖所有具有快速和长期滚动下载的图像加载许多图像时有完全相同的问题。

对于TableView,我解决了它在'didEndDisplayingCell'方法出现的地方看WWDC 2012,但是这对于没有这种方法的CollectionView来说并不是有用的。 :O(

最后,我发现了两个......幸运的是解决方案 的想法是创建ImageLoader的从TableView中或的CollectionView和细胞叫,这将是负责。下载图像 在您的实现,添加这段代码隐藏变量到世界其他地区:

@interface ImageLoader() { 
__block NSURLSession *_session; 
} 

然后,添加下列功能的实现:

- (void)getViewForCell:(UIImageView*)cell 
       FromURL:(NSURL*)url { 

NSBlockOperation *_loadImageOp; 
__block NSOperationQueue *_opQueue; 

_opQueue = [[NSOperationQueue alloc]init]; 

if (_session != nil) { 
    [_session invalidateAndCancel]; 
} 

_loadImageOp = [[NSBlockOperation alloc]init]; 
[_loadImageOp addExecutionBlock:^{ 

    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; 
    _session = [NSURLSession sessionWithConfiguration:config 
              delegate:nil 
             delegateQueue:_opQueue]; 

    NSURLSessionDownloadTask *downloadTask = [_session downloadTaskWithURL:url 
                 completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) { 
     UIImage *imageCell = [UIImage imageWithData:[NSData dataWithContentsOfURL:location]]; 

     if (imageCell) { 
      [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
       cell.image = imageCell; 
      }]; 
     } else { 
      [[NSOperationQueue mainQueue] addOperationWithBlock:^{ 
       cell.image = [UIImage imageNamed:@"imageNotWorking.png"]; 
      }]; 
     } 
    }]; 

    [downloadTask resume]; 
}]; 

[_opQueue addOperation:_loadImageOp]; 
} 

这个想法是1 /。您创建一个块,其中成员变量_session将通过downloadTask,2 /获取传入参数中给定的url。在下载时通过mainQueue将图像显示到用户界面... 3 /。最初创建的块被添加到专用于该特定重用单元的队列中。

重要的是'invalidateAndCancel'函数被调用,如果成员变量不是零,因为每次你想重用单元格时,前一个会话将不会返回最后一个图像,而是新定义的传入参数'url'。

然后,不要忘记在您的TableView/CollectionView单元类中放置1 /。以下代码片段,以便为每个重复单元使用一个imageLoader和2 /要在方法中调用的方法'的tableView:的cellForRowAtIndexPath:' 你的CollectionView的/ TableView中:

@interface TableCell() { //...or CollectionCell :o) 
ImageLoader *_imgLoader; 
} 
@end 

@implementation TableCell 
- (instancetype)initWithCoder:(NSCoder *)aDecoder { 
self = [super initWithCoder:aDecoder]; 
_imgLoader = [[ImageLoader alloc]init]; 
return self; 
} 

- (void)imageURL:(NSURL*)url { 
[_imgLoader getViewForCell:self.imageViewWhereTheDownloadedImageIs 
        FromURL:url]; 
} 
@end 

一旦完成,只需调用该方法[cell imageURL:url] '的tableView:的cellForRowAtIndexPath:' 你的CollectionView/TableView中的。

现在,您可以尽可能快地滚动,并且始终具有适当的图像,而且只有这一个。

我希望它能帮助...很好。 :o)