我有一个生成艺术应用程序,它从一小组点开始,向外生长,并检查生长以确保它不会与任何东西相交。我的第一个天真的实现是在主UI线程上完成这一切,并产生预期的结果。随着大小的增长,需要检查的点越来越多,速度会减慢,最终会阻塞UI。
我做了一件显而易见的事情,并将计算转移到另一个线程,这样UI就可以保持响应。这有帮助,但只是一点点。我通过一个NSBitmapImageRep
来实现这一点,我把NSGraphicsContext
包起来,这样我就可以在里面画画了。但我需要确保我不会在主UI线程上把它画到屏幕上,同时我也在后台线程上画它。所以我介绍了一把锁。随着数据越来越大,绘制可能需要很长时间,所以即使这样也有问题。
我的最新修订版有2个NSBitmapImageRep
s。其中一个是最近绘制的版本,每当视图需要更新时都会绘制到屏幕上。另一个绘制在背景线程上。在背景线程上绘制完成后,它会复制到另一个线程。我通过获取每个像素的基地址并简单地调用memcpy()
来将像素从一个像素移动到另一个像素来进行复制。(我试着交换它们而不是复制,但即使绘图以调用[-NSGraphicsContext flushContext]
结束,我还是会将部分绘制的结果绘制到窗口中。)
计算线程如下所示:
BOOL done = NO;
while (!done)
{
self->model->lockBranches();
self->model->iterate();
done = (!self->model->moreToDivide()) || (!self->keepIterating);
self->model->unlockBranches();
[self drawIntoOffscreen];
dispatch_async(dispatch_get_main_queue(), ^{
self.needsDisplay = YES;
});
}
这对于保持UI的响应性来说已经足够好了。然而,每次我将绘制的图像复制到blitting图像中时,我都会调用[-NSBitmapImageRep baseAddress]
。查看仪器中的内存配置文件,每次调用该函数都会创建一个CGImage
。此外,CGImage
在计算完成之前不会发布,这可能需要几分钟的时间。这会导致内存变得相当大。在我的过程中,我看到了大约3-4 Gigs的CGImages,尽管我从来都不需要超过2个。在计算完成并清空缓存后,我的应用程序的内存降到只有350-500 MB。我没有想过在计算循环中使用自动释放池,但我会尝试一下。
操作系统似乎正在缓存它创建的图像。然而,在计算完成之前,它不会清除缓存,因此在此之前,它在没有绑定的情况下增长。有什么办法防止这种情况发生吗?
不要使用-bitmapData
和memcpy()
来复制图像。将一个图像绘制到另一个图像中。
我经常建议开发人员阅读10.6 AppKit发行说明中的"NSBitmapImageRep:CoreGraphics阻抗匹配和性能说明"一节:
NSBitmapImageRep:CoreGraphics阻抗匹配和性能说明
上面的发行说明详细介绍了NSImage级别的核心更改雪豹。NSBitmapImageRep级别,还用于性能和提高阻抗与CoreGraphics相匹配。
NSImage是一种相当抽象的图像表示。很漂亮这只是一个可以绘制的东西,尽管它没有NSView那么抽象因为它不应该基于上下文的不同方面表现除了质量决策外,它还被卷入其中。这有点不透明语句,但可以用一个例子来说明:如果您绘制按钮插入100x22区域与22x22区域,您可以期待按钮以拉伸其中间但不拉伸其端盖。图像不应该这样做(如果你尝试,你可能会崩溃!)。图像应始终线性且均匀地缩放以填充矩形,其中它是绘制的,尽管它可以选择表示等进行优化该地区的质量。类似地,中的所有图像表示NSImage应该表示相同的绘图。不要装得太满作为代表中的不同图像
偏离主题的是,NSBitmapImageRep是一个更具体的对象NSImage没有像素,NSBitmapImageRep有。一NSBitmapImageRep是一个数据块和像素格式信息和色彩空间信息,使我们能够解释数据作为颜色值的矩形阵列。
这和CGImage差不多。在雪豹NSBitmapImageRep由CGImageRef本机支持,而不是直接是数据块。CGImageRef确实拥有大量数据。而在Leopard中,从CGImage实例化的NSBitmapImageRep将解压缩并可能处理数据(从位图文件格式),在SnowLeopard中,我们努力抓住原始CGImage。
这会对性能产生一些影响。大多数都很好!你应该看看较少地将位图数据编码和解码为CGImages。如果你从JPEG文件初始化NSImage,然后将其绘制成PDF应该得到与原始JPEG文件大小相同的PDF。在里面Leopard你会看到一个解压缩图像大小的PDF。服用另一个例子是CoreGraphics缓存,包括上传到图形卡,都绑定到CGImage实例,所以越相同实例可以使用得更好。
然而:在某种程度上NSBitmapImageRep已更改。CGImages是不可变的,NSBitmapImageRep是。如果您修改NSBitmapImage Rep,则在内部可能需要从CGImage中复制数据更改,并将其重新打包为新的CGImage。所以,基本上,绘画NSBitmapImageRep速度快,查看或修改其像素数据不这在《豹》中是真的,但现在更真实了。
以上步骤确实是懒洋洋地发生的:如果你做了一些导致NSBitmapImageRep从其后台CGImageRef复制数据(类似调用bitmapData),位图不会将数据重新打包为CGImageRef,直到它被绘制或者直到它由于某种其他原因需要CGImage为止。所以当然,访问数据并不是世界末日在某些情况下做正确的事情,但总的来说,你应该而是考虑绘画。如果你认为你想与像素,请查看CoreImage-这是我们的真正用于像素处理的系统。
这与安全性不谋而合。我们在雪豹身上看到的一个问题变化是应用程序非常喜欢硬编码位图格式。一NSBitmapImageRep可以是每像素8、32或128位,也可以是浮点与否,它可以是预乘的,也可以是可能没有alpha通道等。这些方面由位图属性,如-bitmap格式。不幸的是,如果有人想要要从NSBitmapImageRep实例中提取位图数据通常只调用bitmapData,将数据视为(比如)预乘每像素32位RGBA,如果它似乎有效,那就到此为止。
现在,NSBitmapImageRep处理的数据不如以前那么多到,你可能掌握的随机位图图像代表可能会有所不同格式。其中一些硬编码格式可能是错误的
解决方案是而不是尝试处理完整的格式范围NSBitmapImageRep的数据可能在其中,这太难了。相反,将位图绘制成您知道格式的东西,然后看看这个。
看起来是这样的:
NSBItmapImageRep *bitmapIGotFromAPIThatDidNotSpecifyFormat; NSBitmapImageRep *bitmapWhoseFormatIKnow = [[NSBitmapImageRep alloc] initWithBitmapDataPlanes:NULL pixelsWide:width pixelsHigh:height bitsPerSample:bps samplesPerPixel:spp hasAlpha:alpha isPlanar:isPlanar colorSpaceName:colorSpaceName bitmapFormat:bitmapFormat bytesPerRow:rowBytes bitsPerPixel:pixelBits]; [NSGraphicsContext saveGraphicsState]; [NSGraphicsContext setContext:[NSGraphicsContext graphicsContextWithBitmapImageRep:bitmapWhoseFormatIKnow]]; [bitmapIGotFromAPIThatDidNotSpecifyFormat draw]; [NSGraphicsContext restoreGraphicsState]; unsigned char *bitmapDataIUnderstand = [bitmapWhoseFormatIKnow bitmapData];
这只会产生访问数据的副本bitmapIGotFromAPIThatDidNotSpecifyFormat的bitmapData,因为无论如何,数据都需要从后备CGImage中复制出来。而且请注意,这并不取决于源图形是位图。这是一种为任何图形获取已知格式像素的方法,或者只是以获取位图。这是一种比获取位图更好的方法调用-TIFR表示。它也比锁定NSImage上的焦点并使用-NSBitmapImageRepinitWithFocusedViewRect:]。
综上所述:(1)画画很快。玩像素不是。(2) 如果你认为你需要使用像素,(a)考虑是否有办法进行绘图或(b)查看CoreImage。(3) 如果你仍然想要获得像素,绘制一个你知道格式的位图看看那些像素。
事实上,最好从前面的一节开始,用类似的标题——"NSImage、CGImage和CoreGraphics阻抗匹配"——并通读到后面的一节。
顺便说一句,交换图像代表很有可能会奏效,但你只是没有正确地同步它们。您必须显示两个代表使用的代码,以便我们确定。