UITableView可以说是使用频率最高而且话题最多的UIKit组件了,他也是MVC的完美体现。
一. 原理篇
1.1 复用机制
UITableView是继承自UIScrollView,其最核心的思想就是UITableViewCell的复用机制。初始化的时候他会先创建cell的缓存字典和section的缓存array,以及一个用于存放复用cell的mutableSet。并且它会去创建显示的N+1个cell,其他都是从中取出来重用。
每当Cell滑出屏幕时,就会放入到set中(相当于一个重用池),当要显示某一位置的Cell时,会先去集合中取,如果有,就直接拿来显示;如果没有,才会创建。这样做的好处可想而知,极大的减少了内存的开销。
复用机制如下:
维护一个重用队列
当元素离开可见范围时,
removeFromSuperview
并加入重用队列(enqueue)当需要加入新的元素时,先尝试从重用队列获取可重用元素(dequeue)并且从重用队列移除
如果队列为空,新建元素
-
这些一般都在
scrollViewDidScroll:
方法中完成
1.2 调用流程
初始化的时候内部会对_needsReload
进行标记,之后调用setNeedsLayout
方法。对于UIView的setNeedsLayout
方法,在调用后Runloop会在即将到来的周期调用displayIfNeeded
标记,如果为YES则会进行drawRect
视图重绘。当RunLoop到来时,开始重回过程即调用layoutSubViews
方法,且改方法是被重写过的。
它会在内部调用reloadData
方法,在里面会对每个cell进行removeFromSuperview
操作(为了指针悬挂的情况,有可能某个cell被其他视图引用),以及清除cell缓存字典和复用set。
之后会再次去更新缓存:他会先移除每个section的header和footer视图,然后根据DataSource中实现的delegate方法重新设置对应的参数,比如:titleForHeader,titleForFooter,heightForHeader,HeightForFooter,heightForRow,numberOfRows等。
重新缓存完成之后,那么就是进行布局更新了。他会先去获取容器视图相对于父视图的坐标以及偏移量,然后依次将header,cell,footer取出来添加到self上。
我们都知道,UITableView是继承自UIScrollView的,那么需要先确定它的contentSize
及每个Cell的位置,然后才会把重用的Cell放置到对应的位置。所以UITableView的回调顺序是先多次调用tableView:heightForRowAtIndexPath:
以确定contentSize
及Cell的位置,然后才会调用tableView:cellForRowAtIndexPath:
,从而来显示在当前屏幕的Cell。
比如:现在需要展示100个cell,当前屏幕显示10个。那么刷新tableview的时候,首先会调用100次tableView:heightForRowAtIndexPath:
方法,然后调用10次tableView:cellForRowAtIndexPath:
方法。滑动屏幕时,每当Cell进入屏幕,都会调用一次tableView:heightForRowAtIndexPath:
、tableView:cellForRowAtIndexPath:
方法。所以高度计算是一个很有必要优化的地方。
二. 优化篇
2.1 问题分析
- CPU(主要是主线程)/GPU负担过重或者不均衡(诸如mask/cornerRadius/drawRect/opaque带来offscreen rendering/blending等等)。由于所有的UIView都是由CALayer来负责显示,因此对Core Animation的了解就变得尤为重要。这里推荐Nick Lockwood的Core Animation: Advanced Techniques一书,其中有对Core Animation的性能有着非常详尽的梳理和剖析。
- Autolayout布局性能瓶颈,约束计算时间会随着数量呈指数级增长,并且必须在主线程执行。具体分析可以参考这篇文章:http://floriankugler.com/2013/04/22/auto-layout-performance-on-ios/。这也是为何ASDK抛弃了Autolayout而设计了自己的布局系统的重要原因之一(https://github.com/facebook/AsyncDisplayKit/issues/196)。Autolayout在单个View开发时能带来很多便利,而在一些需要高性能的场景下需要谨慎使用。
- 尽管从iPhone4S(A5)开始CPU已经采用多核,然而对于大多数app来说,并行效率仍然非常低下。也就是说,在app卡顿(主线程所占用的核心满负荷)时,往往CPU的其他核心几乎无事可做。一般情况下,由于主线程承担了绝大部分的工作,仅仅是把主线程的任务转移一部分给其他线程进行异步处理,就可以马上享受到并发带来的性能提升。这应该也是AsyncDisplayKit得名的原因之一。
2.2 优化思路
我们知道,当用户开始滚动或点击一个View,所有的事件都会被送到主线程等待处理。此时主线程能否抽出足够充裕的时间来处理变得极为重要,尤其是在连续操作(如UIGestureRecognizer)时,每次touchMoved事件处理都会占用主线程一定的时间(如新的UIImageView进入视图,主线程开始处理布局或者图片解码,而这些需要连续占用大量CPU时间)。如果一个操作耗时超过16ms(1000ms/60fps),那就意味着下一帧无法及时得到处理,引起丢帧。
2.3 可优化点
提前计算并缓存好高度(布局),因为heightForRowAtIndexPath:是调用最频繁的方法。
复杂界面可采用异步绘制。
在大量图片展示时,可以滑动时按需加载。
尽量少用或不用透明图层,多个透明元素重叠显示可采用合并成一张图片显示。
减少subviews的数量,如果是不需要交互可以使用CALayer 替换掉 UIView。
在
heightForRowAtIndexPath:
中尽量不使用cellForRowAtIndexPath:
。根据场景合理使用imageWithContentsOfFile和imageNamed。
页面元素多的时候,减少autolayout布局,采用frame。
缓存NSDateFormatter结果,不多次创建,及时释放。
图片解码时,CALayer 被提交到 GPU 前,CGImage 中的数据才会得到解码,GPU执行,卡主线程。常见的做法是在后台线程先把图片绘制到 CGBitmapContext 中,然后从 Bitmap 直接创建图片。
CALayer 的 border、圆角、阴影、遮罩(mask)触发的离屏渲染,可开启CALayer.shouldRasterize ,转嫁到CPU上或是截图或者采用图片实现。
-
使用RunLoop和多线程在闲时处理一些繁重的计算工作。
具体可查看:UITableView+FDTemplateLayoutCell源码解析
参考文献: