是否使用drawRect(何时使用drawRect/Core Graphics而不是子视图/图像?)

143
为了澄清这个问题的目的:我知道如何使用子视图和使用drawRect创建复杂的视图。我正在尝试充分理解何时以及为什么要使用其中之一。
我也明白事先优化并不是很有意义,在进行任何性能分析之前就采用更困难的方法。请考虑我对这两种方法都很熟悉,现在真正想要深入了解。
我的许多困惑来自于学习如何使表格视图的滚动性能非常流畅和快速。当然,这种方法的原始来源是Twitter背后的作者为iPhone(曾用名Tweetie)开发的。基本上,它说要使表格滚动变得非常流畅,秘诀是不要使用子视图,而是在一个自定义的UIview中进行所有绘图。实质上,似乎使用大量的子视图会减慢渲染速度,因为它们具有很多开销,并且不断重新合成其父视图。

公正地说,这是在3GS相当新的时候编写的,而且自那时以来iDevices已经变得更快了。尽管如此,这种方法仍然被定期推荐用于高性能表格,在互联网上和其他地方。实际上,它是苹果表格示例代码中的一个建议方法(Apple's Table Sample Code),在几个WWDC视频(Practical Drawing for iOS Developers)和许多iOS编程书籍中也被建议。

有一些非常棒的工具可以用来设计图形并生成相应的Core Graphics代码。

所以一开始我认为 "Core Graphics之所以存在是因为它很快!"

但是当我认为自己已经理解了 "尽可能使用Core Graphics" 这个想法时,我开始意识到drawRect通常会导致应用程序响应不佳,内存开销极大,并且会对CPU造成很大负担。基本上,我应该 "避免覆盖drawRect"(WWDC 2012 iOS App Performance: Graphics and Animations

所以,我想,像所有事情一样,这也很复杂。也许你可以帮助我和其他人理解何时何地使用drawRect呢?

我看到了几种明显的使用Core Graphics的情况:

  1. 当你有动态数据时(例如苹果的股票图表示例)
  2. 当你有一个灵活的UI元素,不能用简单的可调整大小的图像来执行
  3. 当你创建一个动态图形,一旦呈现出来就可以在多个地方使用

我认为应该避免使用核心图形(Core Graphics)的情况:

  1. 您的视图属性需要单独进行动画处理
  2. 您的视图层次结构相对较小,因此使用CG的任何感知额外努力都不值得收益
  3. 您想要更新视图的某些部分而不重新绘制整个视图
  4. 当父视图大小更改时,子视图的布局需要更新

那么,在什么情况下您会选择drawRect / Core Graphics(也可以通过子视图完成)?是什么因素导致您做出这个决定?为什么在一种自定义视图中绘制可以实现流畅的表格单元格滚动,但苹果通常建议出于性能原因避免使用drawRect?对于简单的背景图像(何时使用CG创建它们而不是使用可调整大小的png图像)?

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

问题更新

感谢大家提供的信息。这里有一些澄清问题:
  1. 如果您使用核心图形绘制某些内容,但可以通过UIImageViews和预渲染的png实现相同的效果,您是否应该始终选择后者?
  2. 类似的问题:特别是使用如此强大的工具时,何时应考虑使用核心图形来绘制界面元素?(可能是当您的元素的显示是可变的时候,例如具有20种不同颜色变化的按钮。还有其他情况吗?)
  3. 根据我在下面回答的理解,通过在复杂的UIView渲染自身后有效地捕获单元格快照位图,并在滚动时显示该快照并隐藏复杂的视图,是否可能获得相同的性能提升?显然,某些问题必须解决。这只是我想到的一个有趣的想法。
4个回答

75

尽可能使用UIKit和子视图。这样可以更高效,利用所有的面向对象机制,使维护变得更容易。如果无法从UIKit中获得所需的性能,或者在UIKit中尝试混合绘图效果会更加复杂,请使用Core Graphics。

一般的工作流程应该是使用子视图构建表视图。使用Instruments测量您的应用程序将支持的最老硬件的帧速率。如果无法达到60fps,请降级到Core Graphics。当您这样做了一段时间后,您就会有一种感觉,知道何时使用UIKit可能是浪费时间。

那么,为什么Core Graphics很快?

CoreGraphics并不真正快。如果它一直被使用,你可能会很慢。它是一个丰富的绘图API,需要在CPU上完成其工作,而不是很多UIKit工作,它们被卸载到GPU上。如果您必须动画地球越过屏幕,每秒钟调用setNeedsDisplay 60次将是一个可怕的想法。因此,如果您的视图具有子组件,需要分别进行动画处理,则每个组件都应该是单独的图层。

另一个问题是,当您没有使用drawRect进行自定义绘制时,UIKit可以优化存储视图,使drawRect成为no-op,或者使用合成快捷方式。当您覆盖drawRect时,UIKit必须采取慢速路径,因为它不知道您在做什么。

这两个问题可以在表视图单元格的情况下被权衡。当视图首次出现在屏幕上时,会调用drawRect,内容会被缓存,并且滚动是由GPU执行的简单翻译。由于您处理的是单个视图,而不是复杂的层次结构,UIKit的drawRect优化变得不那么重要。因此,瓶颈变成了您可以优化的Core Graphics绘制量。

只要可以,请使用UIKit。进行最简单的实现。进行性能分析。在有激励的情况下,进行优化。


58
UIView和CALayer的区别在于它们基本上处理固定图像。这些图像被上传到图形卡(如果您知道OpenGL,则将图像视为纹理,而UIView / CALayer则显示此类纹理的多边形)。一旦图像在GPU上,就可以非常快地绘制甚至多次,并且甚至可以在其他图像的顶部带有不同级别的alpha透明度(具有轻微的性能损失)。
CoreGraphics / Quartz是用于生成图像的API。它接受一个像素缓冲区(再次,将OpenGL纹理视为参考),并更改其中的单个像素。所有这些都发生在RAM和CPU上,只有在Quartz完成后,图像才会“刷新”回GPU。从GPU获取图像,更改它,然后将整个图像(或者至少是相对较大的一块)上传回GPU的往返速度相当慢。此外,Quartz实际进行的绘制虽然对于您所做的事情真的很快,但比GPU慢得多。
这很明显,因为GPU大多数情况下都是移动大块的未更改像素。 Quartz随机访问像素并与网络,音频等共享CPU。此外,如果您有几个元素同时使用Quartz绘制,则在更改一个元素时必须重新绘制所有元素,然后上传整个块,而如果您更改一个图像,然后让UIView或CALayer将其粘贴到其他图像上,您可以通过向GPU上传较小的数据量来摆脱它们。
当您没有实现-drawRect:时,大多数视图都可以进行优化。它们不包含任何像素,因此无法绘制任何内容。其他视图,如UIImageView,仅绘制UIImage(再次,本质上是对纹理的引用,该纹理可能已加载到GPU中)。因此,如果使用UIImageView 5次绘制相同的UIImage,则它只会被上传一次到GPU,然后在5个不同的位置绘制到显示器上,节省时间和CPU。

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

因此,最快速的绘制方式通常是将静态内容与可变内容分开(在不同的UIView / UIView子类 / CALayer中)。将静态内容作为UIImage加载,并使用UIImageView进行绘制,在运行时生成动态内容并放置在drawRect中。如果有多个相同的内容需要重复绘制(例如3个图标表示某种状态),也可以使用UIImageView。

注意:拥有太多的UIView可能会导致问题。特别是透明区域对GPU的负荷更大,因为它们需要与其背后的其他像素混合显示。这就是为什么可以将UIView标记为“opaque”,以告诉GPU它可以完全覆盖该图像背后的一切。

如果有在运行时生成的内容,但它在应用程序的整个生命周期内保持不变(例如包含用户姓名的标签),实际上可能有意义的做法是使用Quartz一次性绘制整个内容,包括文本、按钮边框等作为背景。但除非Instruments应用程序告诉你不同,否则通常不需要这种优化。


感谢您提供详细的答案。希望更多的人能够向下滚动并点赞。 - Bob Spryn
希望我能给这个点赞两次。感谢你的精彩写作! - Sea Coast of Tibet
稍作更正:在大多数情况下,GPU 没有下载操作,但上传仍然基本上是您可以要求 GPU 执行的最慢操作,因为它必须将大型像素缓冲区从较慢的系统 RAM 传输到更快的 VRAM 中。另一方面,一旦图像到达那里,移动它只涉及向 GPU 发送一组新的坐标(几个字节),以便它知道在哪里绘制。 - uliwitness
@uliwitness,我正在阅读你的答案,你提到将对象标记为“opaque obliterates everything behind that image”。你能否请详细解释一下这部分内容? - Rikh
将一个图层标记为不透明意味着a)在大多数情况下,GPU将不会绘制位于该图层下面的其他图层,至少是那些位于不透明图层下方的部分。即使它们已经被绘制,它也不会实际执行alpha混合,而只会将顶部图层的内容复制到屏幕上(通常在不透明图层中完全透明的像素最终会变成黑色或者至少是它们颜色的不透明版本)。 - uliwitness
如果一个图层没有标记为不透明,GPU 将假定它包含至少一个透明像素,因此将首先绘制它下面的所有图层,然后在它们的顶部绘制该图层的内容。如果图层中的所有像素实际上都是不透明的,那么绘制底部图层就是浪费时间,因为它们的内容实际上并没有显示出来。 - uliwitness

26

我将试图总结我从其他答案中推断出的内容,并在原问题的更新中提出澄清问题。但我鼓励其他人继续回答,并投票支持那些提供了有用信息的人。

一般方法

显而易见的是,正如Ben Sandofsky在他的答案中提到的那样,一般方法应该是“只要可以,就使用UIKit。实现最简单的方式。优化时请进行分析。”

为什么

  1. 在iDevice中,有两个主要可能成为瓶颈的部分,一个是CPU,另一个是GPU
  2. CPU负责视图的初始绘制/渲染
  3. GPU负责大部分动画(Core Animation)、图层效果、合成等操作
  4. UIView具有许多针对处理复杂视图层次结构的优化、缓存等内置功能
  5. 当覆盖drawRect时,会错过许多UIView提供的好处,而且通常比让UIView处理渲染更慢。
在一个平面UIView中绘制单元格内容可以极大地提高滚动表的FPS。如上所述,CPU和GPU是两个可能的瓶颈。由于它们通常处理不同的事情,因此您必须注意您遇到的瓶颈是哪一个。在滚动表的情况下,并不是核心图形绘制得更快,这就是为什么它可以极大地提高您的FPS。实际上,对于初始渲染,核心图形可能比嵌套的UIView层次结构要慢。然而,似乎导致卡顿滚动的典型原因是GPU达到了瓶颈,因此您需要解决这个问题。
为什么重写drawRect(使用核心图形)可以帮助表格滚动:
据我所知,GPU不负责视图的初始渲染,而是在它们被渲染后交付纹理或位图,有时还会带有一些图层属性。然后,它负责合成位图,渲染所有这些图层效果和大部分动画(核心动画)。
在表视图单元格的情况下,GPU 可能会受到复杂视图层次结构的瓶颈影响,因为它不是动画化一个位图,而是动画化父视图,并进行子视图布局计算、渲染层效果和合成所有子视图。所以,它不是负责动画化一个位图,而是负责一堆位图之间的关系以及它们如何相互作用,对于相同的像素区域。 因此,总结一下,在一个视图中使用核心图形来绘制单元格加快表滚动的原因并不是因为它绘制得更快,而是因为它减少了 GPU 的负载,这是在特定情况下给您带来麻烦的瓶颈。

1
要小心。GPU 对于每一帧都会重新组合整个图层树。因此,移动一个图层实际上是零成本的。如果您创建了一个大图层,则仅移动该图层比移动多个图层更快。在此图层内部移动某些内容突然涉及到 CPU 并具有成本。由于单元格内容通常不会经常更改(仅响应相对较少的用户操作),因此使用较少的视图可以加快速度。但是,制作一个包含所有单元格的大视图将是一个糟糕的想法™。 - uliwitness

6
我是一名游戏开发者,当我的朋友告诉我,基于UIImageView的视图层级会减慢我的游戏进程并使其变得糟糕时,我也开始产生了同样的疑问。随后,我开始尽可能地调查有关使用UIView、CoreGraphics、OpenGL或类似Cocos2D这样的第三方工具的相关问题。在与我的朋友、老师和苹果工程师参加的WWDC活动中,我得到的一致答案是,最终并没有太大区别,因为它们都在某个层面上做着同样的事情。高级选项如UIView依赖于低级选项如CoreGraphics和OpenGL,只是包装在代码中,以使您更容易使用。
如果您最终会重新编写UIView,则不要使用CoreGraphics。然而,只要您在一个视图中完成所有绘制,就可以通过使用CoreGraphics来获得一些速度提升,但这是否真的值得呢?我发现通常情况下不值得。当我开始制作游戏时,我正在使用iPhone 3G。随着我的游戏变得越来越复杂,我开始看到一些延迟,但在新设备上,这完全无法察觉。现在我有很多动作,唯一的延迟似乎是在iPhone 4上玩最复杂的关卡时下降了1-3帧。
但我还是决定使用Instruments工具找出占用时间最长的功能。我发现问题与使用UIView无关。相反,它总是针对某些碰撞感应计算反复调用CGRectMake函数,在某些类中单独加载图像和音频文件,而不是从一个中央存储类中绘制。
因此,最终您可能会通过使用CoreGraphics获得轻微的收益,但通常不值得或根本没有任何效果。我只有在绘制几何图形而不是文本和图像时才使用CoreGraphics。

8
我认为你可能把"核心动画(Core Animation)"和"核心图像(Core Graphics)"混淆了。 核心图像是一种非常慢的软件绘制方式,除非你重写drawRect方法,否则不会用于绘制常规UIView。而核心动画是硬件加速的、快速的,并且负责大部分屏幕上所见的内容。 - Nick Lockwood

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接