17

我的课是在屏幕外渲染图像。我认为重复使用CGContext而不是为每张图像一次又一次地创建相同的上下文将是一件好事。我设置了一个成员变量,所以如果nil 像这样_imageContext,我只需要创建一个新的上下文:_imageContext

if(!_imageContext)
    _imageContext = [self contextOfSize:imageSize];

代替:

CGContextRef imageContext = [self contextOfSize:imageSize];

当然我不再发布CGContext了。

这些是我所做的唯一更改,结果表明重用上下文将渲染从大约 10 毫秒减慢到 60 毫秒。我错过了什么吗?在再次绘制之前,我是否必须清除上下文或其他内容?还是为每个图像重新创建上下文的正确方法?

编辑

发现了最奇怪的联系..

当我在寻找应用程序开始渲染图像时应用程序的内存异常增加的原因时,我发现问题在于我将渲染图像设置为NSImageView.

imageView.image = nil;
imageView.image = [[NSImage alloc] initWithCGImage:_imageRef size:size];

看起来 ARC 没有发布以前的NSImage. 避免这种情况的第一种方法是将新图像绘制到旧图像中。

[imageView.image lockFocus];
[[[NSImage alloc] initWithCGImage:_imageRef size:size] drawInRect:NSMakeRect(0, 0, size.width, size.height) fromRect:NSZeroRect operation:NSCompositeSourceOver fraction:1.0];
[imageView.image unlockFocus];
[imageView setNeedsDisplay];

内存问题消失了,CGContext-reuse 问题发生了什么?不重用上下文现在需要 20 毫秒而不是 10 毫秒 - 当然,绘制到图像中比仅仅设置它需要更长的时间。重用上下文也需要 20 毫秒而不是 60 毫秒。但为什么?我看不出可能有任何联系,但我可以通过设置NSImageView' 图像而不是绘制它来重现重用需要更多时间的旧状态。

4

2 回答 2

15

我对此进行了调查,我观察到同样的放缓。查看设置为采样内核调用以及用户态调用的 Instruments 显示了罪魁祸首。@RyanArtecona 的评论是正确的。我在两次测试运行中将 Instruments 集中在最底层的用户态调用CGSColorMaskCopyARGB8888_sse上(一个重用上下文,另一个每次都创建一个新上下文),然后反转生成的调用树。在不重用上下文的情况下,我看到最重的内核跟踪是:

Running Time    Self            Symbol Name
668.0ms   32.3% 668.0           __bzero
668.0ms   32.3% 0.0              vm_fault
668.0ms   32.3% 0.0               user_trap
668.0ms   32.3% 0.0                CGSColorMaskCopyARGB8888_sse

这是内核将由于CGSColorMaskCopyARGB8888_sse访问它们而出错的内存页面清零。这意味着 CGContext 将 VM 页面映射到位图上下文,但内核实际上不会执行与该操作相关的工作,直到有人实际访问该内存。实际映射/故障发生在首次访问时。

现在让我们看看重用上下文时最重的内核跟踪:

Running Time            Self            Symbol Name
1327.0ms   35.0%        1327.0          bcopy
1327.0ms   35.0%        0.0              user_trap
1327.0ms   35.0%        0.0               CGSColorMaskCopyARGB8888_sse

这是内核复制页面。我的钱将用于提供@RyanArtecona 在他的评论中谈到的行为的底层写时复制机制:

在 CGBitmapContextCreateImage 的 Apple 文档中,它说在原始上下文上完成更多绘图之前不会发生实际的位复制操作。

在我用来测试的人为案例中,非重用案例的执行时间为 3392 毫秒,而重用案例的执行时间为 4693 毫秒(明显更慢)。仅考虑每种情况下最重的单个跟踪,内核跟踪表明我们在第一次访问时花费了 668.0 毫秒零填充新页面,并且在映像获得引用后的第一次写入时,我们花费了 1327.0 毫秒写入写时复制页面到那些页面。这是 659 毫秒的差异。仅这一差异就占了两种情况之间差距的约 50%。

因此,稍微提炼一下,未重用的上下文更快,因为当您创建上下文时,它知道页面是空的,并且没有其他人引用这些页面来强制它们在您写入时被复制他们。当您重用上下文时,页面被其他人(您创建的图像)引用,并且必须在第一次写入时复制,以便在上下文状态发生变化时保留图像的状态。

您可以通过在调试器中单步执行时查看进程的虚拟内存映射来进一步探索这里发生了什么。vmmap是有用的工具。

实际上,您可能应该每次都创建一个新的 CGContext 。

于 2013-01-02T15:58:37.173 回答
8

为了补充@ipmcc 出色而彻底的答案,这里是一个指导性概述。

Apple 文档CGBitmapContextCreateImage中指出:

此函数返回的CGImage对象是通过复制操作创建的。在某些情况下,复制操作实际上遵循写时复制语义,因此位的实际物理复制仅在位图图形上下文中的基础数据被修改时发生。

因此,当调用此函数时,可能不会立即复制图像的底层位,而是可能会在下一次修改位图上下文时等待复制。这种位复制可能很昂贵(取决于上下文的大小和色彩空间),并且可能会在 Instruments 配置文件中伪装成CGContext...接下来在上下文中调用的任何绘图函数的一部分(当位被强制复制时)。这可能是CGContextDrawImage.k发生的事情

但是,文档继续说:

因此,您可能希望使用生成的图像并将其释放,然后再对位图图形上下文执行其他绘图。这样,您可以避免数据的实际物理副本。

这意味着,如果在您需要在上下文中进行更多绘图时,您将完成使用内存中创建的图像(即它已保存到磁盘、通过网络发送等),则图像将永远不需要完全被物理复制!

TL;博士

如果在某些时候您需要CGImage从位图上下文中拉出 a ,并且UIImageView在您在上下文中进行更多绘图之前,您不需要保留对它的任何引用(包括将其设置为的图像),那么它是使用 . 可能是个好主意CGBitmapContextCreateImage。如果没有,您的图像将在某个时候被物理复制,这可能需要一段时间,并且每次都使用新的上下文可能会更好。

于 2013-01-02T20:32:54.940 回答