UITableView滚动不流畅。

19

我在包含UIImageView的UITableViewCell上的UITableView中遇到了平滑滚动问题。类似的问题可以在StackOverflow上找到,但是没有一个提出的解决方案能够完全帮助我摆脱延迟。

我的情况非常普遍:

  1. 图像存储在应用程序存储中(在我的示例中是应用程序捆绑包)
  2. 图像可能具有不同的大小(500x500、1000x1000、1500x1500)
  3. 我需要在UITableView中显示这些图像,其中UIImageView的大小为120x120(retina)

我遵循了多个优化提示并成功地优化了滚动。

不幸的是,它仍然不完美。这是我的情况:

  1. 首先,我将所有图像加载/处理/调整大小逻辑移动到后台线程
  2. 启用UITableViewCell重用
  3. 一旦UITableViewCell出现在视图中,我清除旧值(设置为null)并开始后台线程加载图像
  4. 此时我们处于后台线程,并且我添加了500毫秒的延迟,以避免过于频繁地设置新图像(如果我们快速滚动时)(请参见下面的说明)
  5. 如果UIImage存在于静态图像缓存中(使用UIImage实例的常规字典)-获取它并转到步骤9。
  6. 如果没有-使用url到应用程序捆绑包(在真实世界的情况下,图像将存储在应用程序存储中,而不是在捆绑包中),从捆绑包中加载新图像(imageWithName)
  7. 一旦加载图像,就使用图形上下文将其调整为120x120
  8. 将调整大小的图像保存到静态图像缓存中
  9. 此时我们拥有UIImage实例,并且处理在后台线程中。从这里开始,我们使用给定的图像回到UI线程
  10. 如果数据上下文已被清除(例如UITableViewCell消失或重用以显示另一个图像),则跳过当前可用图像的处理。
  11. 如果数据上下文相同-使用alpha动画(UIView.Animate)将UIImage分配给UIImageView
  12. 一旦UITableViewCell不再显示-清除数据上下文

在这里启动新的后台线程获取图像之前(步骤1),最初是在没有后台线程的情况下对UIImage缓存进行检查。在这种情况下,如果我们在缓存中有图像,我们会立即分配它,这会在快速滚动时引入很大的延迟(我们分配图像太频繁,因为我们将它们立即获取)。如下所示的示例中,这些行被注释掉了。

仍存在两个问题:

  1. 在滚动过程中,在我分配新的UIImage给UIImageView的那一刻仍然存在一个小的延迟。
  2. (这个问题更加明显)当您点击项并从详细信息返回时,导航动画完成之前会出现延迟。

欢迎提供任何建议来解决这两个问题,或者如何优化我的方案

请注意,示例是使用Xamarin编写的,但我不相信Xamarin是问题的原因,因为我使用ObjectiveC编写的应用程序也存在同样的问题。

平滑滚动测试应用程序


1
Instruments(或其Xamarin等效工具)可以告诉你关于应用程序性能的什么信息?你花费时间在哪里?它是CPU限制还是GPU限制?等等。 - David Rönnqvist
@carlodurso 它被一对一映射到ObjectiveC。 - Mando
@DavidRönnqvist的工具显示了活动的正态分布(时间测量)。如何衡量“它是CPU绑定还是GPU绑定?” - Mando
测量CPU使用率和GPU使用率。如果其中任何一个达到100%的使用率,则受此限制。您还可能受到磁盘活动的限制。您应该进行测量以找出是什么使您的应用程序变慢并修复它。任何其他更改都不会真正起作用。 - David Rönnqvist
你需要使用 .shouldRasterize 吗? - Fattie
显示剩余4条评论
6个回答

12

你是否尝试过使用一个在Bundle中储存的仅有120x120尺寸图片来填充你的TableView?这样可以检查你的Image rendering是否有问题。

与其将所有图片都调整为120x120并保存到缓存中,我建议创建并使用所有图片的缩略图。你已经在某种程度上这样做了,但每次滚动或者缓存满时都会重复操作。

在我们最近的项目中,我们使用了一个包含书籍封面的UICollectionView。大多数封面大小在400-800kb之间,在滚动时感觉非常糟糕。因此,我们为每个图像创建了一个缩略图(大约40-50kb),并使用缩略图代替真实的封面。效果非常好!我附上了缩略图创建函数。

- (BOOL) createThumbnailForImageAtFilePath:(NSString *)sourcePath withName:(NSString *)name {

    UIImage* sourceImage = [UIImage imageWithContentsOfFile:sourcePath];
    if (!sourceImage) {
        //...
        return NO;
    }

    CGSize thumbnailSize = CGSizeMake(128,198);

    float imgAspectRatio = sourceImage.size.height / sourceImage.size.width;
    float thumbnailAspectRatio = thumbnailSize.height/thumbnailSize.width;

    CGSize scaledSize = thumbnailSize;

    if(imgAspectRatio >= thumbnailAspectRatio){
         //image is higher than thumbnail
         scaledSize.width = scaledSize.height * thumbnailSize.width / thumbnailSize.height;
    }
    else{
        //image is broader than thumbnail
        scaledSize.height = scaledSize.width * imgAspectRatio;
    }

    UIGraphicsBeginImageContextWithOptions( scaledSize, NO, 0.0 );
    CGRect scaledImageRect = CGRectMake( 0.0, 0.0, scaledSize.width, scaledSize.height );
    [sourceImage drawInRect:scaledImageRect];
    UIImage* destImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    NSString* thumbnailFilePath = [[self SOMEDIRECTORY] stringByAppendingPathComponent:name];

   BOOL success = [UIImageJPEGRepresentation(destImage, 0.9) writeToFile:thumbnailFilePath atomically:NO];

return success;

3

尝试使用Facebook的异步显示库。

https://github.com/facebook/AsyncDisplayKit

非常易于使用.. 根据他们的指南: http://asyncdisplaykit.org/guide/

_imageNode = [[ASImageNode alloc] init];
_imageNode.backgroundColor = [UIColor lightGrayColor];
_imageNode.image = [UIImage imageNamed:@"hello"];
_imageNode.frame = CGRectMake(10.0f, 10.0f, 40.0f, 40.0f);
[self.view addSubview:_imageNode.view];

这将在后台线程上解码图像。
我不确定在Xamarin上使用iOS库是否容易,但如果容易的话,请尝试一下。

2
我继承了Paul Hegarty的CoreDataTableViewController类,并在CoreDataTableView中使用了我的照片缩略图。
请查看第14讲中标题为FlickrFetcher和Photomania的示例。您还需要在同一链接处下载CoreDataTableViewController。
创建一个适当命名的CoreData实体,并定义任何属性(数据变量)您想要的。您需要定义两个“可转换”属性,一个用于照片,一个用于缩略图。
然后在CoreDataTableView中加载您的缩略图:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

{ NSArray *exceptions = [NSArray arrayWithObjects:@"SCR", @"DNS", @"NT", @"ND", @"NH", nil]; }

这段代码是将字符串"SCR"、"DNS"、"NT"、"ND"和"NH"存储在一个名为"exceptions"的数组中。
static NSString *CellIdentifier = @"resultsDisplayCell";

UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

if (cell == nil) {
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}

cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;

MarksFromMeets *athleteMarks = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString* date = [ITrackHelperMethods dateToAbbreviatedString:athleteMarks.meetDate];
NSMutableString *title = [NSMutableString stringWithFormat:@"%@", athleteMarks.markInEvent];
NSMutableString *subTitle = [NSMutableString stringWithFormat:@"%@ - %@",date, athleteMarks.meetName];
[title replaceOccurrencesOfString:@"(null)"
                       withString:@""
                          options:0
                            range:NSMakeRange(0, [title length])];

// cell.imageView.image = athleteMarks.photoThumbNail; // Don't like image in front of record.

[cell.textLabel setFont:[UIFont
                         fontWithName:@"Helvetica Neue" size:18]];

[cell.detailTextLabel setFont:[UIFont fontWithName:@"Helvetica Neue" size:16]];
[cell.detailTextLabel setTextColor:[UIColor grayColor]];

// make selected items orange
if ([athleteMarks.eventPR integerValue] != 0
    && (![exceptions containsObject:athleteMarks.markInEvent])) {

    title = [NSMutableString stringWithFormat:@"%@      (PR)",title];
    [cell.textLabel setTextColor:[UIColor redColor]];
}
else if ([athleteMarks.eventSB integerValue] != 0
         && (![exceptions containsObject:athleteMarks.markInEvent])) {
    title = [NSMutableString stringWithFormat:@"%@      (SB)",title];

    [cell.textLabel setTextColor:[UIColor orangeColor]];
} else {
    [cell.textLabel setTextColor:[UIColor grayColor]];
}

cell.textLabel.text = title;
cell.detailTextLabel.text = subTitle;

cell.indentationLevel = indentationLevelOne;
cell.indentationWidth = indentationForCell;

return cell;

}

如果您想,我可以给您发送一个实体的NSManagedObject子类的分类示例。这个分类将照片和缩略图加载到CoreData实体中。第一次会比较慢,但之后用户应该能够顺畅地滚动TableView,然后所有更新的结果都将自动加载。让我知道吧。
一个好处是CoreData处理了所有的内存管理。
祝好运!

P.S. Tim Roadley在他的网站上有一系列关于CoreData的教程,还提供了一些有用的辅助类。 - PhillipOReilly

0
也许你可以尝试使用SDWebImage。它也是一个Xamarin 组件,提供异步图像下载器和异步内存和磁盘图像缓存,并具有自动缓存过期处理功能。使用它可能意味着放弃很多艰苦编写的代码,但这可能是值得的 - 而且你的代码将变得更简单。在iOS中,你还可以在控制器的viewDidLoad中设置SDWebImageManager
- (void)viewDidLoad{
...
SDWebImageManager *manager = [SDWebImageManager sharedManager];
manager.delegate = self;
...
}

并将视图控制器设置为委托。然后,当调用以下委托方法时:

- (UIImage *)imageManager:(SDWebImageManager *)imageManager transformDownloadedImage:(UIImage *)image withURL:(NSURL *)imageURL

在缓存图像之前,您可以将其缩放为适当大小的缩略图。

希望能有所帮助。


0

我没有足够的声望来评论,所以这里有一个帮助我表格滚动性能的答案:

  • 将表格视图高度设置为大于可视窗口。单元格将会"离屏"加载,并有助于提高滚动平滑性。
  • 使用以下方法进行图像处理: - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath

这些技巧让我的表格非常流畅。我从API服务获取图像数据,AFNETWORKING有一个很棒的图像加载器,但对你来说并不是必需的,因为图像在包中。


0

我曾经遇到过类似的问题,我的滚动不够流畅。我在表格中插入了一个带有标签视图的变量UIImageView。 我所做的是将HeightforRowAtIndexPath方法更改为estimatedHeightforRowAtIndexPath,现在滚动很流畅。


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