【问题标题】:Table View Scrolling Async表视图滚动异步
【发布时间】:2012-07-14 19:35:48
【问题描述】:

我正在将图像加载到表格视图单元格中,每个单元格都有一个图像。我已经为下面的代码调整了几个教程,但我的速度仍然很慢。

我正在从文档目录加载这些图像。有关如何加快此过程的任何提示或想法?

编辑修改后的代码:

Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;

// did we already cache a copy of the image?
if (beer.image != nil) {
    // good.  use it.  this will run quick and this will run most of the time
    cell.beerImage.image = beer.image;
} else {
    // it must be the first time we've scrolled by this beer.  do the expensive
    // image init off the main thread

    cell.beerImage.image  = nil;   // set a default value here.  nil is good enough for now

    [self loadImageForBeer:beer atIndexPath:indexPath];
}
- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{

        UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
        beer.image = image;

        dispatch_sync(dispatch_get_main_queue(), ^{
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.beerImage.image = image;
        });
    });
}

【问题讨论】:

标签: iphone objective-c ios


【解决方案1】:

你的算法看起来不错。您已经避免了许多典型的陷阱。如果您仍然遇到 UI 性能问题,我建议您做几件事:

  1. 您应该尝试将图像缓存在内存中。您可以使用NSMutableArrayNSMutableDictionary,但在Best way to cache images on ios app? Caleb 讨论了NSCache 类的优点,它简化了流程。如果您缓存图像,请确保响应内存压力并在必要时清除缓存。您可以回复didReceiveMemoryWarning或将自己添加为通知中心的UIApplicationDidReceiveMemoryWarningNotification的观察者。

  2. 确保您的缓存图像是缩略图大小,否则您的 UI 中总会出现一些卡顿(如果您需要调整大小算法,请告诉我们),这会不必要地占用内存;

  3. 当您将图像更新分派回主队列时,您应该异步执行此操作(为什么后台队列在等待块被发送回主队列时会挂起并占用资源完成......一旦您在快速滚动期间备份了几个图像,这尤其是一个问题);和

  4. 1234563从理论上讲,您可以让有问题的单元格滚出屏幕,您的算法可能会崩溃)。

我使用的算法与您的非常相似,具有几乎相同的 GCD 结构(具有上述注意事项),并且滚动非常流畅,即使在旧设备上也是如此。如果您希望我发布代码,我很乐意。

如果您仍然遇到问题,CPU 分析器非常适合识别瓶颈并让您知道应该将注意力集中在哪里。网上有一些很棒的 WWDC 会议,重点介绍如何使用 Instruments 来识别性能瓶颈,我发现它们对熟练使用 Instruments 非常有帮助。

这是我的代码。在viewDidLoad,我初始化了我的图片缓存:

- (void)initializeCache
{
    self.imageCache = [[NSCache alloc] init];
    self.imageCache.name = @"Custom Image Cache";
    self.imageCache.countLimit = 50;
}

然后我在我的tableView:cellForRowAtIndexPath 中使用它:

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

    // set the various cell properties

    // now update the cell image

    NSString *imagename = [self imageFilename:indexPath]; // the name of the image being retrieved

    UIImage *image = [self.imageCache objectForKey:imagename];

    if (image)
    {
        // if we have an cachedImage sitting in memory already, then use it

        cell.imageView.image = image;
    }
    else
    {
        cell.imageView.image = [UIView imageNamed:@"blank_image.png"];

        // the get the image in the background

        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

            // get the UIImage

            UIImage *image = [self getImage:imagename];

            // if we found it, then update UI

            if (image)
            {
                dispatch_async(dispatch_get_main_queue(), ^{

                    // if the cell is visible, then set the image

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

                    [self.imageCache setObject:image forKey:imagename];
                });
            }
        });
    }

    return cell;
}

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];

    [self.imageCache removeAllObjects];
}

顺便说一句,您可能考虑的进一步优化是将缓存的图像预加载到单独的队列中,而不是在单独的线程中即时加载图像。我认为没有必要,因为这对我来说似乎已经足够快了,但它是加快 UI 速度的另一种选择。

【讨论】:

  • 我很想看看代码,也许我能看到我做错了什么
  • @Vikings 我已经更新了我的代码。除了用于从我的 sqlite 数据库中检索数据的数据库例程之外,我已经包含了所有内容(因为我认为这与当前的讨论无关,我不想让这变得比必要的更混乱)。跨度>
  • 我正在使用 NSFetchedResultsController 来获取结果,我不确定这是否会有所不同,但我收到了内存警告,并且表崩溃了。也许我应该在启动时获取结果。大量数据没有问题?
  • @Vikings 不,有 100 多张缩略图我没有记忆问题,但如果我有记忆问题,didReceiveMemoryWarning 会处理它。我敢肯定,如果我有成百上千的缩略图大小,我最终会遇到问题,但在这种情况下,我可能会添加一些逻辑来将缓存限制为最近缓存的缩略图的合理数量图片。关于性能问题,我原以为在 Core Data 中缓存图像会对性能造成不小的影响,尽管我不知道 NSFetchedResultsController 是否会在自己的内存中进行缓存。
  • 您收到内存警告的事实既令人惊讶(因为我看不出在您的代码中会导致什么),也令人担忧。我以为我们只是在解决一个性能问题,但似乎你必须有其他事情发生。我假设您已经寻找泄漏,通过静态分析器运行它等等?你的图片是漂亮的小缩略图吗?
【解决方案2】:

对于初始加载,您在此处无能为力,您的速度差不多。 如果仍然太慢,请尝试加载较小的图像。

有几点:

首先,小心使用 -imageWithContentsOfFile,它不会缓存任何内容。每次加载图像时,您都会受到全部打击,而不是 -imageNamed 会使图像在某些缓存中保持温暖。 您当然可以将其缓存在您的域对象中,但我个人强烈建议不要这样做。 你的内存占用会飞涨,迫使你实现自己的缓存过期机制,而苹果通过 -imageNamed 有一个非常好的图像缓存。 如果你能在所有 3 个系列的设备上做得比苹果更好,我会感到惊讶:)

那么,你在这里打破了 UITableView 的享元模式:

dispatch_sync(dispatch_get_main_queue(), ^{
            cell.beerImage.image = image;
            beer.image = image;
            [cell setNeedsLayout];
        });

要求表格视图在给定索引处提供您的单元格,而不是捕获块中的单元格:在加载图像时,该单元格实例实际上可能已被重用于另一个索引路径,您将在错误的单元格中显示图像。

而且这里不需要-setNeedsLayout,换个图片就够了。

编辑:哎呀!我错过了表格视图中图像的明显之处。你的图片有多大,图片视图有多大,图片上的内容模式是什么? 如果您的图像与图像视图的大小非常不同,并且您要求图像视图调整大小,这将发生在主线程上,并且您将在那里获得大量性能。 加载后,将图像大小调整为关闭线程的图像视图(快速谷歌搜索将为您提供执行此操作的核心图形代码)。

【讨论】:

  • +1 表示您对 imageNamedimageWithContentsOfFile 的注释。完全正确。当我建议自定义缓存时,我是从 Vikings 在另一个关于将图像存储在 Core Data 与本地文件中的讨论中提出的其他 cmets 的。但是,如果您使用存储在 Documents 文件夹或包中的图像,那么imageNamed 是最好的,无需自定义缓存。 -1 仅使用 dispatch_sync 中的单元格变量,而无需 (a) 使用 cellForRowAtIndexPath 重新检索它,然后 (b) 检查它是否不为空。
  • stackoverflow.com/questions/11511548/… 上,我被介绍给NSCache,它提供了类似于imageNamed 的巧妙缓存,但您可以智能地查询某些内容是否被缓存。因此,如果您使用 NSCache,则不需要(也不应该)使用 imageNamed,但您会享受精心设计的缓存带来的好处!
  • @RobertRyan 是的,重复使用捕获的单元正是我的观点;-) 我告诉他不要那样做。我什至懒得去检查 nil,反正它是安全的......
  • 是的,NSCache 很好用,但是您需要在一个中心位置使用它,通常是一个 ImageService,它将成为您访问任何资产的入口点,无论它是本地的还是远程的。由于他直接在控制器中从磁盘读取图像,这听起来有点矫枉过正。
  • 如果您正在进行异步 UI 更新,则需要 UITableView 的 cellForRowAtIndexPath(不要与 UITableViewDataSource 协议的 tableView:cellForRowAtIndexPath 混淆,这是完全不同的)以防相关行滚动离开屏幕,该单元格已被重新用于另一行。如果该行已经滚出屏幕,UITableView 的cellForRowAtIndexPath 绝对可以是nil。也许我不明白你的意思。但我同意,如果你正在做 imageWithContentsOfFile,它可能是矫枉过正(除非你正在做一些图像处理)。
【解决方案3】:

缺少的步骤是使用获取的图像更新模型。实际上,您每次都在为每个单元进行新的加载。该模型是缓存相对昂贵的负载结果的正确位置。你能添加一个 Beer.image 属性吗?

然后,您的配置代码将如下所示:

Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
cell.displayBeerName.text = beer.name;

// did we already cache a copy of the image?
if (beer.image != nil) {
    // good.  use it.  this will run quick and this will run most of the time
    cell.beerImage.image = beer.image;
} else {
    // it must be the first time we've scrolled by this beer.  do the expensive
    // image init off the main thread

    cell.beerImage.image  = nil;   // set a default value here.  nil is good enough for now

    [self loadImageForBeer:beer atIndexPath:indexPath];
}

为了清楚起见,将加载器逻辑移到此处...

- (void)loadImageForBeer:(Beer *)beer atIndexPath:(NSIndexPath *)indexPath {

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
    dispatch_async(queue, ^{

        UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];
        beer.image = image;

        dispatch_sync(dispatch_get_main_queue(), ^{
            UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
            cell.beerImage.image = image;
        });
    });
}

【讨论】:

  • 顺便说一句,您不必使用核心数据模型作为缓存图像的地方,但我认为这是一个好主意。这样,应用程序的其他部分将可以访问它。但如果由于某种原因这不切实际,您可以将可变字典添加到您的视图控制器。键可以是图像路径,值可以是图像。
  • 这个 Beer.image 属性,什么是 BOOL 或 ImageView?
  • 他只是在检查 beer.image 是否存在。通过执行 if (beer.image) 您实际上是在检查 if (beer.image != nil)
  • 是的。将对其进行编辑以使其更清晰。我的意思是让你保存实际的 UIImage。您实际上是在保存创建的映像、该映像的分配和初始化从磁盘中速度成本所在的位置。
  • @danh 我为 UIImage 的核心数据模型添加了一个属性,我通过编辑更新了我的代码。如果我重复使用单元格,我对它的工作原理有点困惑
【解决方案4】:

你可以看看这个问题,以前在堆栈溢出时回答过。

UIImage in uitableViewcell slowdowns scrolling table

或者试试这个代码

- (void)configureCell:(BeerCell *)cell 
          atIndexPath:(NSIndexPath *)indexPath 
{
    Beer *beer = (Beer *) [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.displayBeerName.text = beer.name;

           UIImage *image = [UIImage imageWithContentsOfFile:beer.imagePath];

            cell.beerImage.image = image;
            [cell setNeedsLayout];
        }

【讨论】:

  • 我认为该解决方案不会重复使用 tableview 单元格,这是一个问题
  • 然后只需以编程方式创建 UIImageview 并将子视图添加到 cell.content 视图,这可能会加载得更快。
猜你喜欢
  • 1970-01-01
  • 2019-03-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多