滚动性能和UIImage绘制

4

我正在构建一个类似于iPod.app的专辑浏览视图的UITableView:

IMG_2316.PNG

在第一次启动时,我将所有艺术家和专辑的艺术品从iPod库中导入。将所有内容保存到CoreData中并重新获取到NSFetchedResultsController中。我重复使用单元格标识符,并在我的cellForRowAtIndexPath:方法中添加了以下代码:

Artist *artist = [fetchedResultsController objectAtIndexPath:indexPath];
NSString *identifier = @"bigCell";

SWArtistViewCell *cell = (SWArtistViewCell*)[tableView dequeueReusableCellWithIdentifier:identifier];

if (cell == nil)
    cell = [[[SWArtistViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier] autorelease];

cell.artistName = artist.artist_name;
cell.artworkImage = [UIImage imageWithData:artist.image];

[cell setNeedsDisplay];

return cell;

我的SWArtistViewCell单元格实现了drawRect:方法来绘制字符串和图像:

[artworkImage drawInRect:CGRectMake(0,1,44,44)]
[artistName drawAtPoint:CGPointMake(54, 13) forWidth:200 withFont:[UIFont boldSystemFontOfSize:20] lineBreakMode:UILineBreakModeClip];

滚动仍然很卡顿,我就是想不明白为什么。像iPod和Twitter这样的应用程序具有非常流畅的滚动效果,但它们都像我一样在单元格中绘制了一些小图像。
我的所有视图都是不透明的。我错过了什么?
编辑:这是Shark的内容:

alt text 我不熟悉Shark。这些符号与什么相关?当我查看这些的跟踪时,它们都指向我的drawRect:方法,特别是UIImage绘制。

alt text 如果窒息点是文件读取,它会指向其他东西吗?它肯定是绘图吗?
编辑:保留图像
我已经按照pothibo建议的方式添加了一个artworkImage方法到我的Artist类中,该方法保留了使用imageWithData:创建的图像。
- (UIImage*)artworkImage {
    if(artworkImage == nil)
        artworkImage = [[UIImage imageWithData:self.image] retain];

    return artworkImage;
}

所以现在我可以直接将保留的图像设置为我的TableViewCell,如下所示:
cell.artworkImage = artist.artworkImage;

我还在我的tableViewCell类的setArtworkImage:方法中设置了setNeedsDisplay。滚动仍然很卡顿,Shark显示的结果完全相同。

5个回答

6
您的分析数据表明,瓶颈在于解压PNG图像。我的猜测是,58.5%的CPU时间用于解压PNG数据(即如果memcpy调用也包含在加载中)。可能更多的时间都花在这里,但没有更多的数据很难说。我的建议如下:
  1. 如前所述,将加载的图像保存在UIImage中,而不是NSData中。这样,每次显示图像时就不必解压缩PNG。
  2. 将图像加载放入工作线程中,以不影响主线程的响应能力(尽可能)。创建工作线程非常容易:
  3. [self performSelectorOnMainThread:@selector(preloadThreadEntry:) withObject:nil waitUntilDone:NO];

  4. 预先加载图像,例如100行或更多(在您滚动的方向上70行,在相反的方向上保留30行)。如果所有图像都需要在视网膜上为88x88像素,则100个图像最多需要两MB。
  5. 当您进行更多的分析时,名为“png”、“gz”、“inflate”等的调用可能不会从您的列表中消失,但它们肯定不会以这种糟糕的方式影响应用程序的感觉。
只有在此之后仍然存在性能问题时,我建议您考虑缩放,并为视网膜加载“...@2x.png”图像。祝您好运!

4

[UIImage imageWithData:]不会缓存。

这意味着每次在该数据源方法中传递时,CoreGraphic都会解压缩和处理您的图像。

我建议将您的艺术家对象更改为保存UIImage而不是NSData。如果您经常收到内存警告,可以随时刷新图像。

此外,我不建议在数据源调用中使用setNeedsDisplay,而应该从单元格内部使用它。

setNeedsDisplay不是直接调用drawRect的方法:

它只告诉操作系统在运行循环结束时再次绘制UIVIew。您可以在同一运行循环中调用setNeedsDisplay 100次,操作系统只会调用一次drawRect方法。


问题是你不能将UIImage存储在CoreData中。您建议我应该将图像保存到磁盘上吗?例如,我应该在setArtworkImage:方法中调用[self setNeedsDisplay]吗? - samvermette
1
不建议在CoreData中存储图像(无论是通过NSData还是其他方式)。当然,如果图像的大小很小,那么可以考虑,但否则,这类似于在mySQL中使用BLOBs。您可能希望使用NSKeyArchiver来存储图像,并仅在CoreData中存储文件名。 - Pier-Olivier Thibault
1
我的建议是在你的艺术家对象中添加一个名为imageWithData的方法。请记住,CoreData只是后备存储。你可以随意对你的方法-(NSData )artworkImage进行子类化处理,变成-(UIImage)artworkImage,在该方法中从后备存储中获取图像并将其存储在私有变量中(如果还没有存储)。我不知道你是否能明白。 - Pier-Olivier Thibault
嗯嗯,我明白了。那么从磁盘存取88x88的图像肯定比从CoreData获取更快吗? - samvermette
这是同样的延迟还是更快吗?另外,请尝试检查每次通过以下方式获取艺术家时是否创建了新对象Artist *artist = [fetchedResultsController objectAtIndexPath:indexPath];我不确定CoreData如何工作,因为我只使用过一次。如果每次实例化一个新的艺术家,问题仍然存在于图像创建中。此外,Sharks的分析数据指向图像解压缩操作,因此我必须考虑它是每次解压缩UIImage。 - Pier-Olivier Thibault
显示剩余5条评论

1

我猜测延迟是由于在Core Data中存储图像所致。通常,Core Data不是存储大型数据块的好方法。

更好的解决方案是将图像作为单独的文件存储在磁盘上,并使用相册ID来标识每个图像。然后,您可以设置一个内存缓存,将图像存储在RAM中,以便快速加载到您的UIImageView中。最好在后台线程上(例如尝试performSelectorOnBackgroundThread)从磁盘加载图像到RAM,以便I/O不会拖慢主线程(这会影响滚动性能)。

附加说明1

如果您每次仅调用一次单元格加载的-drawRect:(正如您应该做的那样),则问题可能是图像的缩放。使用代码通过 drawInRect 绘制图像进行缩放将使用CPU时间,因此另一种方法是在从iPod库接收到它们时将图像进行缩放(如果您需要它们以多种大小保存每个大小的版本)。您可能需要在导入数据时在后台线程上执行此操作,以避免阻塞主(UI)线程。

一个值得考虑的替代方案是,UIImageView 可能使用核心动画进行缩放,这意味着它是硬件加速的(我不确定这是否属实,只是猜测)。因此,将图像切换到 UIImageView 将消除图像缩放的 CPU 负担。你会有轻微的合成开销增加,但这可能是接近“最佳”滚动性能的最简单方法。

你是否熟悉Shark?我编辑后的回答是否指出了窒息是由于在CoreData中存储图像所致? - samvermette
不,看起来更像是你绘制的太频繁了。你是否可能通过设置setNeedsDisplay而过于频繁地调用-drawRect:?尝试在-drawRect:中放置一个NSLog()消息,它应该只在加载新单元格时被调用。如果它在任何其他时间被调用,那就是你的问题(或者至少是你的其中一个问题)。 - Nick Forge
是的,那也是我猜测之一。但是,drawRect只被调用了一次。我发现需要调用setNeedsDisplay有点奇怪,通常我不需要这样做。 - samvermette
这可能与你绘制图像时的缩放有关 - 请参见上面的附录1。 - Nick Forge
好的观点。唯一的问题是,当我将88x88图像绘制到44x44矩形中时,它并没有被缩放。不管怎样,在Retina显示屏上绘制图像都是这样的。 - samvermette

1
如果你的延迟发生在-drawRect中,那么你可能想看一下this article:Tweetie的开发者相当详细地解释了他用来获得你想要的平滑滚动效果的方法。虽然自那时以来这变得更容易了一些: CALayer有一个shouldRasterize属性,它基本上将其子层级压缩成位图,这样只要图层内部没有任何变化,当你像滚动UITableView时,就可以为你提供更好的性能。在这种情况下,你可能会将该属性应用于单个UITableViewCell的图层。

1
在 FastScrolling 项目中找不到 shouldRasterize 属性。ABTableView 引入的确实是直接绘制内容,而不是分配 UILabel 和 UIImageView。这正是我正在做的。 - samvermette

0

目前最好的选择是使用Instruments(以前称为Shark)来尝试找到代码中的瓶颈。


我终于弄清楚了,Shark现在是Instruments的一部分。这就是为什么我以前不能使用它的原因。感谢那个视频。我会用我的结果更新我的问题。你觉得呢? - samvermette
2
当你将图像绘制到矩形中时,图像的尺寸与矩形的尺寸相比如何?也许在将其导入应用程序时,你应该将图像缩小为44x44。 - Kris Markel
原始图像为88x88。我将其绘制为Retina显示器上的44x44,非Retina显示器上的44x44。有没有更好的方法在Retina上绘制图像? - samvermette
我不知道。那个想法就这样了。 - Kris Markel
忘记了CG会自动适应设备屏幕比例。所以我保存我的图像尺寸为88x88,这在Retina上会转换为176x176,在非Retina上则为88x88。将176x176的图像绘制到44x44的矩形中是导致卡顿的原因。所以你是对的!早些时候应该检查导入的图像尺寸:/ - samvermette

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