绘制 Rect 还是不绘制 Rect (什么时候应该使用 draRect/Core Graphics,而不是子视图/图像,为什么?)

为了澄清这个问题的目的: 我知道如何使用 subview 和 draRect 创建复杂的视图。我正在努力完全理解什么时候和为什么要用一个而不是另一个。

我也明白,提前那么多时间进行优化,并在进行任何分析之前以更困难的方式进行一些工作,这是没有意义的。考虑到我对这两种方法都很满意,现在真的需要更深入的理解。

我的许多困惑来自于学习如何使表格视图滚动性能真正平滑和快速。当然,这个方法的原始源代码来自 iPhone 的 推特背后的作者(以前是 Twittie)。从本质上看,使用大量的子视图会降低渲染速度,因为它们有大量的开销,并且不断地在其父视图上重新组合。

公平地说,这是在3GS 刚刚崭露头角的时候写的,从那时起,苹果设备已经变得更快了。对于高性能表来说,这种方法仍然是 互联网和其他地方的 经常 建议。事实上,它是 苹果的表示例代码中的一个建议方法,已经在几个 WWDC 视频(IOS 开发者实用绘图)和许多 iOS编程书籍中被建议过。

甚至还有 好看的工具来设计图形并为其生成核心图形代码。

所以一开始我相信“核心图形的存在是有原因的,它是快速的!”

但是一旦我想到“尽可能支持核心图形”这个想法,我就开始发现 draRect 通常会导致应用程序的响应性差,内存非常昂贵,并且真的会让 CPU 承担很大的负担。基本上,我应该“ 避免重写”(WWDC 2012 IOS 应用程序性能: 图形和动画)

所以我想,一切都很复杂。也许你可以帮助我和其他人理解使用 draRect 的时间和原因?

我看到了一些使用核心图形的明显情况:

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

我看到的情况是避免核心图形:

  1. 视图的属性需要单独进行动画处理
  2. 您有一个相对较小的视图层次结构,因此任何使用 CG 的额外努力都是不值得的
  3. 您希望在不重绘整个视图的情况下更新视图的各个部分
  4. 当父视图大小改变时,子视图的布局需要更新

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

制作有价值的应用程序可能不需要对这个主题有深入的理解,但是我不喜欢在不能解释原因的情况下在技术之间做出选择。我的大脑在生我的气。

问题更新

感谢大家提供的信息,这里有一些澄清的问题:

  1. 如果您正在用核心图形绘制某些东西,但是可以用 UIImageView 和预先呈现的 png 完成同样的事情,那么您是否应该总是沿着这条路线?
  2. 一个类似的问题: 特别是在 像这样厉害的工具中,什么时候应该考虑在核心图形中绘制界面元素?(可能当元素的显示是可变的时候。例如一个有20种不同颜色变化的按钮。还有别的案子吗?)
  3. 考虑到我在下面的回答中的理解,是否可以通过在复杂 UIView 渲染之后有效地捕获单元格的快照位图,并在滚动和隐藏复杂视图时显示该位图,从而获得与表单元格相同的性能提升?很明显,有些部分需要解决。我有个有趣的想法。
18496 次浏览

我是一个游戏开发者,当我的朋友告诉我我的基于 UIImageView 的视图层次结构将会减慢我的游戏并且使它变得很糟糕的时候,我正在问同样的问题。然后我开始研究我能找到的关于是否使用 UIView,CoreGraphics,OpenGL 或者像 Cocos2D 这样的第三方软件的一切。我从朋友、老师和 WWDC 的苹果工程师那里得到的一致答案是,最终不会有太大的区别,因为 在某种程度上,它们都在做同样的事情。较高级别的选项(如 UIView)依赖于较低级别的选项(如 CoreGraphics 和 OpenGL) ,只是这些选项包装在代码中,使您更容易使用。

不要使用 CoreGraphics,如果你只是要重写 UIView。但是,您可以从使用 CoreGraphics 获得一些速度,只要您在一个视图中完成所有绘图,但它真的是 价值吗?我找到的答案通常是否定的。当我第一次开始我的游戏时,我使用的是 iPhone 3G。随着我的游戏越来越复杂,我开始看到一些延迟,但随着新的设备,它是完全不可察觉的。现在我有很多动作要做,唯一的延迟似乎是在最复杂的水平上玩 iPhone4时每秒1-3格的下降。

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

所以最后,您可能会从使用 CoreGraphics 中获得一些收益,但是通常它不值得使用,或者可能根本没有任何效果。我唯一使用 CoreGraphics 的时候是在绘制 Unicode几何图形列表而不是文字和图像的时候。

只要有可能,就坚持使用 UIKit 和子视图。您可以提高工作效率,并利用所有应该更容易维护的面向对象机制。当你无法从 UIKit 中获得所需的性能时,或者你知道在 UIKit 中尝试整合绘图效果会更加复杂时,使用 Core Graphics。

一般的工作流程应该是使用子视图构建表视图。使用 Instruments 来测量应用程序支持的最老硬件的帧速率。如果你不能达到60fps 的速度,下降到 CoreGraphics。当您这样做了一段时间之后,您就会感觉到 UIKit 可能是在浪费时间。

那么,为什么核心图形速度很快呢?

CoreGraphics 并不快。如果它一直被使用,你可能会慢下来。它是一个丰富的绘图 API,需要在 CPU 上完成它的工作,而不是将许多 UIKit 工作卸载到 GPU 上。如果你必须让一个球在屏幕上移动,那么每秒钟调用 setNeedsDisplay 60次是一个糟糕的主意。因此,如果视图中有需要单独动画的子组件,那么每个组件应该是一个单独的层。

另一个问题是,当您不使用 draRect 进行自定义绘图时,UIKit 可以优化股票视图,因此 draRect 是不可操作的,或者它可以使用合成的快捷方式。当您覆盖 draRect 时,UIKit 必须采用缓慢的路径,因为它不知道您在做什么。

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

无论何时,只要可以,使用 UIKit。做最简单的实现工作。配置文件。当有激励,优化。

我会试着总结一下我从其他人的回答中推断出来的东西,并在更新原始问题时提出一些澄清性的问题。但我鼓励其他人继续寻找答案,并为那些提供了有用信息的人投票。

一般方法

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

为什么

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

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

如上所述,CPU 和 GPU 是两个可能的瓶颈。因为它们通常处理不同的事情,所以您必须注意您所遇到的瓶颈。在滚动表的情况下,并不是核心图形绘制速度更快,这就是为什么它可以大大提高你的 FPS。

实际上,对于初始呈现,核心图形可能比嵌套的 UIView 层次结构慢得多。然而,这似乎是典型的原因,不稳定的滚动是你是瓶颈的 GPU,所以你需要解决这个问题。

为什么重写 draRect (使用核心图形)有助于表滚动:

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

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

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

区别在于 UIView 和 CALayer 基本上处理的是固定的图像。这些图像被上传到图形卡(如果您知道 OpenGL,可以将图像想象为一个纹理,将 UIView/CALayer 想象为一个显示这种纹理的多边形)。一旦图像在 GPU 上显示,它可以非常快速地绘制,甚至可以多次绘制,而且即使在其他图像之上添加不同级别的透明度,也可以(带来轻微的性能损失)。

CoreGraphics/Quartz 是用于 产生图像的 API。它需要一个像素缓冲区(再想想 OpenGL 纹理) ,并在其中更改单个像素。这一切都发生在 RAM 和 CPU 上,只有当 Quartz 完成后,映像才会“刷新”回 GPU。这种从 GPU 获取图像、修改图像、然后将整个图像(或至少是相对较大的一块)上传回 GPU 的往返过程相当缓慢。此外,石英的实际绘图,虽然真的很快,你在做什么,是远远慢于 GPU 的做法。

这是显而易见的,考虑到 GPU 主要是移动不变的像素大块。Quartz 实现像素的随机访问,并与网络、音频等共享 CPU。另外,如果你同时使用 Quartz 绘制几个元素,你必须在一个元素改变时重新绘制所有元素,然后上传整个块,而如果你改变一个图像,然后让 UIView 或 CALayers 粘贴到你的其他图像上,你可以上传少得多的数据到 GPU。

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

当您实现 -draRect: 时,这将导致创建一个新的映像。然后使用 Quartz 在 CPU 上进行绘制。如果你在 DrawRect 中绘制一个 UIImage,它很可能会从 GPU 下载图像,将其复制到你正在绘制的图像中,一旦完成,就会将图像的 第二份上传回显卡。所以你在设备上使用了两倍的 GPU 内存。

因此,最快的绘制方法通常是将静态内容与正在更改的内容分开(在单独的 UIView/UIView 子类/CALayers 中)。将静态内容加载为 UIImage 并使用 UIImageView 绘制它,然后将在运行时动态生成的内容放在 draRect 中。如果你有重复绘制的内容,但是内容本身并没有改变(比如3个图标在同一个插槽中显示以表示某种状态) ,那么也可以使用 UIImageView。

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

如果你的内容是在运行时动态生成的,但是在应用程序的生命周期内保持不变(比如包含用户名的标签) ,那么使用 Quartz、文本、按钮边框等作为背景的一部分来绘制整个内容实际上是有意义的。但是,除非 Instruments 应用程序告诉您不同的结果,否则通常不需要进行这种优化。