原文链接 http://floriankugler.com/2013/05/24/layer-trees-vs-flat-drawing-graphics-performance-across-ios-device-generations/
犹如黄油般顺滑的滚动视图,是许多iOS开发者引以为傲的地方,也是一些优秀iOS应用的代表。我以前并不是开发iOS平台的,但是如今的观点给我的感觉是,Loren Brichter所写的Tweetie是许多成功的app之一,这些app推崇一种艺术,那就是充分利用设备所提供的每1bit的图像性能。Loren非常乐于分享他的技术,通过Core Graphics把每个cell绘制成一个单独的bitmap,这样GPU就可以做它最擅长的事情——将不透明的纹理组合起来。
这可以追溯到2008年,那时候iPhone 3G刚刚问世,第一代的iPhone并不是一台你立刻会想丢弃的设备。那时候高清屏也还未出现。最近,Twitter的开发团队发布了一篇关于让滚动视图顺滑的文章,同样的包含了将table view cells渲染成平面的位图这一技术。
尽管起初的iPhone,设备CPU跟GPU的能力都极大的提高了,但同时,例如高清显示这样的新的硬件特性又对它们提出了更高的要求。软件方面也发生了一些变化,例如,CAGradientLayer 随着iOS 3被引进。伴随着这些发展,我很好奇,为了获得更好的性能,那套将视图绘制成平面的位图的旧机制是否还能起作用。
在这篇文章里,我将展示一个简单的示例结果,该示例被分别运行在iPhone 3G,4, 4S 以及5还有iPad3,iPad mini和iPad 4。根据这些结果我将讨论根据不同的使用场景,究竟哪一种策略更可能有最佳的结果。
第一次渲染 & 层动画
平顺的table view滚动有两个要素。首先你需要能够在1/60秒(大约是16毫秒)把一个新的table view cell显示在屏幕上,为了真正快速的滚动,也可能需要显示多于一个cell。其次,你需要能够以大约60帧每秒的速率将已经显示在屏幕上的cells进行移动。上述的第一个方面根据你的代码,跟CPU以及GPU有不同程度的关联,然而第二个方面主要与GPU有关(假设你没有修改已经显示在屏幕中的cells)。
我想要以各自独立的形式来测试所有图像性能的方面。因此我创建了一个简单的app,能够像这样来渲染一个view:
这个view高度100,宽度在iPhone跟iPad上面都是全屏。它包含了:一个不透明的渐变背景,左边是一个100x100的图片,两个带有透明背景的label。我创建了这个view的三个版本,它们的输出都是一样的。
第一个版本由几个subview构建而成。一个subview作为渐变,一个UIImageView作为图片,两个UILabel作为文本。第二个版本由几个sublayer构建而成。一个CAGradientLayer作为渐变背景,一个CALayer作为图片,CALayer的内容属性是一个CGImageRef,以及两个CATextLayer。最后一个版本是用Core Graphics来绘制的平面纹理。
为了测试这些views能够以多快的速度显示在屏幕上,我测试了渲染5到30个(以5个递增)这样的视图所需要花费的时间,每一次测试了60个样本。为了测试屏幕上能够以60帧每秒的速率进行动画的视图有多少个,我在我的测试应用中递增地渲染越来越多这样的视图,直到帧率掉到了60fps以下,这些视图的动画位置是随机的。
测量技术
你可以在GitHub上找到用来测试的工程,所以我只打算简述一下主要的流程。所有的检测我都使用了CADisplayLink来更新屏幕。在初始化CADisplayLink的时候所定义的选择器,每秒钟会调用60次(假设你做的事情不会花费太多时间),同时很方便的是,你首次获得的也是唯一的一个变量,这个跟显示有关的东西,它有一个timestamp的属性能告诉你上一帧显示的时间戳。
为了测量新的视图能以多快的速度显示到屏幕上,我仅仅将一个包含了可变数量的测试视图的父视图移除并重建。
- (void)setupDisplayLink {
CADisplayLink* displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(nextFrame:)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)nextFrame:(CADisplayLink*)displayLink {
// ...
[view removeFromSuperview];
view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height)];
for (NSUInteger i = 0; i < numberOfViews; i++) {
// add new views ...
}
[self.view addSubview:view];
}
在以固定数量的视图进行60次的迭代之后,我打印了每次循环平均花费的时间
,然后在原来的数量基础上增加5个显示视图,进入下一轮的测试。
为了测试屏幕上的视图能以多快的速度进行动画,由display link所调用的方法有一点不同:
- (void)nextFrame:(CADisplayLink*)displayLink {
// ...
view.transform = CGAffineTransformMakeTranslation(self.randomNumber * 50 - 25, self.randomNumber * 50 - 25);
}
这里仅仅在每次显示周期中设置一个不同的,随机的平移变换作用于容器视图。由于
设置这样的变换并不会阻碍display link在16毫秒之后调用这个方法,即使这导致GPU操作需要花更长的时间,我用OpenGL ES驱动测试了实际的帧率,并在数秒之后平均这个数值。
首次渲染的性能
iPhone
下面的图表显示了在iPhone 3G,4,4s以及5上面,渲染一个测试视图到屏幕中所花费的平均时间。每一组的三个竖状条代表了不同版本的视图:由subview构成,由sublayer构成,以及Core Graphics绘制。
随着设备更新换代,使用subviews跟sublayers来渲染视图的时间也改善了,与此不同的是,我们观察到在iPhone4上,使用绘制使得性能大幅度降低了。高清屏导致有4倍数量的像素需要被绘制。由于使用Core Graphics绘制是CPU的任务,CPU的性能提高并不足以支持新一代的显示需求。在这个例子中,iPhone 5是使用Core Graphics绘制这个方法首次在性能上超过了iPhone 3GS。
下面的线性图显示了在每个设备中,每一次渲染5到30个视图所需要花费的时间。我想要在此强调,对于iPhone 3GS,使用Core Graphics绘制视图的性能跟使用sublayers相比是很差的。进一步,你可以看到对于iPhone 4s,使用sublayers可以60帧每秒地渲染10个视图,但是subviews只能渲染5个,这是由于UIViews是通过CALayers来包装的,所以会增加CPU额外的工作。在iPhone5的时候,这个数量分别增加了15个(sublayers)以及10个(subviews)。
iPad
对于iPad来说,情况跟我们所看到的iPhone的情况是类似的。
然而,由于iPad需要比iPhone绘制更多的像素,我们甚至可以看到在非高清的iPad mini(跟iPad 2应该很类似)上,跟使用subviews还有sublayers相比,使用Core Graphics绘制的性能要更差一些。
iPad 3在此对比中相当明显。使用绘制的性能相当糟糕。高清屏幕是优秀的,但是很显然硬件还没为此准备好。
动画的性能
现在让我们来看一下不同代的iPhone以及iPad在动画性能上的差别,例如,绕着屏幕移动一个已存在的视图。对于这些测试,我将仅仅显示sublayers跟Core Graphics的结果差别,因为由subview构成的结果和由sublayer构成的是基本一致的。
iPhone
下面的图表显示了能够顺滑地以60帧每秒在每个设备上进行动画的视图的数量。正如预期中的,在这个测试里,绘制成平面位图明显优于由数个sublayer构成的情况(大概是4:1,8:1,7:1跟7:1),由于对于每个视图,GPU仅仅只需要移动一个不透明的纹理。
然而,我仍然想指出,尽管iPhone 3GS能够以60帧每秒,推动大约20个由sublayers构成的测试视图。对于这么小的屏幕来说这已经是相当多的视图了。
iPad
在iPad上,绘制成平面位图跟由sublayer构成,它们之间性能的差别更加巨大。iPad mini的比例大概是7:1,这跟我们在最新一代iPhone上面所见类似。但是retina ipad 3的比例是15:1,iPad 4 更加厉害。
iPad较大的屏幕尺寸使得它一次显示的视图比iPhone要多,要么是更大的table views,要么是在使用grid views的时候。由于跟同期的iPhone相比,它的大尺寸屏幕让GPU(CPU也是如此)显得性能不够强大。这经常使得iPad成为了更多时候被吐槽性能的设备。
结论
随着高清屏幕在iPhone和更大尺寸的iPad上面的应用,使用Core Graphics将视图绘制成平面位图的性能特性发生了巨大的改变。Core Graphics绘制是基于CPU的操作,绘制4倍数量的像素意味着CPU更多的负担。此外,在不同代的设备之间,跟GPU性能相比,CPU的性能增加的并不多。
在高清屏的iPad跟iPhone上,与使用Core Graphics手动绘制性能敏感的视图这种做法相比,将视图拆解成sublayers通常会是更好的选择。不过我需要两个前提来保证这个观点:首先,这个视图需要有可以用layer来表示的元素(统一颜色的区域,渐变,图片)。只有那样你才会在不同的技术之间察觉到有意义的区别。其次,GPU仍然需要能够流畅地对layer tree进行动画。这取决于视图本身的复杂度以及同时显示在屏幕中的视图数量。
如果GPU不能以每秒60帧的速度对layer tree进行动画,你将可能从视图层级的扁平化中受益。但是并不需要一定将整个视图都绘制到一个位图中。你也可以在手动绘制的同时使用layers来对需要扁平化的部分进行实验,例如一个渐变的背景以及一个图像。在这个例子中,我们可以将所有label都绘制到一个layer,同时保持渐变以及图片在各自的层级中。这会使得GPU需要处理的层级数量减少25%,CPU没有了渲染大幅背景区域这一负担。另一种观点是,试着设置视图layer的shouldRasterize为YES。
在一些情况下这将改善动画的性能,同时付出的代价是渲染的时间。
在这个示例中,对于subview跟sublayer,我使用了CAGradientLayer创建了渐变背景,对于平面绘制,我使用了CGContextDrawLinearGradient。使用一张图片来作为渐变(或者将渐变预渲染到一个位图上下文)会相对快一些,但是并不会改变不同视图实现方法之间的性能差别。如果你使用一张图片,一定要注意使用视图合适的尺寸(这样它就不用被缩放),@1x跟@2x版本都同时提供了。否则,性能将会比自己绘制渐变来的差。
最后,你需要评估你特殊的使用情况,然后对你所支持的设备找到最佳的权衡方案。
我高度推荐使用CADisplayLink技术来测试你可以多快地将视图显示到屏幕中,以及每秒可以多频繁地对它们进行动画。这很好地分离了图像性能的两个方面,跟仅仅手动滚动table view相比,它给了你更多可靠的数据。
结论:随着高清屏的引进,自定义绘制可能不再是最佳的解决方案,使用sublayers来构成视图成为了一个真正的可选方案。