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

时间:2012-07-23 09:52:27

标签: ios multithreading uitableview uiimageview lazy-loading

我实现了一个UITmageView UITableView单元,每个单元通过NSTimer每隔5秒定期刷新一次。每个图像都是从后台线程中的服务器加载的,并且从后台线程我也通过调用performSelectorOnMainThread来更新UI,显示新图像。到目前为止一切都很好。

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

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

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

我想知道这可能不是最好的做法,因此,寻求你的意见。

(我也不能使用SDWebImage,因为我的要求是在从服务器加载的循环中显示一组图像)

// 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) {
    }];
}

2 个答案:

答案 0 :(得分:1)

好的... 另一种方法是将“图像请求代码”放入您的AnimatedImageView中,并在每次设置新的图像URL时使待处理请求无效

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

然后,在你的Cell类中,'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...

答案 1 :(得分:0)

在CollectionView&amp ;;中加载许多图像时,我遇到了完全相同的问题。带有重用单元格的TableView通过快速和长时间滚动覆盖所有下载的图像。

对于TableView,我解决了它看WWDC 2012,其中提出了'didEndDisplayingCell'方法,但这对没有这种方法的CollectionView没有用。 :O(

最后,我找到了两个解决方案......幸运的是。 我们的想法是创建由TableView或CollectionView中的单元格调用的类 ImageLoader ,并且负责下载图像。 在您的实现中,添加此代码段以将变量隐藏到世界其他地方:

@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 /。将初始创建的块添加到专用于此特定重用单元的队列中。

重要的是如果成员变量不是nil,则调用'invalidateAndCancel'函数,因为每次要重用单元格时,前一个会话都不会将最后一个图像带回去,而是由传入参数'url'。

之后,不要忘记将1 /。下面的代码段放在TableView / CollectionView单元类中,以便每个重用单元只使用一个imageLoader 和2 /。该方法是在CollectionView / TableView的方法'tableView:cellForRowAtIndexPath:'中调用:

@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

完成后,只需在CollectionView / TableView的方法'tableView:cellForRowAtIndexPath:'中调用[cell imageURL:url]

现在,您可以根据需要快速滚动,并且您将始终拥有正确的图像,而且只有这一图像。

我希望它会有所帮助...... :O)