回头看之UITableView-(基本代理方法及复用原理)

UITableVIew是iOS开发中最常见的视图中最经典的视图了,没有之一,相信对这个视图敢称精通的人开发个好应用应该是问题不大的。


闲话少叙,进入正题。

怎么使用

掌握两个代理

  1. UITableViewDelegate

    @optional  
    //下文再提到该方法用heightForRow代替
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
    
  2. 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的下边就行了。

总结一下:

  1. 调用numberOfSection获得 A个 section
  2. 先调用numberOfRowsInSection获得B个cell,再调用heightForRowB次。如此循环A次
  3. 循环调用cellForRowheightForRow,直到cell的个数充满当前屏幕。

这就是一个普通的tableView一开始加载数据的过程,有几点需要说明:

  1. 如果你展示在每个cell上的内容是相对固定的,准确点说是每个cell的高度是固定的,那么heightForRow是不建议让代理去实现的,而是通过tableViewrowHeight属性来代替,当数据量比较大,比如说有10000个(其实只要 >= 2)cell时,tableView只需要10000*rowHeight就知道应该准备的空间大小了,而不是调用一个方法10000次通过累加获知需要的大小。而且你懂的,要想获取一个cell的高度并不是那么容易的事,尤其是在自动布局出现之前,你需要计算各种字符串的所占空间的大小,这对性能是相当大的损耗。
  2. 如果每个cell高度确实不一样,数据量又很大时该怎么解决这个性能问题呢,iOS7之后系统提供了估算高度的办法,estimatedRowHeight- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath//下文再提到该方法用estimateHeightForRow代替,这样每次在加载数据之前,tableView不再通过heightForRow消耗大量的性能获取空间大小了,而是通过在estimateRowHeight或者estimatedHeightForRow不需要费劲计算就能获取的一个估算值来获取一个大体的空间大小,等到真正的加载数据时才根据获取真实数据,并做出相应的调整,比如contentSize或者scrollIndicator的位置。关于动态计算高度,推荐羊教授的一篇文章优化UITableViewCell高度计算的那些事
  3. 这些方法的调用在保证大顺序不变的情况下,每个方法的调用次数是不一定的,每个iOS版本又不一样,你如果想知道可以动手去试验一下。尤其是在iOS8,它认为cell会随时变化,所以一滑动就重新计算cell的高度。
  4. 这些方法的调用其实也是有插曲的,比如调用了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

  1. 先注册
    [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"UITableViewCell"];

    [self.tableView registerNib:[UINib nibWithNibName:@"NibTableViewCell" bundle:nil] forCellReuseIdentifier:@"NibTableViewCell"];

  2. 在代理方法里获取

    - (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的更多知识我们后续再谈


标题:回头看之UITableView-(基本代理方法及复用原理)
作者:yuyedaidao
地址:http://mooncake.wang/articles/2018/11/09/1573699417062.html