前言
- 这篇文章对 UITableView 的优化主要从以下3个方面分析:
- 基础的优化准则(高度缓存, cell 重用...)
- 学会使用调试工具分析问题
- 异步绘制
-
涉及到 tableView 请一定要 用真机调试!用真机调试!用真机调试!
手机的性能比起电脑还是差别很大,不要老想着用模拟器调试。一定要用真机才能看出效果。 -
不要过早的做复杂的优化
虽然这篇文章讲的是如何优化table,但是根据我的经验,不要一开始就去做这些工作(基本的优化除外),因为不管怎么说,PM不会闲着的,产品的变动并不是由开发人员控制。但是大的优化对代码的结构还是有很大影响的,这意味着过早优化可能会拖慢工程的进度。在项目初期能用 xib 就用吧,本来大部分这样的文章都是不推荐使用 IB 的东西,但是不得不说,在效率上 IB 实在是有了很多天然的优势。 -
优化总是在 空间 和 时间 之间权衡
一般优化的后期总是以更多的空间换取更短时间的响应。这表示可能会增加额外的内存和CPU资源的开销,需要缓存高度,缓存布局...,当然也可能有别的考量以时间换取空间。具体怎么做,还得根据项目相关的业务逻辑确定。其实我想表达的是目前并没有十全十美的方案既可以节省内存,又可以加快速度,如果非要说好的话,也只能是在资源调度上下了功夫(如果你知道更好的请告诉我,谢谢)。如果你追求的是非常完美,还是不要朝下看了。
基础的优化准则
- 正确地使用UITableViewCell的重用机制
UITableView最核心的思想就是 UITableViewCell 的重用机制。UITableView 只会创建一屏幕(或一屏幕多一点)的 UITableViewCell ,每当 cell 滑出屏幕范围时,就会放入到一重用池当中,当要显示新的 cell 时,先去重用池中取,若没有可用的,才会重新创建。这样可以极大的减少内存的开销。
比较早的一种写法
static NSString *cellID = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellID];
//cell 初始化
}
// cell 设置数据
return cell;
或者通过注册cell的方式
//注册cell
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"cell"];
//获取cell
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
-
提前计算好 cell 的高度和布局。
UITableView
有两个重要的回调方法:- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
UITableView
的回调顺序是先多次调用tableView:heightForRowAtIndexPath:
用来确定contentSize
及Cell
的位置,然后才会调用tableView:cellForRowAtIndexPath:
,从而来显示在当前屏幕的 cell 。
iOS8会更厉害,还会边滑动边调用tableView:heightForRowAtIndexPath:
,可见这个方法里一定不能重复进行着大量的计算。我们应该提前计算好 cell 的高度并且缓存起来,在回调时直接把高度值直接返回。
这里说一种我经常采用的策略:
一般在网络请求结束后,在更新界面之前就把每个 cell 的高度算好,缓存到相对应的 model 中。
这里可能有人说要是一个 model 对应多种 cell 怎么办?
model 可以添加多个高度属性啊,这点空间上的开销还是可以接受的吧。
当然这个时候最好把布局也都算好了最好,下面的YY的做法会介绍。 -
避免阻塞主线程。
很多时候我们需要从网络请求图片等,把这些操作放在后台执行,并且缓存起来。现在我们大都使用 SDWebImage 进行网络图片处理,正常的使用是没有大问题的,但是如果对性能要求比较高,或者要处理gif图,我还是推荐 YYWebImage,详细内容请自行移步到github查看,当然这只是个人建议。
还有就是不要在主线程做一些文件的I/O操作。 -
按需加载。
这一条真的是看各位喜好了,我是觉得滚动的过程中有大量的 “留白” 并不太好,不过作为优化的建议还是要考虑的。
如快速滚动时,仅绘制目标位置的 cell ,可以提高滚动的顺畅程度。
具体可以参考 VVebo 。 -
减少
SubViews
的数量。
总觉得这条有点多余,能简单点的我们肯定不会做复杂了吧。这更多的取决于UI界面的复杂度。 -
尽可能重用开销比较大的对象。
如NSDateFormatter
和NSCalendar
等对象初始化非常慢,我们可以把它加入类的属性当中,或者创建单例来使用。 -
尽量减少计算的复杂度
在高分屏尽量用 ceil 或 floor 或 round 取整。不要出现 1.7,10.007这样的小数。 -
不要动态的
add
或者remove
子控件
最好在初始化时就添加完,然后通过hidden来控制是否显示。
学会使用调试工具分析问题
Instruments里的:
- Core Animation instrument
- OpenGL ES Driver instrument
模拟器中的:
- Color debug options View debugging
Xcode的:
- View debugging
Xcode 已经集成了 Instruments 工具,通过菜单 profile 即可打开。
在模拟器中你可以在 Debug 中找到如下的菜单:
D5806B24-BB86-449D-81C2-82DA247E053C.png下面是一些常见的调试选项的含义:
1. Color Blended Layers
Instruments可以在物理机上显示出被混合的图层Blended Layer(用红色标注),
Blended Layer是因为这些Layer是透明的(Transparent),
系统在渲染这些view时需要将该view和下层view混合(Blend)后才能计算出该像素点的实际颜色。
解决办法:检查红色区域view的opaque属性,记得设置成YES;检查backgroundColor属性是不是[UIColor clearColor]
2. Color Copied Images
这个选项主要检查我们有无使用不正确图片格式,若是GPU不支持的色彩格式的图片则会标记为青色,
则只能由CPU来进行处理。我们不希望在滚动视图的时候,CPU实时来进行处理,因为有可能会阻塞主线程。
解决办法:检查图片格式,推荐使用png。
3. Color Misaligned Images
这个选项检查了图片是否被放缩,像素是否对齐。
被放缩的图片会被标记为黄色,像素不对齐则会标注为紫色。
如果不对齐此时系统需要对相邻的像素点做anti-aliasing反锯齿计算,会增加图形负担
通常这种问题出在对某些View的Frame重新计算和设置时产生的。
解决办法:参考 基本优化准则的第7点
4. Color Offscreen-Rendered
这个选项将需要offscreen渲染的的layer标记为黄色。
离屏渲染意思是iOS要显示一个视图时,需要先在后台用CPU计算出视图的Bitmap,
再交给GPU做Onscreen-Rendering显示在屏幕上,因为显示一个视图需要两次计算,
所以这种Offscreen-Rendering会导致app的图形性能下降。
大部分Offscreen-Rendering都是和视图Layer的Shadow和Mask相关。
下列情况会导致视图的Offscreen-Rendering:
- 使用Core Graphics (CG开头的类)。
- 使用drawRect()方法,即使为空。
- 将CALayer的属性shouldRasterize设置为YES。
- 使用了CALayer的setMasksToBounds(masks)和setShadow*(shadow)方法。
- 在屏幕上直接显示文字,包括Core Text。
- 设置UIViewGroupOpacity。
解决办法:只能减少各种 layer 的特殊效果了。
这篇博文 Designing for iOS: Graphics & Performance 对offsreen以及图形性能有个很棒的介绍,
异步绘制
这个属于稍高级点的技能。
如果我们使用 Autolayout 可能就无能为力了。这也是为什么那么多人在优化这块拒绝使用 IB 开发。
但是这里并不展示具体的绘制代码。给个简单的形式参考:
//异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = CGRectMake(0, 0, 100, 100);
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
[[UIColor lightGrayColor] set];
CGContextFillRect(context, rect);
//将绘制的内容以图片的形式返回,并调主线程显示
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
//code
});
});
另外绘制 cell 不建议使用 UIView,建议使用 CALayer。
从形式来说:UIView 的绘制是建立在 CoreGraphic 上的,使用的是 CPU。CALayer 使用的是 Core Animation,CPU,GPU 通吃,由系统决定使用哪个。View的绘制使用的是自下向上的一层一层的绘制,然后渲染。Layer处理的是 Texure,利用 GPU 的 Texture Cache 和独立的浮点数计算单元加速 纹理 的处理。
从事件的响应来说:UIView是 CALayer 的代理,layer本身并不能响应事件,因为layer是直接继承自NSObject,不具备处理事件的能力。而 UIView 是继承了UIResponder 的,这也是事件转发的角度上说明,view要比单纯的layer复杂的多。在滑动的列表上,多层次的view再加上各种手势的处理势必导致帧数的下降。
在这一块还有个问题就是当 TableView 快速滑动时,会有大量异步绘制任务提交到后台线程去执行。线程并不是越多越好,太多了只会增加 CPU 的负担。所以我们需要在适当的时候取消掉不重要的线程。
目前这里有两种做法:
YY的做法是:
尽量快速、提前判断当前绘制任务是否已经被取消;在绘制每一行文本前,都会调用 isCancelled() 来进行判断,保证被取消的任务能及时退出,不至于影响后续操作。
VVebo的做法是:
当滑动时,松开手指后,立刻计算出滑动停止时 Cell 的位置,并预先绘制那个位置附近的几个 Cell,而忽略当前滑动中的 Cell。忽略的代价就是快速滑动中会出现大量空白内容。
两者都是不错的优化方法,各位自行取舍。
番外
-
YYText 的使用
这个框架涉及到的方面还是很多的,在这里说再多的理论还是不如自己去看代码的好。
关于用法,没什么比YY作者说的更明白的了:当获取到 API JSON 数据后,我会把每条 Cell 需要的数据都在后台线程计算并封装为一个布局对象 CellLayout。CellLayout 包含所有文本的 CoreText 排版结果、Cell 内部每个控件的高度、Cell 的整体高度。每个 CellLayout 的内存占用并不多,所以当生成后,可以全部缓存到内存,以供稍后使用。这样,TableView 在请求各个高度函数时,不会消耗任何多余计算量;当把 CellLayout 设置到 Cell 内部时,Cell 内部也不用再计算布局了。
对于通常的 TableView 来说,提前在后台计算好布局结果是非常重要的一个性能优化点。为了达到最高性能,你可能需要牺牲一些开发速度,不要用 Autolayout 等技术,少用 UILabel 等文本控件。但如果你对性能的要求并不那么高,可以尝试用 TableView 的预估高度的功能,并把每个 Cell 高度缓存下来。这里有个来自百度知道团队的开源项目可以很方便的帮你实现这一点:FDTemplateLayoutCell。
一开始我是想要在这里长篇大论的,不过后来想想千言万语还是不及一个 Demo (还没做完)。
-
AsyncDisplayKit 的使用
这个框架是 facebook 团队开源的,它的使用代价有点大,因为它已经不是按照我们正常的UIKit框架来写了。由于这个框架就是建立在各种 Display Node 上的,所以要使用该框架,那么就需要使用 Display Node 层次结构替换视图层次结构和/或 Layer 树。但是这个框架还是值得尝试的,因为 AsyncDisplayKit 支持在非主线程执行之前只能在主线程才能执行的任务。这就能减轻主线程的工作量以执行其他操作,例如处理触摸事件,滑动事件。下面附一篇 AsyncDisplayKit教程
当然,不管是 YYText 还是 AsyncDisplayKit,他们都有非常流畅的体验,对于复杂的列表来说都是神器。用好其中一种都可以解决大部分问题了,那么还有一部分问题来自于哪里呢?继续向下看。
-
SDWebImage
应该是使用最为广泛的图片库了吧。上面也提到了,我们正常的使用是没有大问题的,但是如果对性能要求比较高,或者要处理gif图,SD 难免会拖慢速度。特别是 gif 的内存暴增问题,SD 一直没有一个较好的解决方案。 -
YYWebImage
这个就是在列表优化时我推荐使用的网络图片库,在对性能要求比较高的情况下,YY 可以直接以 layer 作为图片的载体这样减少了相当的一部分资源消耗。具体在什么情况下使用layer 什么情况下使用 imageView 戳这里 。
对于gif图,YY 还提供了分享gif的方案。
小结
-
如果项目比较紧,我更推荐
IB
+基础的优化准则
,既可以保证整体效率也不至于卡的太严重。 -
如果项目已经到后期,并且有时间进行大量的优化。我倾向于使用
纯代码
+异步绘制
,这一部分在苹果现在的多种屏幕尺寸上适配工作量并不小。但是效果却也是很明显的。 -
如果想获得流畅的体验,但是有没有太多的时间去做优化,那就可以使用一些封装好的第三方库,比如 YYText,AsyncDisplayKit 。
下一步
本文的篇幅有限再加上作者的功力也是相当有限,讲性能调优即使再来几千字也不一定讲得完、讲的全,我希望这只是个引子,能够让你对table的优化有个大致的了解,并且提供几个可以研究的方向。下面的相关链接都是非常值得仔细研究的,如果有时间读一下一定会有收获。
网友评论
如何取舍,要看项目特点、团队实力、迭代周期,目标用户群的设备类型,不可一概而论。
开发效率,需要考虑:
①如何让团队中的每个人都能看得懂,都能改的了
②如何才能【有效】的支撑产品迭代,迭代的目标、周期都需要考虑
③如何才能避免iOS系统升级带来的繁琐适配
使用体验,需要额外多想想的是:
实现当前效果的方案,在未来的技术迭代中有没有重大的风险,绝对要避免推倒重来
UITableView,已经是各家App中最常见、最好写也最难写的一个系统基础控件了。
可以结合下不同行业的,不同类型的,不同效果的,列举下demo