回头看之UITableView-(基本代理方法及复用原理)
UITableVIew是iOS开发中最常见的视图中最经典的视图了,没有之一,相信对这个视图敢称精通的人开发个好应用应该是问题不大的。
闲话少叙,进入正题。
怎么使用
掌握两个代理
-
UITableViewDelegate
@optional //下文再提到该方法用heightForRow代替 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
-
UITableViewDataSource
@required
//下文再提到该方法用numberOfRowsInSection代替
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
//下文再提到该方法用cellForRow代替
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;@optional //下文再提到该方法用numberOfSection代替 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
要想比较完整的展示你的数据,这四个方法是最经常被实现的。
调用过程大体是这样的:tableView
会先询问代理(在一般MVC里大部分是当前视图控制器ViewController)要展示多少个section
,就是调用numberOfSections
,如果代理没有实现该方法默认就是1个section
。然后tableView
调用numberOfRowsInSection
先询问第一个section
有多少个cell
,然后挨个执行heightForRow
获取每个cell的高度。tableView
对每个section
都执行一遍这样的操作后,那么结果来了:tableView
通过对这些cell
高度的累加就知道了需要多大的空间才能安放得了所有的内容,于是它调整好了contentSize
的值。这样走下来就为我们后续在滑动时能通过scrollIndicator
观察到我们大体滑到了哪个位置做好了准备。
准备好空间之后接下来的任务就是准备内容了。当然大家都知道真正的内容是依附在UITableViewCell
上的,tableView
先调用cellForRow
去获取代理返回给它的第一个cell
,对于所有的cell
来说width
都是固定的,即tableView
本身的宽度,对于第一个cell
来说它的origin
也是确定的,即(0,0)
,也就是说要想确定这个cell
的位置就只需要知道它的height
了。于是tableView
再去调用heightForRow
去获取它的高度,这样一个视图能确定显示在屏幕什么位置的充要条件就具备了。剩下的cell
同理,挨个放在上一个cell
的下边就行了。
总结一下:
- 调用
numberOfSection
获得 A个section
- 先调用
numberOfRowsInSection
获得B个cell
,再调用heightForRow
B次。如此循环A次 - 循环调用
cellForRow
和heightForRow
,直到cell
的个数充满当前屏幕。
这就是一个普通的tableView
一开始加载数据的过程,有几点需要说明:
- 如果你展示在每个
cell
上的内容是相对固定的,准确点说是每个cell
的高度是固定的,那么heightForRow
是不建议让代理去实现的,而是通过tableView
的rowHeight
属性来代替,当数据量比较大,比如说有10000个(其实只要 >= 2)cell
时,tableView
只需要10000*rowHeight
就知道应该准备的空间大小了,而不是调用一个方法10000次通过累加获知需要的大小。而且你懂的,要想获取一个cell
的高度并不是那么容易的事,尤其是在自动布局出现之前,你需要计算各种字符串的所占空间的大小,这对性能是相当大的损耗。 - 如果每个
cell
高度确实不一样,数据量又很大时该怎么解决这个性能问题呢,iOS7之后系统提供了估算高度的办法,estimatedRowHeight
和- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath//下文再提到该方法用estimateHeightForRow代替
,这样每次在加载数据之前,tableView
不再通过heightForRow
消耗大量的性能获取空间大小了,而是通过在estimateRowHeight
或者estimatedHeightForRow
不需要费劲计算就能获取的一个估算值来获取一个大体的空间大小,等到真正的加载数据时才根据获取真实数据,并做出相应的调整,比如contentSize
或者scrollIndicator
的位置。关于动态计算高度,推荐羊教授的一篇文章优化UITableViewCell高度计算的那些事 - 这些方法的调用在保证大顺序不变的情况下,每个方法的调用次数是不一定的,每个iOS版本又不一样,你如果想知道可以动手去试验一下。尤其是在iOS8,它认为
cell
会随时变化,所以一滑动就重新计算cell
的高度。 - 这些方法的调用其实也是有插曲的,比如调用了
reloadData
之后,tableView
只会调用能让它知道所需空间大小的代理方法,然后立马执行reloadData
之后的语句,也就说cellForRow
并不会在reloadData
之后紧接着执行。所以reloadData
之后尽量避免对数据源数组的操作。
复用机制
了解UITableView
的人肯定对这一著名特性多少有点了解。咱们先假设UITableView
没有复用机制,那么我们要展示10000条数据的话,那就得生成10000个UITableViewCell
,占用了大量内存不说,性能也可想而知了,必然是一滑一卡顿,一顿一暴怒啊,控制力弱的估计要摔手机了。
复用机制大体是这样:UITableView
首先加载一屏幕(假设UITableView
的大小是整个屏幕的大小)所需要的UITableViewCell
,具体个数要根据每个cell
的高度而定,总之肯定要铺满整个屏幕,更准确说当前加载的cell
的高度要大于屏幕高度。然后你往上滑动,想要查看更多的内容,那么肯定需要一个新的cell
放在已经存在内容的下边。这时候先不去生成,而是先去UITableView
自己的一个资源池里去获取。这个资源池里放了已经生成的而且能用的cell
。如果资源池是空的话才会主动生成一个新的cell
。那么这个资源池里的cell
又来自哪里呢?当你滑动时视图是,位于最顶部的cell
会相应的往上滑动,直到它彻底消失在屏幕上,消失的cell
去了哪里呢?你肯定想到了,是的,它被UITableView
放到资源池里了。其他cell
也是这样,只要一滑出屏幕就放入资源池。这样,有进有出,总共需要大约一屏幕多一点的cell
就够了。相对于1000来说节省的资源就是指数级啊,完美解决了性能问题。
iOS6之后我们一般在代码里这样处理cell
-
先注册
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];
或
[self.tableView registerNib:[UINib nibWithNibName:@"NibTableViewCell" bundle:nil] forCellReuseIdentifier:@"NibTableViewCell"]; -
在代理方法里获取
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"UITableViewCell" forIndexPath:indexPath]; // do something return cell; }
那么具体在代码里是怎么实现的呢?我们可以大胆的猜测一下。
UITableView
有几个属性(假想的):
NSMutableDictionary *registerCellInfo;
NSMutableDictionary *reusableCellsDictionary;
NSMutableArray *visibleCells;
我们推测两个注册方法的实现
- (void)registerNib:(UINib *)nib forCellReuseIdentifier:(NSString *)identifier{
[self.registerCellInfo setObject:nib forKey:identifier];
[self.registerCellsDictionary setObject:[NSMutableArray array] forKey:identifier];
}
- (void)registerClass:(Class)cellClass forCellReuseIdentifier:(NSString *)identifier{
[self.registerCellInfo setObject:cellClass forKey:identifier];
[self.registerCellsDictionary setObject:[NSMutableArray array] forKey:identifier];
}
然后推测最关键的获取方法
- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath{
//indexPath这个参数是为了重置`cell`的大小,相关的处理并不是本文的重点,所以暂不实现
NSMutableArray *array = self.reusableCellsDictionary[identifier];
UITableViewCell *cell = nil;
if(array.count){
cell = array.lastObject;
[self.visibleCells addObject:cell];
[array removeLastObject];
}else{
id obj = self.registerCellInfo[identifier];
if([obj isKindOfClass:[UINib class]]){
cell = [[((UINib *)obj) instantiateWithOwner:nil options:nil] lastObject];
}else{
cell = [[(Class)obj alloc] init];
}
if(cell){
[self.visibleCells addObject:cell];
}
}
return cell;
}
😂,请忽略以上所有推测方法的不严谨,许多该有的条件判断并没有去处理。但是写到这里相信亲爱的读者已经了解了UITableView
复用机制的原理了。现在,你已经具备了自己动手写一个UITableView
的基础了(当然,假设你已经对UIScrollView
有了充足的了解)。如果我的文章对你有用,烦请点个喜欢,好激励我继续写下去。。。
关于UITableView
的更多知识我们后续再谈