带有图像的UITableView滚动非常缓慢。

4

可能是重复问题:
带图片的表视图,加载和滚动缓慢

我有一个UITableView,它从服务器下载UITableViewCells的图片。我发现这个tableView滚动得非常慢。

我以为这可能是下载问题,但我已经意识到,在下载完成后,即使图标大小较小,表格仍然滚动缓慢。

我在谷歌上搜索了一下,但没有找到任何帮助。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    btnBack.hidden = FALSE;

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil)
    {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        cell.backgroundColor = [UIColor clearColor];

        cell.textLabel.font = [UIFont fontWithName:@"Noteworthy" size:17.0];
        cell.textLabel.font = [UIFont boldSystemFontOfSize:17.0];
        cell.textLabel.textColor = [UIColor blackColor];
        cell.textLabel.highlightedTextColor = [UIColor blackColor];
    }

        cell.textLabel.text = [NSString stringWithFormat:@"     %@", [test.arrTitle objectAtIndex:indexPath.row]];

        NSString *Path;
        Path = [NSString stringWithFormat:@"http://%@",[test.arrImages objectAtIndex:indexPath.row]];
        NSLog(@"image-->%@",[test.arrImages objectAtIndex:indexPath.row]);
        NSString *strImage = Path;
        NSURL *url4Image = [NSURL URLWithString:strImage];    
        NSData *data = [NSData dataWithContentsOfURL:url4Image];
        image =[[UIImage alloc] initWithData:data];
        cell.imageView.image =image;
        [image release];

        return cell;
}

请看这里:https://dev59.com/12s05IYBdhLWcg3wAdWT#7502281 - HeikoG
6个回答

18
虽然下面我的原始回答试图解决与异步图像检索相关的几个关键问题,但它仍然基本上有限制。适当的实现还将确保如果您快速滚动,则可见单元格优先于已滚动离屏幕的单元格。它还将支持取消先前的请求(并在适当时为您取消它们)。
虽然我们可以向下面的代码添加这些功能,但最好采用一个已经建立和证明的解决方案,该解决方案利用下面讨论的NSOperationQueue和NSCache技术,但也解决了上述问题。最简单的解决方案是采用支持异步图像检索的已建立UIImageView类别之一。AFNetworkingSDWebImage库都有UIImageView类别,可以优雅地处理所有这些问题。
您可以使用NSOperationQueueGCD来实现懒加载(有关不同异步操作技术的讨论,请参见Concurrency Programming Guide)。前者的优势在于您可以精确指定允许的并发操作数量,这在从Web中加载图像时非常重要,因为许多Web服务器限制了它们将从给定客户端接受的并发请求数量。
基本思路是:
  1. 在单独的后台队列中提交图像数据请求;
  2. 下载完成后,将 UI 更新调度回主队列,因为不应该在后台进行 UI 更新;
  3. 在主队列上运行分派的最终 UI 更新代码时,请确保 UITableViewCell 仍然可见,并且它没有被取消排队并重新使用,因为涉及的单元格已滚动出屏幕。如果不这样做,则错误的图像可能会短暂地显示出来。

您需要用以下代码替换您的代码:

首先,为您将用于下载图像的 NSOperationQueue 定义一个属性,以及用于存储这些图像的 NSCache

@property (nonatomic, strong) NSOperationQueue *imageDownloadingQueue;
@property (nonatomic, strong) NSCache *imageCache;

其次,在viewDidLoad中初始化此队列和缓存:

- (void)viewDidLoad
{
    [super viewDidLoad];

    self.imageDownloadingQueue = [[NSOperationQueue alloc] init];
    self.imageDownloadingQueue.maxConcurrentOperationCount = 4; // many servers limit how many concurrent requests they'll accept from a device, so make sure to set this accordingly

    self.imageCache = [[NSCache alloc] init];

    // the rest of your viewDidLoad
}

第三步,您的cellForRowAtIndexPath可能如下所示:

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    btnBack.hidden = FALSE;

    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    if (cell == nil) {
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
        cell.selectionStyle = UITableViewCellSelectionStyleNone;

        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
        cell.backgroundColor = [UIColor clearColor];

        cell.textLabel.font = [UIFont fontWithName:@"Noteworthy" size:17.0];
        cell.textLabel.font = [UIFont boldSystemFontOfSize:17.0];
        cell.textLabel.textColor = [UIColor blackColor];
        cell.textLabel.highlightedTextColor = [UIColor blackColor];
    }

    cell.textLabel.text = [NSString stringWithFormat:@"     %@", [test.arrTitle objectAtIndex:indexPath.row]];

    // code change starts here ... initialize image and then do image loading in background

    NSString *imageUrlString = [NSString stringWithFormat:@"http://%@", [test.arrImages objectAtIndex:indexPath.row]];
    UIImage *cachedImage = [self.imageCache objectForKey:imageUrlString];
    if (cachedImage) {
        cell.imageView.image = cachedImage;
    } else {
        // you'll want to initialize the image with some blank image as a placeholder

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

        // now download in the image in the background

        [self.imageDownloadingQueue addOperationWithBlock:^{

            NSURL *imageUrl   = [NSURL URLWithString:imageUrlString];    
            NSData *imageData = [NSData dataWithContentsOfURL:imageUrl];
            UIImage *image    = nil;
            if (imageData) 
                image = [UIImage imageWithData:imageData];

            if (image) {
                // add the image to your cache

                [self.imageCache setObject:image forKey:imageUrlString];

                // finally, update the user interface in the main queue

                [[NSOperationQueue mainQueue] addOperationWithBlock:^{
                    // Make sure the cell is still visible

                    // Note, by using the same `indexPath`, this makes a fundamental
                    // assumption that you did not insert any rows in the intervening
                    // time. If this is not a valid assumption, make sure you go back
                    // to your model to identify the correct `indexPath`/`updateCell`

                    UITableViewCell *updateCell = [tableView cellForRowAtIndexPath:indexPath];
                    if (updateCell)
                        updateCell.imageView.image = image;
                }];
            }
        }];
    }

    return cell;
}

最后,当我们需要在低内存情况下清除缓存时,我们可能会倾向于编写代码来实现这一点。但事实证明,NSCache会自动清除缓存,因此这里不需要额外的处理。如果您在模拟器中手动模拟低内存情况,则不会看到它驱逐其对象,因为NSCache不响应UIApplicationDidReceiveMemoryWarningNotification,但在实际操作中,当内存不足时,缓存将被清除。 实际上,NSCache不再优雅地响应低内存情况,因此您确实应该添加观察者以便接收此通知,并在低内存情况下清空缓存。

我可能会建议其他一些优化(例如,也许将图像缓存到持久存储中以简化未来操作;我实际上将所有这些逻辑放在了自己的AsyncImage类中),但首先请查看是否解决了基本性能问题。


感谢Rob提供的代码,但是当我滚动时,我的图片会从一个单元格跳到另一个单元格。 - anil kumar
嗨,Rob!首先非常出色的代码编写。我使用了你的代码,但有一个小问题,我的图像和其他数据来自 JSON 文件,文本数据完美地显示出来,但是图像需要很长时间才能显示出来,大部分时间图片无法加载。为什么会这样呢?我的意思是我卡在这里,不知道为什么会发生这样的情况...希望您能迅速回复。谢谢! - Deepak Khiwani
谢谢兄弟,它运行得非常棒。 - SampathKumar
你的代码中UIImage对象的图像缓存会得到什么值? - sandeep tomar
因为我试图在 imagecache 中获取 URL 时得到了 null。 - sandeep tomar
无论您在缓存中保存图像时使用什么键,都应该与检索它的键相同。如果您得到了“nil”,请确保您的缓存本身不是“nil”。如果您仍然遇到问题,请发布您自己的问题,并附上代码示例和对您所做的描述,因为仅在此处进行评论将很难诊断。 - Rob

1

在你的UITableViewcellForRowAtIndex:方法中写入以下内容:

asyncImageView = [[AsyncImageView alloc]initWithFrame:CGRectMake(30,32,100, 100)];         
[asyncImageView loadImageFromURL:[NSURL URLWithString:your url]];
[cell addSubview:asyncImageView];
[asyncImageView release];

需要导入 AsyncImageView 类 并创建该类的对象


这似乎不是AsyncImageView的良好应用。在图像下载完成后,应该检查表格视图中正在处理的单元格是否仍然在屏幕上(例如,[self.tableview cellForRowAtIndexPath:indexPath] != nil),以确保正确异步加载图像。如果您快速翻转表格视图(特别是如果图像很大),则可以通过重新排队和重用单元格来完成图像下载。 - Rob

0

建议您将图像存储在数组中,并在viewDidLoad中填充它们,然后在cellForRowAtIndexPath:中仅设置即可。

cell.imageView.image = [yourImageArray objectAtIndex:indexPath.row];

就速度而言,这是因为您在cellForRowAtIndexPath方法中阻塞了主线程以下载URLDATA,因此在滚动时,除非图像被获取,否则应用程序运行的主线程将被阻塞。

我理解这种方法的直观吸引力,但是虽然它解决了慢滚动的问题,却替换成了更糟糕的问题,即表格视图的初始加载非常缓慢。这也是一种相当奢华的内存使用方式。通常,出于最佳性能和内存利用率的原因,我们大多数人都采用异步延迟加载。也许你可以通过异步预加载某些智能子集的图像缓存(例如下一组图像)来完善它,但这并不容易,并且在那之前我会追求一堆其他的缓存和优化措施。 - Rob

0

滚动非常缓慢,因为您正在主线程即同步加载图像。 您可以在后台线程即异步执行相同操作,请查看SDWebImage


谢谢您的快速回复。我是iPhone编程的新手。我尝试了这个方法,但是在添加头文件时,我的Xcode没有显示“其他链接器标志”设置,并且无法添加“-ObjC”标志。 - anil kumar

0

0
你应该使用NSOperationQueue来处理图片的懒加载和自定义tableviewcell。你可以在这里找到NSOperationQueue的示例。
Google搜索tweetie custom tableviewcell,那应该指引你正确的方向。
苹果有一个在tableViews中下载图片的示例项目:LazyTableImages

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