优化UITabelView
内建方法
- 首先是重用cell/header/footer的单个实例,即便是我们需要显示多个。这是优化UIScrollView(UITableView的父类)最明显的方式,UIScrollView是由苹果的工程师提供的。为了正确的使用它,你应该只有cell/header/footer类,一次性初始化它们,并返回给UITableView。
但重要的事情是:在UITableView的dataSource中实现的tableView:cellForRowAtIndexPath:方法,需要为每个cell调用一次,它应该快速执行。所以你需要尽可能快地返回重用cell实例。
不要在这里去执行数据绑定,因为目前在屏幕上还没有cell。为了执行数据绑定,可以在UITableView的delegate方法tableView:willDisplayCell:forRowAtIndexPath:中进行。这个方法在显示cell之前会被调用。
但是UITableView的问题在哪?正如所解释的一样,UITableView不会同时维护所有cell的实例。相反,它只需要维护显示给用户的那些cell。
那么,UITableView是如何知道它的contentSize呢?它是通过计算所以cell的高度之和来计算contentSize的值。
UITableView的delegate方法tableView:heightForRowAtIndexPath:会为每个cell调用一次,所以你应该非常快地返回高度值。
很多人会犯一个错误,他们会在布局初始化cell实例并绑定数据后去获取它们的高度。如果你想优化滑动的性能,就不应该以这种方式来计算cell的高度,因为这事难以置信的低效,iOS设备标准的60 FPS将会降低到15-20 FPS,滑动会变得很慢。
AutoLayout
但如果是AutoLayout呢?它真的跟我所说的一样慢么?你可能会很惊讶,但这是事实。如果你想让你的App在所有设备上都能平滑的滚动,你就会发现这种方法难以置信的慢。你使用的子视图越多,AutoLayout的效率越低。
AutoLayout相对低效的原因是隐藏在底层的命名为”Cassowary“的约束求解系统。如果布局中子视图越多,那么需要求解的约束也越多,进而返回cell给UITableView所花的时间也越多。
哪一个更快呢:使用少量的值来执行基本的数学计算,还是找一个求解大量线性等式或不等式的系统么?现在想像一下,用户想要快速地滑动,每个cell的自动布局也执行着疯狂的计算。
使用内建方法优化UITableView的正确方法是:
重用cell实例:对于特殊类型的cell,你应该只有一个实例,而没有更多。
不要在cellForRowAtIndexPath:方法中绑定数据,因为在此时cell还没有显示。可以使用UITableView的delegate中的tableView:willDisplayCell:forRowAtIndexPath:方法。
我们需要更深一步
当然,上面提到的这些点不足以实现真正的平滑滚动,特别是当你需要实现一些复杂的cell(如有大量的渐变、视图、交互元素、一些修饰元素等等)时,这变得尤其明显。问题就不在布局了,而在渲染了
让我们把关注点放在UIView的opaque属性上。文档中说它用于辅助绘图系统定义UIView是否透明。如果不透明,则绘图系统在渲染视图时可以做一些优化,以提高性能。
他们可能没有最新的iPhone,所以cell必须快速地被渲染。比通常的视图更快。
渲染最慢的操作之一是混合(blending)。混合操作由GPU来执行,因为这个硬件就是用来做混合操作的(当然不只是混合)提高性能的方法是减少混合操作的次数。
在iOS模拟器上运行App,在模拟器的菜单中选择Debug
,然后选中Color Blended Layers
。然后iOS模拟器就会将全部区域显示为两种颜色:绿色和红色。
绿色区域没有混合,但红色区域表示有混合操作。
[图片上传失败...(image-415510-1523694499666)]
[图片上传失败...(image-d7e32d-1523694499666)]
正如你所看到的一样,在cell中至少有两处执行了混合操作,但你可能看不出差别来(这个混合操作是不必要的)。
优化UITableView中绘制数据操作的要点
- 减少iOS执行无用混合的区域:
1.1 不要使用透明背景 (使用iOS模拟器或者Instruments来确认这一点;
1.2 如果可以,尽量使用没有混合的渐变。 - 优化代码,以平衡CPU和GPU的负载。你需要清楚地知道哪部分渲染需要使用GPU,哪部分可以使用CPU,以此保持平衡。
为特殊的cell类型编写特殊的代码。 - 减少子像素情况
3.1 对所有像素相关的数据做四舍五入处理,包括点坐标,UIView的高度和宽度。
3.2 跟踪你的图像资源:图片必须是像素完美的,否则在Retina屏幕上渲染时,它会做不必要的抗锯齿处理。
什么是子像素
在完美的世界中(我们尝试构建的),屏幕点总是被处理成物理像素的整型坐标。但在现实生活中它可能是浮点值,例如,线段可能起始于x为0.25的地方。这时候,iOS将执行子像素渲染
。
如果所有的平滑线段都使用子像素渲染技术来渲染,那你会让iOS执行一些不必要的任务,从而降低FPS。
什么情况下会出现这种不必要的子像素抗锯齿操作呢?最常发生的情况是通过代码计算而变成浮点值的视图坐标,或者是一些不正确的图片资源,这些图片的大小不是对齐到屏幕的物理像素上的(例如,你有一张在Retina显示屏上的大小为6061的图片,而不是6060的)。
在前面我们讲到,要解决问题,首先需要找到问题在哪。在iOS模拟器上运行程序,在”Debug
“菜单中选中”Color Misaligned Image
“。
这一次有两种高亮区域:品红色区域会执行子像素渲染,而黄色区域是图片大小没有对齐的情况。
那如何在代码中找到对应的位置呢?我总是使用手动布局,并且部分会自定义绘制,所以通常找到这些地方没有任何问题。如果你使用Interface Builder,那我对此深表同情。
通常,为了解决这个问题,你只要简单地使用ceilf, floorf和CGRectIntegral方法来对坐标做四舍五入处理。就是这样!
异步UI
可能这看起来有点奇怪,但这是一种非常有效的方法。如果你知道如何做,那么可以让UITableView滑动得更平滑。
现在我们来讨论一下你应该做什么,然后再讨论下你是否可能这么做。
✻ ✻ ✻ ✻ ✻
每个中等以上规模的应用都可能会使用带有媒体内容的cell:文本、图片、动画,甚至还有视频。
而所有这些都可能带有装饰元素:圆角头像、还#
号的文本、用户名等。
我们已经多次提及尽可能快地返回cell的需求,而在这里有一些麻烦:clipsToBounds很慢,图片需要从网络加载,需要在字符串中定位#号,和许多其它的问题。
优化的目标是很明确的:如果在主线程中执行这些操作,则会让你不能很快地返回cell。
在后台加载图片,在相同的地方处理圆角,然后将处理后的图片指定给UIImageView。
立刻显示文本,但在后台定位#号,然后使用属性字符串来刷新显示。
在你的cell中,需要具体情况具体分析,但主要的思想是在后台执行大的操作。这可能不止是网络代码,你需要使用Instruments来找到它们。
记住:需要尽快返回cell。
✻ ✻ ✻ ✻ ✻
有时候,上面的所有技术可能都帮不上忙。如GPU仍然不能使用(iPhone4+iOS7)时,cell中有很多内容时,需要CALayer的支持以实现动画时(因为在drawRect:中实现起来真的很麻烦)。
在这种情况下,我们需要在后台渲染所有其它东西。此外它能在用户快速滑动UITableView时有效地提高FPS。
我们来看看Facebook的应用。为了检测这些,你可能需要往下滑足够的高度,然后点击状态栏。列表会往上滑动,因此你可以清楚地看到此时没有渲染cell。如果想要更精确,则不能及时获得。
这很简单,所以你可以自己试试。这时,你需要设置CALayer的drawsAsynchronously属性为YES。
但是我们可以检查这些行为的必要性。在iOS模拟器上运行程序,然后选择“Debug”菜单中的”Color Offscreen-Rendered“。现在所有在后台渲染的区域都被高亮为黄色。
优化的目标是很明确的:如果在主线程中执行这些操作,则会让你不能很快地返回cell。
在后台加载图片,在相同的地方处理圆角,然后将处理后的图片指定给UIImageView。
立刻显示文本,但在后台定位#号,然后使用属性字符串来刷新显示。
在你的cell中,需要具体情况具体分析,但主要的思想是在后台执行大的操作。这可能不止是网络代码,你需要使用Instruments来找到它们。
记住:需要尽快返回cell。
✻ ✻ ✻ ✻ ✻
这里是异步化UI的实现清单:
找到让你的cell
无法快速返回的瓶颈。
将操作移到后台线程,并在主线程刷新显示的内容。
最后一招是设置你的CALayer
为异步显示模式(即使只是简单的文本或图片)–这将帮你提高FPS。
结论
我尝试解释了iOS绘图系统(没有使用OpenGL,因为它的情况更少)的主要思路。当然有些看起来很模糊,但事实上这只是一些方向,你应该朝着这些方向来检查你的代码以找出影响滚动性能的所有问题。
具体情况具体分析,但原则是不变的。
获取完美平滑滚动的关键是非常特殊的代码,它能让你竭尽iOS的能力来让你的应用更加平滑。