143

为了澄清这个问题的目的:我知道如何使用子视图和使用 drawRect 创建复杂的视图。我试图完全理解何时以及为什么要使用其中一个。

我也明白提前优化那么多是没有意义的,并且在进行任何分析之前做一些更困难的事情。考虑到我对这两种方法都很满意,现在真的想要更深入的了解。

我的很多困惑来自于学习如何使表格视图滚动性能真正流畅和快速。当然,这个方法的原始来源是来自twitter for iPhone(以前的 tweetie)背后的作者。基本上它说要使表格滚动流畅,秘诀是不使用子视图,而是在一个自定义 uiview 中完成所有绘图。从本质上讲,使用大量子视图似乎会减慢渲染速度,因为它们有很多开销,并且不断地在它们的父视图上重新合成。

公平地说,这是在 3GS 还很新的时候写的,从那时起 iDevices 变得更快了。仍然互联网和其他地方经常建议这种方法用于高性能表。事实上,这是Apple 的 Table Sample Code中建议的方法,已在多个 WWDC 视频(iOS 开发者实用绘图)和许多 iOS编程书籍中提出。

甚至还有一些很棒的工具来设计图形并为它们生成核心图形代码。

所以起初我被引导相信“Core Graphics 的存在是有原因的。它很快!”

但是,一旦我想到“尽可能使用核心图形”的想法,我就开始看到 drawRect 通常是导致应用程序响应能力差的原因,在内存方面非常昂贵,并且确实对 CPU 造成了负担。基本上,我应该“避免覆盖 drawRect ”(WWDC 2012 iOS App Performance: Graphics and Animations

所以我想,就像所有事情一样,它很复杂。也许您可以帮助自己和其他人了解使用 drawRect 的时间和原因?

我看到一些使用 Core Graphics 的明显情况:

  1. 您有动态数据(Apple 的股票图表示例)
  2. 你有一个灵活的 UI 元素,不能用简单的可调整大小的图像来执行
  3. 您正在创建一个动态图形,该图形一旦渲染就会在多个地方使用

我看到了避免使用核心图形的情况:

  1. 您的视图的属性需要单独设置动画
  2. 您的视图层次结构相对较小,因此任何使用 CG 的额外努力都不值得
  3. 您想更新视图的各个部分而不重绘整个视图
  4. 当父视图大小发生变化时,您的子视图的布局需要更新

所以赐予你的知识。在什么情况下你会使用 drawRect/Core Graphics(也可以通过子视图来实现)?是什么因素导致你做出这个决定?如何/为什么建议在一个自定义视图中绘制黄油般平滑的表格单元格滚动,但出于性能原因,Apple 建议不要使用 drawRect?那么简单的背景图片呢(你什么时候用 CG 创建它们而不是使用可调整大小的 png 图片)?

制作有价值的应用程序可能不需要对这个主题有深入的了解,但我不喜欢在无法解释原因的技术之间进行选择。我的大脑生我的气。

问题更新

谢谢大家的信息。这里有一些澄清问题:

  1. 如果您正在使用核心图形绘制一些东西,但可以使用 UIImageViews 和预渲染的 png 完成同样的事情,您是否应该一直走这条路?
  2. 一个类似的问题:尤其是像这样的坏蛋工具,你什么时候应该考虑在核心图形中绘制界面元素?(可能当您的元素的显示是可变的。例如,具有 20 种不同颜色变化的按钮。还有其他情况吗?)
  3. 鉴于我在下面的回答中的理解,是否可以通过在复杂的 UIView 渲染本身之后有效地捕获单元格的快照位图并在滚动和隐藏复杂视图时显示它来获得表格单元格的相同性能提升?显然,有些部分必须解决。只是我的一个有趣的想法。
4

4 回答 4

75

尽可能坚持使用 UIKit 和子视图。您可以提高工作效率,并利用所有更易于维护的 OO 机制。当您无法从 UIKit 中获得所需的性能时,请使用 Core Graphics,或者您知道尝试在 UIKit 中组合绘图效果会更加复杂。

一般的工作流程应该是用子视图构建表格视图。使用 Instruments 测量您的应用将支持的最旧硬件上的帧速率。如果您无法获得 60fps,请下拉到 CoreGraphics。当你这样做了一段时间后,你就会感觉到 UIKit 什么时候可能是在浪费时间。

那么,为什么 Core Graphics 快?

CoreGraphics 的速度并不快。如果它一直在使用,你可能会变慢。它是一个丰富的绘图 API,需要在 CPU 上完成它的工作,而不是大量 UIKit 的工作被卸载到 GPU 上。如果您必须为在屏幕上移动的球设置动画,那么每秒 60 次在视图上调用 setNeedsDisplay 将是一个糟糕的主意。因此,如果您的视图的子组件需要单独设置动画,则每个组件都应该是一个单独的层。

另一个问题是,当您不使用 drawRect 进行自定义绘图时,UIKit 可以优化库存视图,因此 drawRect 是无操作的,或者它可以使用合成的捷径。当你覆盖 drawRect 时,UIKit 不得不走慢路,因为它不知道你在做什么。

在表格视图单元格的情况下,这两个问题可能会被好处所抵消。当视图首次出现在屏幕上时调用 drawRect 后,内容被缓存,滚动是由 GPU 执行的简单翻译。因为您处理的是单个视图,而不是复杂的层次结构,所以 UIKit 的 drawRect 优化变得不那么重要了。所以瓶颈变成了你可以优化你的核心图形绘图。

只要有可能,就使用 UIKit。做最简单的有效实现。轮廓。当有激励时,优化。

于 2013-02-02T18:32:34.717 回答
57

不同之处在于 UIView 和 CALayer 本质上处理的是固定图像。这些图像被上传到显卡(如果你了解 OpenGL,可以将图像视为纹理,将 UIView/CALayer 视为显示此类纹理的多边形)。一旦图像在 GPU 上,它可以非常快速地绘制,甚至可以绘制多次,并且(具有轻微的性能损失)即使在其他图像之上具有不同级别的 alpha 透明度。

CoreGraphics/Quartz 是一个用于生成图像的 API 。它需要一个像素缓冲区(同样,想想 OpenGL 纹理)并更改其中的单个像素。这一切都发生在 RAM 和 CPU 中,只有在 Quartz 完成后,图像才会“刷新”回 GPU。这种从 GPU 获取图像、对其进行更改、然后将整个图像(或至少是相当大的一部分)上传回 GPU 的往返过程相当缓慢。此外,Quartz 所做的实际绘图虽然对你正在做的事情来说非常快,但比 GPU 所做的要慢得多。

这很明显,考虑到 GPU 主要是在大块不变的像素周围移动。Quartz 随机访问像素并与网络、音频等共享 CPU。此外,如果您同时使用 Quartz 绘制多个元素,则必须在其中一个更改时重新绘制所有元素,然后上传整个块,而如果您更改一个图像,然后让 UIViews 或 CALayers 将其粘贴到您的其他图像上,您可以向 GPU 上传更少量的数据。

当你不实现 -drawRect: 时,大多数视图都可以被优化掉。它们不包含任何像素,因此无法绘制任何内容。其他视图,如 UIImageView,仅绘制 UIImage(同样,它本质上是对纹理的引用,可能已经加载到 GPU 上)。所以如果你用一个 UIImageView 绘制同一个 UIImage 5 次,它只上传到 GPU 一次,然后在 5 个不同的位置绘制到显示器上,节省了我们的时间和 CPU。

当您实现 -drawRect: 时,这会导致创建一个新图像。然后,您使用 Quartz 在 CPU 上绘制它。如果您在 drawRect 中绘制 UIImage,它可能会从 GPU 下载图像,将其复制到您正在绘制的图像中,完成后,将图像的第二个副本上传回显卡。因此,您在设备上使用了两倍的 GPU 内存。

所以最快的绘制方法通常是将静态内容与变化的内容分开(在单独的 UIViews/UIView 子类/CALayers 中)。将静态内容加载为 UIImage 并使用 UIImageView 进行绘制,并将运行时动态生成的内容放入 drawRect 中。如果您有重复绘制的内容,但其本身并没有改变(即,在同一插槽中显示的 3 个图标以指示某些状态)也使用 UIImageView。

一个警告:有太多的 UIViews 这样的事情。特别是透明区域在绘制时对 GPU 的影响更大,因为它们在显示时需要与它们后面的其他像素混合。这就是为什么您可以将 UIView 标记为“不透明”,以向 GPU 表明它可以消除该图像后面的所有内容。

If you have content that is generated dynamically at runtime but stays the same for the duration of the application's lifetime (e.g. a label containing the user name) it may actually make sense to just draw the whole thing once using Quartz, with the text, the button border etc., as part of the background. But that's usually an optimization that's not needed unless the Instruments app tells you differently.

于 2014-04-10T10:23:09.323 回答
26

我将尝试总结我从其他人的答案中推断出来的内容,并在对原始问题的更新中提出澄清问题。但我鼓励其他人继续提供答案,并投票给那些提供了良好信息的人。

一般的做法

很明显,正如 Ben Sandofsky 在他的回答中提到的那样,一般方法应该是“只要有可能,就使用 UIKit。做最简单的可行实现。配置文件。当有激励时,优化。”

为什么

  1. iDevice 中可能存在两个主要瓶颈,CPU 和 GPU
  2. CPU 负责视图的初始绘制/渲染
  3. GPU负责大部分动画(Core Animation)、图层效果、合成等。
  4. UIView 有很多优化、缓存等,内置用于处理复杂的视图层次结构
  5. 当覆盖 drawRect 时,你会错过 UIView 提供的许多好处,而且它通常比让 UIView 处理渲染要慢。

在一个平面 UIView 中绘制单元格内容可以大大提高您在滚动表上的 FPS。

就像我上面说的,CPU 和 GPU 是两个可能的瓶颈。由于它们通常处理不同的事情,因此您必须注意您遇到的瓶颈。在滚动表格的情况下,并不是 Core Graphics 的绘制速度更快,这就是为什么它可以大大提高您的 FPS。

事实上,Core Graphics 的初始渲染可能比嵌套的 UIView 层次结构要慢得多。但是,滚动不连贯的典型原因似乎是您遇到了 GPU 的瓶颈,因此您需要解决这个问题。

为什么覆盖drawRect(使用核心图形)可以帮助表格滚动:

据我了解,GPU 不负责视图的初始渲染,而是在渲染后处理纹理或位图,有时还具有一些图层属性。然后它负责合成位图、渲染所有这些图层效果以及大部分动画(核心动画)。

在表格视图单元的情况下,GPU 可能会遇到复杂的视图层次结构的瓶颈,因为它不是为一个位图设置动画,而是为父视图设置动画,并进行子视图布局计算、渲染层效果和合成所有子视图。因此,它不是为一个位图设置动画,而是负责同一像素区域的一组位图之间的关系,以及它们如何交互。

总而言之,使用核心图形在一个视图中绘制单元格可以加快表格滚动的原因不是因为它绘制得更快,而是因为它减少了 GPU 上的负载,这是在特定情况下给你带来麻烦的瓶颈.

于 2013-02-03T00:43:44.937 回答
6

我是一名游戏开发人员,当我的朋友告诉我基于 UIImageView 的视图层次结构会减慢我的游戏速度并使其变得糟糕时,我也在问同样的问题。然后我继续研究我能找到的关于是否使用 UIViews、CoreGraphics、OpenGL 或像 Cocos2D 之类的第 3 方的所有内容。我从 WWDC 的朋友、老师和 Apple 工程师那里得到的一致回答是,最终不会有太大的不同,因为在某种程度上他们都在做同样的事情。UIViews 等高级选项依赖于 CoreGraphics 和 OpenGL 等低级选项,只是它们被包装在代码中,以便您更容易使用。

如果您最终要重新编写 UIView,请不要使用 CoreGraphics。但是,您可以通过使用 CoreGraphics 获得一些速度,只要您在一个视图中完成所有绘图,但这真的值得吗?我找到的答案通常是否定的。当我第一次开始我的游戏时,我使用的是 iPhone 3G。随着我的游戏变得越来越复杂,我开始看到一些滞后,但使用更新的设备时完全不明显。现在我有很多动作要进行,唯一的延迟似乎是在 iPhone 4 上玩最复杂的关卡时会下降 1-3 fps。

尽管如此,我还是决定使用 Instruments 来查找占用时间最多的功能。我发现这些问题与我对 UIViews 的使用无关。相反,它反复调用 CGRectMake 进行某些碰撞感应计算,并为使用相同图像的某些类分别加载图像和音频文件,而不是让它们从一个中央存储类中提取。

所以最后,你可能会从使用 CoreGraphics 中获得一点好处,但通常不值得,或者根本没有任何效果。我唯一一次使用 CoreGraphics 是在绘制几何形状而不是文本和图像时。

于 2013-02-02T07:52:06.260 回答