【问题标题】:Core Graphics Performance on iOSiOS 上的核心图形性能
【发布时间】:2012-11-07 19:46:02
【问题描述】:

总结

我正在为 iOS 开发一款相当简单的 2D 塔防游戏。

到目前为止,我一直专门使用 Core Graphics 来处理渲染。应用程序中根本没有图像文件(还)。我在进行相对简单的绘图时遇到了一些严重的性能问题,我正在寻找有关如何解决这个问题的想法,而不是转向 OpenGL。

游戏设置

在高层次上,我有一个 Board 类,它是 UIView 的子类,用于表示游戏板。游戏中的所有其他对象(塔、小兵、武器、爆炸等)也是 UIView 的子类,并在创建时作为子视图添加到 Board。

我将游戏状态与对象中的视图属性完全分开,并且每个对象的状态都会在主游戏循环中更新(由NSTimer 以 60-240 Hz 的频率触发,具体取决于游戏速度设置)。该游戏完全可以玩,无需绘制、更新或动画视图。

我使用CADisplayLink 计时器以原生刷新率 (60 Hz) 处理视图更新,该计时器在需要根据游戏状态变化更新其视图属性的棋盘对象上调用 setNeedsDisplay。板上的所有对象都会覆盖drawRect:,以在其框架内绘制一些非常简单的二维形状。因此,例如,当一个武器被激活时,它会根据武器的新状态重新绘制自己。

性能问题

在 iPhone 5 上进行测试,板上总共有大约 2 打游戏对象,帧速率明显低于 60 FPS(目标帧速率),通常在 10-20 FPS 范围内。随着屏幕上的更多动作,它从这里开始走下坡路。而在 iPhone 4 上,情况更糟。

使用仪器我确定只有大约 5% 的 CPU 时间用于实际更新游戏状态——其中绝大多数用于渲染。具体来说,CGContextDrawPath 函数(据我了解,它是矢量路径的光栅化完成的地方)占用了大量的 CPU 时间。有关详细信息,请参阅底部的 Instruments 屏幕截图。

根据对 StackOverflow 和其他网站的一些研究,Core Graphics 似乎无法满足我的需求。显然,描边矢量路径非常昂贵(尤其是在绘制不透明且 alpha 值

问题

我应该考虑哪些优化来尝试从 Core Graphics 中获得流畅的 60 FPS?

一些想法...

有人建议我考虑将我的所有对象都绘制到一个 CALayer 上,而不是让每个对象都单独使用 CALayer,但根据 Instruments 显示的内容,我不相信这会有所帮助。

就我个人而言,我有一个理论,即使用CGAffineTransforms 来制作我的动画(即在drawRect: 中绘制对象的形状一次,然后在后续帧中进行变换以移动/旋转/调整其图层大小)将解决我的问题,因为它们直接基于OpenGL。但我认为这样做不会比直接使用 OpenGL 更容易。

示例代码

为了让您了解我正在做的绘图水平,这里是我的一个武器对象(从塔发射的“光束”)的drawRect: 实现示例。

注意:此光束可以“重新定位”并且它穿过整个电路板,因此为简单起见,它的框架与电路板尺寸相同。然而,板上的大多数其他对象都将其框架设置为可能的最小外接矩形。

- (void)drawRect:(CGRect)rect
{
    CGContextRef c = UIGraphicsGetCurrentContext();

    // Draw beam
    CGContextSetStrokeColorWithColor(c, [UIColor greenColor].CGColor);
    CGContextSetLineWidth(c, self.width);
    CGContextMoveToPoint(c, self.origin.x, self.origin.y);
    CGPoint vector = [TDBoard vectorFromPoint:self.origin toPoint:self.destination];
    double magnitude = sqrt(pow(self.board.frame.size.width, 2) + pow(self.board.frame.size.height, 2));
    CGContextAddLineToPoint(c, self.origin.x+magnitude*vector.x, self.origin.y+magnitude*vector.y);
    CGContextStrokePath(c);

}

仪器运行

让游戏运行一段时间后看看 Instruments:

TDGreenBeam 类具有上面示例代码部分中显示的确切 drawRect: 实现。

Full Size Screenshot

【问题讨论】:

  • +1 表示问题的格式。容易理解。

标签: iphone ios performance core-graphics quartz-2d


【解决方案1】:

核心图形工作由 CPU 执行。然后将结果推送到 GPU。当您致电setNeedsDisplay 时,您表示需要重新进行绘图工作。

假设您的许多对象保持一致的形状并且只是移动或旋转,您只需在父视图上调用setNeedsLayout,然后将该视图的layoutSubviews 中的最新对象位置推送到@987654326 @ 财产。仅仅调整位置不会导致需要重绘的东西;合成器将简单地要求 GPU 再现它在不同位置已有的图形。

除了初始设置之外,更通用的游戏解决方案可能是忽略 centerboundsframe。只需将您想要的仿射变换推送到transform,可能是使用these helpers 的某种组合创建的。这将允许您在没有 CPU 干预的情况下任意重新定位、旋转和缩放对象——这一切都将由 GPU 工作。

如果你想要更多的控制,那么每个视图都有一个CALayer 和它自己的affineTransform,但它们也有一个sublayerTransform,它与子层的变换相结合。因此,如果您对 3d 如此感兴趣,那么最简单的方法是在超层上加载合适的透视矩阵作为 sublayerTransform,然后将合适的 3d 变换推送到子层或子视图。

这种方法有一个明显的缺点,即如果您绘制一次然后按比例放大,您将能够看到像素。您可以提前调整图层的contentsScale 以尝试对此进行改善,但否则您只会看到允许 GPU 继续进行合成的自然结果。如果要在线性过滤和最近过滤之间切换,图层上有一个magnificationFilter 属性;线性是默认值。

【讨论】:

  • 伟大的见解。我怀疑大部分核心图形工作是在 CPU 上完成的,然后 GPU 只是在进行图层合成。我相信这是问题的核心——虽然我的游戏图形非常简单,但仍有大量矢量路径需要在每一帧中更改和再次光栅化。对于 CPU 来说,这似乎是一项不平凡的工作。我认为您的建议是尽可能多地扩展核心图形的好主意,但实际上,看起来我的工作水平需要 OpenGL 将所有图形渲染卸载到 GPU。
  • 更新:我可以通过告诉董事会setNeedsLayout然后更新没有'不需要全面重绘。但仍然不够好,所以我正在使用 GLKit 将所有视图移至 OpenGL。使用this excellent tutorial,我构建了一个简单的游戏引擎来满足我的需求。其实也不是很痛。我已经完成了将所有渲染转移到它的一半,完成后将报告基准和信息。
  • Smileyborg 的过渡情况如何?
  • @JorisWeimar 我最终并没有真​​正完成向 OpenGL 的过渡,因为我遇到了另一个有趣的绊脚石:我的简单 OpenGL 游戏引擎虽然功能强大,但没有很好地设计或优化以最小化 CPU 负载.我发现一旦我移动了大多数游戏对象来使用它而不是 Core Graphics,该应用程序再次受到 CPU 限制,试图为 GPU 提供每帧的新顶点......哎呀。最终搁置了这个项目(无论如何这只是为了好玩/学习)。如果我再次选择这个,iOS 7 的 Sprite Kit 和相关技术将是可行的方法。
【解决方案2】:

很有可能,您正在透支。即绘制冗余信息。

因此,您需要将视图层次结构分解为层(正如您也提到的)。仅更新/绘制需要的内容。这些层可以缓存合成的中间层,然后 GPU 可以快速合成所有这些层。但是你需要小心地只绘制你需要绘制的东西,并且只使实际发生变化的图层区域无效。

调试它:打开“Quartz Debug”并启用“Flash Identical Screen Updates”,然后在模拟器中运行您的应用程序。您想尽量减少这些彩色闪光。

一旦修复了过度绘制,请考虑您可以在辅助线程上渲染什么(例如CALayer.drawsAsynchronously),或者您可以如何处理合成中间表示(例如缓存)或光栅化不变层/矩形。执行这些更改时,请注意衡量成本(例如内存和 CPU)。

【讨论】:

  • 总体上不错的提示,但根据我从仪器运行中看到的情况,这并不能完全解决我的问题。例如,TDGreenBeam 类(我给出的 drawRect: 代码作为示例)需要在每一帧重新绘制自身,因为代表光束的线的宽度(又名笔划)会随着时间而变化——光束变得更细并且变薄,直到它消失。而且由于 Instruments 似乎表明为线条描边矢量路径是一项非常昂贵的操作,我对此无能为力。将不得不采取不同的方法,例如对图层进行变换以动画化“细化”。
  • @smileyborg 这当然不是为了涵盖所有可能的技术。如果那个光束所有的变化,你可以光栅化它后面的所有东西,然后 60 fps 就没有问题了。 Quartz Debug 可以指出这个问题。另外,你能在某些情况下使用 CGContextFillRect 吗?
  • 我将 CGContextFillRect 用于其他形状。你认为将线条画成细长的填充矩形会更快吗? (只是好奇,仅此一项不足以获得我正在寻找的性能。)
  • @smileyborg 我怀疑它会是,但不是一个巨大的收获。尝试和测量并没有什么坏处 - 只需更改几行!
【解决方案3】:

如果您的代码“必须在每一帧上重绘”,那么最好考虑如何使用图形硬件来实现相同的功能,而不是每次都回调到您的代码中重绘 CALayer。

例如,在您的线条示例中,您可以创建由一系列图块组成的渲染逻辑。填充的中心部分可以是一个实体图块,您将其缓存为 CALayer,然后一遍又一遍地绘制以将该区域填充到某个高度。然后,有另一个 CALayer,它是光线的边缘,它的 alpha 淡化为透明,远离光线边缘。在顶部和底部渲染这个 CALayer(旋转 180 度),这样你最终会得到一条具有一定高度的射线,它的上下有一个很好的混合边缘。然后,重复这个过程,使光线变宽然后变短,直到最终结束。

然后您可以使用显卡硬件加速渲染更大的形状,但您的调用代码不需要在每个循环中实际绘制并传输图像数据。每个“瓦片”已经从 CPU 传输到 GPU,GPU 中的仿射变换非常快。基本上,您只是不想每次都渲染然后必须等待所有渲染的图像内存都必须传输到 GPU。

【讨论】:

  • 良好的反馈。 (显而易见的)问题是这类优化并不容易。真正的答案是你真的想利用某种图形引擎,它已经解决了这个问题并以更方便的方式公开它——而 Core Graphics 只是不能胜任这项任务。如果我还在做这个项目,我会转向 iOS 7 中的新 SpriteKit API。
猜你喜欢
  • 2012-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-12-29
  • 1970-01-01
  • 1970-01-01
  • 2016-02-27
  • 1970-01-01
相关资源
最近更新 更多