滚动时在错误的UITableViewCell上加载图像

时间:2011-12-03 14:41:29

标签: objective-c ios uitableview uiscrollview lazy-loading

我正在加载一个特定单元格的图像集,只有当我等到所有图像都被加载时它才能正常工作,我正在使用线程从Web上获取图像时问题是当我在加载时滚动表格视图进程仍然在进行中它会在错误的单元格位置生成图像。我知道这是错误的位置,因为我还为每个图像集设置了标签。任何帮助将不胜感激提前!!!!

以下是示例代码。

- (void)viewDidLoad{
[super viewDidLoad];
/* loads xml from the web and parses the images and store it in core data(sqlite)
   it uses NSFetchedController protocol for inserting cell after saving into core data
*/
[self loadFromXmlSource];
}

我有3个部分在故事板中使用3个不同的原型单元,我遇到的问题是照片部分。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath         *)indexPath
{
Media *mediaModel = [_fetchedResultsController objectAtIndexPath:indexPath];
NSString *CellIdentifier = @"MediaCellNoImage";
if ([[mediaModel.category lowercaseString] isEqualToString:@"photos"]) {
    CellIdentifier = @"MediaGalleryCell";
} else if ([[mediaModel.category lowercaseString] isEqualToString:@"videos"]) {
    CellIdentifier = @"MediaCellNoImage";
} else if ([[mediaModel.category lowercaseString] isEqualToString:@"podcasts"]) {
    CellIdentifier = @"MediaCell";
}

MediaGalleryCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
    cell = [[MediaGalleryCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
// I am also having a problem with image duplication so I always delete all images
if([cell.reuseIdentifier isEqualToString:@"MediaGalleryCell"]) {
    for(id subview in cell.scrollView.subviews){
        [subview removeFromSuperview];
    }
}

[self configureCell:cell atIndexPath:indexPath];
return cell;
}

这里是配置单元格,我在其中生成一个用于图像下载的线程

-(void)configureCell:(MediaGalleryCell *)cell atIndexPath:(NSIndexPath *)indexPath
{

Media *mediaModel = [_fetchedResultsController objectAtIndexPath:indexPath];
if ([[mediaModel.category lowercaseString] isEqualToString:@"photos"]) {
        NSEnumerator *e = [mediaModel.attachments objectEnumerator];
        Attachment *attachment;
        // iterate all images in a particular cell
        while ((attachment = [e nextObject])) {
                NSDictionary *objects = [[NSMutableDictionary alloc] initWithObjectsAndKeys:

                                         attachment.identifier, @"identifier",
                                         attachment.thumb, @"thumb",
                                         attachment.filename, @"filename",
                                         cell, @"cell", 
                                         nil];

                // for each image spawn a thread i am also passing the cell in order to set it there.
                [NSThread detachNewThreadSelector:@selector(loadImageInScrollView:)
                                         toTarget:self withObject:objects];
        }   
} else {
    // code for other section
}


cell.titleLabel.text = mediaModel.title;
cell.contentLabel.text = mediaModel.content;
}

这是线程中的方法

-(void)loadImageInScrollView:(NSDictionary *)objects
{
MediaGalleryCell *cell = [objects valueForKey:@"cell"];
SBCacheFile *cacheFile = [self getCache];
NSString *finalIdentifier = [NSString stringWithFormat:@"%@_s", [objects valueForKey:@"identifier"]];

// here is where I fetched the image and cache it locally
UIImage *image = [cacheFile getCachedFileWithUrl:[objects valueForKey:@"thumb"]
                                        withName:[objects valueForKey:@"filename"]
                                        andStringIdentifier:finalIdentifier];

NSDictionary *obj = [[NSDictionary alloc] initWithObjectsAndKeys:
                     cell, @"cell",
                     image, @"image",
                     nil];
//after fetching the image I need to send it back to the main thread in order to do some UI operation
[self performSelectorOnMainThread:@selector(setImageViewForGallery:) withObject:obj waitUntilDone:YES];

}

这是最后一段代码,我将图像添加为单元格属性的子视图,这是一个scrollView,它就像单元格中的图像轮播一样。

-(void)setImageViewForGallery:(NSDictionary *)dictionary
{
MediaGalleryCell *cell = [dictionary valueForKey:@"cell"];
UIImage *image = [dictionary valueForKey:@"image"];

UIImageView *imageView = [[UIImageView alloc] initWithImage:image];

UIImageView *lastImageView =[[cell.scrollView subviews] lastObject];
if (lastImageView != nil) {
    [imageView setFrame:CGRectMake((lastImageView.frame.origin.x + lastImageView.frame.size.width + 10.0f), 
                                   5.0f, 70.0f, 70.0f)];
} else {
    [imageView setFrame:CGRectMake(5.0f, 5.0f, 70.0f, 70.0f)];
}

 // This is where I add the imageView
 [cell.scrollView addSubview:imageView];

 UIImageView *lastViewForSize = [[cell.scrollView subviews] lastObject];
    [cell.scrollView setContentSize:CGSizeMake(lastViewForSize.frame.origin.x + lastViewForSize.frame.size.width + 0.5,cell.scrollView.frame.size.height)]; 
}

2 个答案:

答案 0 :(得分:3)

首先,这非常危险:

// for each image spawn a thread i am also passing the cell in order to set it there.
[NSThread detachNewThreadSelector:@selector(loadImageInScrollView:)
                         toTarget:self withObject:objects];

这可以分叉一个巨大的(并且无限制的)线程数。可能发生的情况是,当您滚动时,线程会为已经重用的单元格完成,因此会踩踏它们的新数据。

你应该在这里做几件事来改善事情。首先,您很少想使用detachNewThreadSelector:toTarget:withObject:。相反,请使用NSOperationQueue,GCD队列或仅使用一名工作人员NSThreadperformSelectorOnThread:...

接下来,您混合了模型和视图逻辑。您当前的方式是在视图(单元格)内存储模型数据(图像列表)。重复使用单元格时,这不起作用。视图不用于存储数据。

你应该有一个能够跟踪所有图像及其去向的模型。在你的情况下,这可能只是一个数组(行/图像)数组。表视图数据源将始终返回一个单元格,其中包含数组相应行的当前内容。

每当模型更改时(因为下载了新图像),您的表视图委托应该调用reloadRowsAtIndexPaths:withRowAnimation:让表视图知道该行已更改。如果恰好需要该行,那么(并且只有那时)表视图将向数据源请求新单元格,您将使用新图像更新该单元格。这种方法更清洁,更稳定,更快速,并且使用更少的系统资源。

答案 1 :(得分:1)

这是 UITableView 的经典陷阱。将单元格滚出视图时,将重复使用该单元格以显示不同的数据。它基本上弹出顶部,将填充新数据并重新出现在底部。

但是,您的后台进程不会收到通知。因此,当图像最终加载时,将其附加到已被重复使用的表格单元格。

因此,要修复它,您需要取消图像加载,如果表格单元格不再可见,或者至少通知线程表格单元格已被重用,并且一旦它不再可能分配图像装了。当单元格滚出视图或重复使用单元格以显示不同的数据时,可能会发生这种情况。

在我的应用中,我正在使用中央图像缓存。它最多使用四个线程来加载图像。它保留了等待加载图像的所有表格单元格的列表。如果其中一个表格单元格出现在不同的图像请求中,则先前的请求会被修改,因此它不会更新表格单元格,而只是将图像放入中央缓存区。