【iOS沉思录】UITableView的重用机制与加载优化详解

UITableView可以说是UIKit中最重要的一个组件,用来展示数据列表,还可以灵活使用进行页面的布局。UITableView的使用遵循MVC模式,数据模型(NSObject)、视图(UIView)和控制器(UITableViewController)分离。UITableView继承自UIScrollView,可上下滑动,可以作为跟视图也可以作为子视图组件。

UITableViewController中,创建UITableViewCell时,initWithSytle:resuseIdentifier中,reuseIdentifier有什么用?UITableViewCell的复用原理?

reuseIdentifier顾名思义是一个复用标识符,是一个自定义的独一无二的字符串,用来唯一地标记某种重复样式的可复用UITableViewCell,系统是通过reuseIdentifier来确定已经创建了的指定样式的cell来进行复用,iOS中表格的cell通过复用来提高加载效率,因为多数情况下表格中的cell样式都是重复的,只是数据模型不同而已,因此系统可以在保证创建足够数量的cell铺满屏幕的前提下,通过保存并重复使用已经创建的cell来提高加载效率和优化内存,避免不停地创建和销毁cell元素。

UITableViewCell的复用原理其实很简单,可以通过下面一个简单的例子来理解:

首先在开发中我们在UITableViewController类中写cell复用代码的最基本模板会像下面这样:

/**
 * 可复用cell制作
 */
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 定义cell重用的静态标志符
    static NSString *cell_id = @"cell_id_demo";
    // 优先使用可复用的cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cell_id];
    // 如果要复用的cell还没有创建,则创建一个供之后复用
    if (cell == nil) {
        // 新创建cell并使用cell_id复用符标记
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cell_id];
    }
    // 配置cell数据
    cell.textLabel.text = [NSString stringWithFormat:@"Cell%i", countNumber];
    // 其他cell设置...
    return cell;
}

代码这样写的原因是通过调用当前tableView的dequeueReusableCellWithIdentifier方法看指定的reuseIdentifier是否有可以重复使用的了,如果有则会返回可复用的cell,cell就绪之后便可以开始更新cell的数据;如果还不可复用,则返回nil,然后会进入后面的if语句,此时创建新的cell并对其设置cell样式标记reuseIdentifier。注意上面的if语句并不是只要执行一次创建一次新的cell就完成任务,然后之后全部重复利用新创建的那一个cell,这是对cell复用机制的误解。事实是要创建足够数量的可覆盖整个tableView的可复用cell之后才会开始复用之前的(UITableView中有一个visiableCells数组保存当前屏幕可见的cell,还有一个reusableTableCells数组用来保存那些可复用的cell),这个我们用下面的测试来验证。

如何简洁清楚的展示UITableViewCell的复用机制呢?这里的方法是创建最基本的文本cell,并创建一个cell创建计数器,每次新创建cell计数器加1并显示在cell上,如果是复用的cell则会显示是复用的哪一个cell,测试代码如下:

/**
 * 分区个数设置为1
 */
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

/**
 * 创建20个cell,保证覆盖并超出整个tableView
 */
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

/**
 * cell复用机制测试
 */
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    // 定义cell重用的静态标志符
    static NSString *cell_id = @"cell_id_demo";
    // 计数用
    static int countNumber = 1;
    // 优先使用可复用的cell
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cell_id];
    // 如果要复用的cell还没有创建,则创建一个供之后复用
    if (cell == nil) {
        // 新创建cell并使用cell_id复用符标记
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cell_id];
        // 计数器标记新创建的cell
        cell.textLabel.text = [NSString stringWithFormat:@"Cell%i", countNumber];
        // 计数器递增
        countNumber++;
    }
    return cell;
}

运行在iPhone5S设备上(UITableViewController作为跟控制器,tableView覆盖整个屏幕),20个cell显示结果依次为:

Cell1、Cell2、Cell3、Cell4、Cell5、Cell6、Cell7、Cell8、Cell9、Cell10、Cell11、Cell12、Cell13、Cell14、Cell1、Cell2、Cell3、Cell4、Cell5、Cell6
【iOS沉思录】UITableView的重用机制与加载优化详解_第1张图片

【iOS沉思录】UITableView的重用机制与加载优化详解_第2张图片

可以看出一共创建了14个cell,其中整个屏幕可显示13个cell,系统多创建一个的原因是保证在表格滑动显示半个cell时仍然能覆盖整个tableView。之后的6个cell就是复用了开始创建的那6个cell了。这样UITableViewCell复用的基本机制就很清楚了,另外还会有reloadData或者reloadRowsAtIndex等刷新表格数据的情况,可能会伴随新的cell创建和可复用cell的更新,但也是建立在基本复用机制的基础之上的。


能否在一个视图控制器中嵌入两个tableview控制器?

可以,相当于视图以及视图控制器的嵌套,视图可以添加子视图,视图控制器也可以添加子控制器。这么问应该是因为这种情况有时会用到而且很重要,因为有一点容易被忽视,就是将子视图添加到了父视图却忘记将对应的控制器作为子控制器添加到父控制器,导致子视图能显示但是不能响应(没有对接好控制器)。例如在当前视图上放一个小尺寸的表格组件,也就是在UIViewController上添加一个UITableViewController子控制器及其子view:

    // 假设有三个视图控制器,一个作为父控制器,两个作为子控制器
    UIViewController *superVC = [[UIViewController alloc]init];
    UITableViewController *subVC1 = [[UITableViewController alloc]init];
    UITableViewController *subVC2 = [[UITableViewController alloc]init];

    // 将子视图控制器添加到父视图控制器(要注意调整子视图的尺寸和位置合理显示,这里忽略)
    [superVC.view addSubview:subVC1.view];
    [superVC addChildViewController:subVC1];

    [superVC.view addSubview:subVC2.view];
    [superVC addChildViewController:subVC2];

    // 子视图控制器的移除有对称的方法,但只能是子视图控制器主动从父视图控制器中移除
    [subVC1.view removeFromSuperview];
    [subVC1 removeFromParentViewController];

    [subVC2.view removeFromSuperview];
    [subVC2 removeFromParentViewController];

此外要注意和presentViewController函数添加子视图控制器的区别,上面手动添加子视图控制器是可以自由调整子视图的frame的(包括子视图位置和尺寸),而presentViewController是用于页面切换,切换后的子页面会覆盖整个屏幕而不可以自由调整子页面位置和尺寸,对称的子视图控制器移除方法为dismissViewControllerAnimated:

    // 显示子视图控制器,completion后的代码块如果不为空添加结束后会触发
    [[parentVC presentViewController:childVC animated:NO completion:nil];
    // 移除子视图控制器,completion后的代码块如果不为空添加结束后会触发
    [childVC dismissViewControllerAnimated:NO completion:nil];

一个tableView是否可以关联两个不同的datasource数据源?如何处理?

多个数据源是完全可以的,关键是如何关联,问题的重点是如何处理,因为将数据源(Model)和tableview视图(View)的对接工作是程序员完成的,因此数据源的多少没有根本影响。处理上可以分开依次对接,也可以通过数据的集合操作先将数据整理合并成一个数据源然后对接。

例如:一个表格中的每个cell显示的是一个人的基本信息,为了简单这里假设只有一个头像和一个姓名。假设有两个数据源,一个数据源是头像的url数组,一个是姓名的字符串数组,对接时完全可以分开在cell数据回调中对接,也可以将两个数组合并然后对接。

合并数据用到的数据模型:

@interface Model : NSObject

@property (nonatomic,copy) NSString *name;  // 姓名
@property (nonatomic,copy) NSString *url;   // 图片

@end

数据源缓冲器:

// 数据源
@property (nonatomic, strong)NSArray *name_datasource;
@property (nonatomic, strong)NSArray *url_datasource;
@property (nonatomic, strong)NSMutableArray *datasource;

处理多数据源:

/**
 * 请求数据
 */
- (void)request {
    // 姓名数据源
    _name_datasource = @[@"张三", @"李四", @"小明", @"小李"];
    _url_datasource = @[@"male", @"male", @"male", @"male"];

    // 合并数据源
    for (int i; i<_name_datasource.count; i++) {
        Model *model = [[Model alloc]init];
        model.name = _name_datasource[i];
        model.url = _url_datasource[i];
        [_datasource addObject:model];
    }
}

数据对接:

/**
 *  cell数据回调
 */
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    static NSString *identifier = @"identifier";
    // 自制cell组件
    AccountCell *cell = [[AccountCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];

    /** 多数据源分开对接:**/
    // 头像
    [cell.avatar setImage:[UIImage imageNamed:_url_datasource[indexPath.row]]];
    // 姓名
    cell.name.text = _name_datasource[indexPath.row];

    // 或者:

    /** 数据源合并后对接**/
    // 取出对应数据模型
    Model *model = _datasource[indexPath.row];
    // 头像
    [cell.avatar setImage:[UIImage imageNamed:model.url]];
    // 姓名
    cell.name.text = model.name;

    return cell;
}

如何对UITableView的滚动加载进行优化,防止卡顿?

UITableView的滚动优化主要在于以下两个方面:

  • 减少cellForRowAtIndexPath代理中的计算量(cell的内容计算)
  • 减少heightForRowAtIndexPath代理中的计算量(cell的高度计算)

【iOS沉思录】UITableView的重用机制与加载优化详解_第3张图片

减少cellForRowAtIndexPath代理中的计算量

  • 首先要提前计算每个cell中需要的一些基本数据,代理调用的时候直接取出;
  • 图片要异步加载,加载完成后再根据cell内部UIImageView的引用设置图片;
  • 图片数量多时,图片的尺寸要跟据需要提前经过transform矩阵变换压缩好(直接设置图片的contentMode让其自行压缩仍然会影响滚动效率),必要的时候要准备好预览图和高清图,需要时再加载高清图。
  • 图片的‘懒加载’方法,即延迟加载,当滚动速度很快时避免频繁请求服务器数据。
  • 尽量手动Drawing视图提升流畅性,而不是直接子类化UITableViewCell,然后覆盖drawRect方法,因为cell中不是只有一个contentview。绘制cell不建议使用UIView,建议使用CALayer。原因要参考UIView和CALayer的区别和联系。

减少heightForRowAtIndexPath代理中的计算量

  • 由于每次TableView进行update更新都会对每一个cell调用heightForRowAtIndexPath代理取得最新的height,会大大增加计算时间。如果表格的所有cell高度都是固定的,那么去掉heightForRowAtIndexPath代理,直接设置TableView的rowHeight属性为固定的高度;
  • 如果高度不固定,应尽量将cell的高度数据计算好并储存起来,代理调用的时候直接取,即将height的计算时间复杂度降到O(1)。例如:在异步请求服务器数据时,提前将cell高度计算好并作为dataSource的一个数据存到数据库供随时取用。

你可能感兴趣的:(iOS沉思录)