快速滚动时,异步加载的tableView单元格图像闪烁。

10

我正在使用异步块(Grand Central Dispatch)来加载我的单元格图像。但是,如果您快速滚动,则它们仍然会非常快地出现,直到加载正确的图像为止。我确定这是一个常见的问题,但我似乎找不到解决办法。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];


// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];

    dispatch_async(dispatch_get_main_queue(), ^{

        UIImage *offersImage = [UIImage imageWithData:data];

        cell.imageView.image = offersImage;
    });
});

cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];

return cell;
}

请查看以下链接:https://dev59.com/52Ik5IYBdhLWcg3wYtP7#19327472 或者 https://dev59.com/zmQn5IYBdhLWcg3wpomp#16663759 - Rob
请考虑我的帖子https://dev59.com/U2Uo5IYBdhLWcg3w7S-T#26147814。谢谢! - Thomás Pereira
我建议阅读这篇博客文章http://www.natashatherobot.com/how-to-download-images-asynchronously-and-make-your-uitableview-scroll-fast-in-ios/。 - pprochazka72
4个回答

18

至少,在执行 dispatch_async 之前,您可能希望从单元格中删除图像(如果该单元格是重复使用的):

cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];

或者

cell.imageView.image = nil;

在更新之前,您还需要确保所涉及的单元格仍然在屏幕上(通过使用UITableView方法cellForRowAtIndexPath:,如果该行的单元格不再可见,则返回 nil ,请勿与UITableViewDataDelegate方法tableView:cellForRowAtIndexPath:混淆),例如:

static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

cell.imageView.image = [UIImage imageNamed:@"placeholder.png"];

// Load the image with an GCD block executed in another thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];

    if (data) {
        UIImage *offersImage = [UIImage imageWithData:data];
        if (offersImage) {
            dispatch_async(dispatch_get_main_queue(), ^{
                UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];

                if (updateCell) {
                    updateCell.imageView.image = offersImage;
                }
            });
        }
    }
});

cell.textLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] title];
cell.detailTextLabel.text = [[[appDelegate offersFeeds] objectAtIndex:indexPath.row] subtitle];

return cell;

坦率地说,你还应该使用缓存来避免不必要地重新获取图像(例如,当你向下滚动一点并向上滚动时,你不希望为那些先前的单元格的图像发出网络请求)。更好的做法是使用其中之一的UIImageView类别(例如在SDWebImageAFNetworking中包含的类别)。这实现了异步图像加载,但也优雅地处理了缓存(不要重新检索刚刚检索过的图像),取消尚未发生的图像(例如,如果用户快速滚动到第100行,则可能不希望在向用户显示第100行的图像之前等待前99个被检索)。

还有一个潜在的问题,即在异步调用完成之前,单元格可能会被重复使用,导致正确的图像被替换为先前调用的图像。为表视图单元格异步加载图像并不是一件简单的事情。 - kubi
@kubi 同意,但我的代码示例通过检查 updateCell 来解决这个问题,以确保单元格没有被重用。但你说得对,这是非常棘手的问题,我对即时问题的战术修复并不能解决其他各种问题(缓存、积压请求等),因此我建议使用其中一个 UIImageView 类别,这将使它变得 容易。 - Rob
@Rob,如果您能看一下以下问题就好了:https://dev59.com/-JDea4cB1Zd3GeqPdIxa - casillas
@Rob,我真的很好奇为什么使用UIImageView类别之一会解决所提到的问题。据我所知,这些库所做的所有事情都是使请求/缓存变得更容易。然而,它们不会解决像单元格重用、更改indexPaths(例如通过在加载表时动态添加/删除单元格)和积压请求等问题,对吗? - Mamouneyya
@Rob 如果你有空的话,能否请在这里回答我的问题:https://dev59.com/sJLea4cB1Zd3GeqP0UzV 因为那里的第一条评论陈述了不同的事实。如果你能澄清一下,我会非常感激,因为这将有助于社区中的任何人。 - Mamouneyya
显示剩余2条评论

2
这是异步加载图片的一个问题... 假设您在任何给定时间都有5个可见行。如果您快速滚动,并向下滚动10行,tableView:cellForRowAtIndexPath将被调用10次。问题在于这些调用比图像返回的速度更快,并且您有来自不同URL的10个挂起的图像。当图像最终从服务器返回时,它们将返回到您放置在异步加载器中的那些单元格上。由于您仅重用5个单元格,因此其中一些图像将在每个单元格上显示两次,因为它们从服务器下载,这就是您看到闪烁的原因。还要记得在调用异步下载方法之前调用cell.imageView.image = nil,因为重用单元格的先前图像将保留并在分配新图像时也会导致闪烁效果。
解决方法是存储您需要在每个单元格上显示的最新图像的URL,然后当图像从服务器返回时,检查该URL与您请求的URL是否相同。如果不同,请将该图像缓存以供稍后使用。有关缓存请求,请查看NSURLRequestNSURLConnection类。
我强烈建议您使用AFNetworking进行任何服务器通信。
祝你好运!

2
您的闪烁问题是因为在滚动期间同时启动了多个图像下载,每当屏幕上显示一个单元格时,就会执行新的请求,并且可能旧的请求尚未完成,每当一个请求完成时,图像就设置在单元格上,因此如果您快速滚动,则使用同一单元格3次= 3个请求将被触发= 3个图像将设置在该单元格上= 闪烁。
我曾经遇到过相同的问题,这是我的解决方法: 创建一个自定义单元格并包含所有所需视图。每个单元格都有自己的下载操作。在单元格的-prepareForReuse方法中,我会使图像为空并取消请求。
通过这种方式,对于每个单元格,我只有一个请求操作=一个图像=没有闪烁。
即使使用AFNetworking,如果不取消图像下载,也可能出现相同的问题。

我只是设置了ImageView和Image为nil,就完成了任务! - Elsammak

0
问题是您在主线程上下载图像。将其调度到后台队列进行处理:
dispatch_queue_t myQueue = dispatch_queue_create("myQueue", NULL);

    // execute a task on that queue asynchronously
    dispatch_async(myQueue, ^{
      NSData *data = [NSData dataWithContentsOfURL:[NSURL URLWithString:[[[appDelegate offersFeeds] objectAtIndex:indexPath.row] imageurl]]];
//UI updates should remain on the main thread
UIImage *offersImage = [UIImage imageWithData:data];
    dispatch_async(dispatch_get_main_queue(), ^{

        UIImage *offersImage = [UIImage imageWithData:data];

        cell.imageView.image = offersImage;
    });
    });

user2920762没有在主线程上下载图像。他正在使用全局(即后台)队列进行操作(这并不是很好,但比主队列要好)。他只是没有正确处理单元格的重用(也没有正确处理单元格可能已经滚动出屏幕的情况,而异步检索仍在进行中)。 - Rob
在他的代码中,这是在主线程上执行的:UIImage *offersImage = [UIImage imageWithData:data]; - Nikos M.
同意。 (顺便说一句,您实际上在后台队列和主队列中都使用了“imageWithData”一次,我相信这不是您的本意。)然而,主要问题是在后台队列检索需要很长时间,因此他在下载新图像时看到旧图像(并且没有处理单元格在图像下载期间可能已被重用的事实)。 因此,您是正确的,应该在后台队列中执行“imageWithData”,但这并不能解决问题。 问题是在调度之前未初始化“cell.imageView.image”。 - Rob

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接