我的课程是在屏幕外渲染图像。我想重复使用CGContext
而不是一次又一次地为每个图像创建相同的上下文将是一件好事。我设置了一个成员变量_imageContext
,所以如果_imageContext
为零,我只需要创建一个新的上下文:
if(!_imageContext)
_imageContext = [self contextOfSize:imageSize];
而不是:
CGContextRef imageContext = [self contextOfSize:imageSize];
当然我不再发布CGContext
了。
这是我所做的唯一改变,结果是重用上下文会使渲染速度从大约10ms减慢到60ms。我错过了什么吗?在重新插入之前,我是否必须清除上下文或其他内容?或者它是为每个图像重建上下文的正确方法吗?
修改
找到最奇怪的连接..
当我在应用程序开始渲染图像时,我正在搜索应用程序内存增加的原因时,我发现问题在于我将渲染图像设置为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
- 重用问题发生了什么?
不重复使用上下文现在需要20ms而不是10ms - 当然,绘制到图像所需的时间比设置它要长。
重用上下文也需要20ms而不是60ms。但为什么?我没有看到可能存在任何连接,但我可以通过设置NSImageView
的图像而不是绘制图像来重现旧的状态,其中重用需要更多时间。
答案 0 :(得分:15)
我对此进行了调查,并观察到同样的减速情况。使用设置为内核调用示例的仪器以及用户空调调用显示了罪魁祸首。 @ 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文档中,它说的是实际的 直到在更多绘图上完成才会发生位复制操作 原始背景。
在我曾经测试的设计案例中,非重用案例需要3392ms才能执行,重用案例需要4693ms(明显更慢)。考虑到每种情况下单个最重的跟踪,内核跟踪表明我们在第一次访问时花费668.0ms零填充新页面,并且在图像获得引用后在第一次写入时写入写入时复制页面1327.0ms那些页面。这相差659ms。仅这一个差异就占了两个案例之间差距的约50%。
因此,要稍微提炼一下,非重用的上下文会更快,因为当您创建上下文时,它知道页面是空的,并且没有其他人提到这些页面以强制它们被复制时你写信给他们。当您重复使用上下文时,页面会被其他人(您创建的图像)引用,并且必须在第一次写入时复制,以便在上下文状态发生变化时保留图像的状态。
通过在调试器中逐步查看进程的虚拟内存映射,您可以进一步了解此处发生的情况。 vmmap
是有用的工具。
实际上,你应该每次只创建一个新的CGContext。
答案 1 :(得分:8)
为了补充@ ipmcc的优秀和彻底的答案,这里是一个教学概述。
在Apple docs for CGBitmapContextCreateImage
中声明:
此函数返回的
CGImage
对象由副本创建 操作。在某些情况下,副本 操作实际上遵循copy-on-write语义,以便实际 只有当底层数据存在时才会发生位的物理副本 位图图形上下文被修改。
因此,当调用此函数时,可能不会立即复制图像的基础位,而是可能在下次修改位图上下文时等待复制。这种位复制可能很昂贵(取决于上下文的大小和颜色空间),并且可能在Instruments配置文件中将其伪装成在上下文中被调用的任何CGContext...
绘制函数的一部分(当位是被迫复制)。这可能是CGContextDrawImage
。k
然而,文档继续这样说:
因此,您可能希望使用生成的图像并发布 在您对位图图形执行其他绘制之前 上下文。这样,就可以避免实际的物理副本了 数据
这意味着如果您需要使用内存中创建的图像(即已将其保存到磁盘,通过网络发送等)已完成,那么您需要执行更多操作在上下文中绘制,图像根本不需要进行物理复制!
如果在某些时候你需要从位图上下文中提取CGImage
, 和 ,则不需要保留对它的任何引用(在您在上下文中进行任何绘制之前,包括将其设置为UIImageView
的图像),那么使用CGBitmapContextCreateImage
可能是个好主意。如果没有,您的图像将在某个时刻进行物理复制,这可能需要一段时间,而且每次只使用新的上下文可能会更好。