再谈UITableView的dequeueReusableCellWithIdentifier机制

一、前言

UITableView里面的cell复用是一个很不错的设计,原理是只创建可视
屏幕的UITableViewCell(正好手机屏幕上看到的内容),也可以说是所谓的“按需加载”,开始在屏幕上能看到多少条信息,就创建多少个UITableViewCell,然后如果用户滚动屏幕来查看下面的内容这样就会创建新的UITableViewCell。但是每次滚动都创建新的UITableViewCell显然不是最好的做法。

所以苹果公司的开发团队在此做了进一步的优化那就是利用dequeueReusableCellWithIdentifier来优化单元格。这个方法的具体实现是用一个ID标记给单元格,假如有20条信息也就是有20个单元格,每一屏显示10个,当往下(或往上)滚动的时候最上面的第一条信息如果被完全不显示了(也就是再屏幕以外了),就会把这个第一条信息的UITableViewCell放入到对象池里,然后下面的第11个信息只要有一点点显示出屏幕,就不再新创建UITableViewCell,而是去对象池里拿刚才的第一个信息用过的UITableViewCell给自己用。

二、重用过程

比如总共12个cell,屏幕上显示11个,所以打印indexpath.row

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"%ld",(long)indexPath.row);
   
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kJLXProfileTableViewCell];
    if (!cell) {
        cell = [[ProfileTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:str];
    }
    cell.m_titleStr = [nameArray objectAtIndex:indexPath.row];
    return cell;
}

当往上拉的时候,第12个漏出来,打印12,往下拉,按显示的先后顺序会打印

2017-03-10 13:49:53.617 xxx[95074:9138595] 12
2017-03-10 13:49:53.617 xxx[95074:9138595] 2
2017-03-10 13:49:55.478 xxx[95074:9138595] 1
2017-03-10 13:49:56.005 xxx[95074:9138595] 0

在屏幕一直显示的则不用重新加载。

三、重用遇到的问题-视图覆盖

重用相当于不用再重新创建UITableViewCell的对象,而是直接复用,从一个UITableViewCell对象池中获取一个以Identifier参数命名的UITableViewCell对象。
如果在资源紧缺的时候,这个池会自动清理多余的UITableViewCell对象,则可能无法返回对象,但如果资源丰富,则会保存一些UITableViewCell对象,在需要调用的时候迅速的返回,而不用创建。所以重取出来的cell是有可能已经捆绑过数据或者加过子视图的。

屏幕快照 2017-03-10 14.13.59.png

比如在UITableViewCell上面加一个TextView,或者其他子视图的时候,在复用的时候,快速的来回滚动太快,就会出现视图互相覆盖的现象,如果自定义cell中子视图太多,出现问题更频繁严重。

虽然做了限制,但是最后还是无奈,终于在找到了几个方案,统一说下

比如我重新定义的一个cell子类ProfileTableViewCell,正常实现的方案

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    static NSString *cellIdentifier = @"cellID";
    
    ProfileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[ProfileTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text = @"Cell显示内容";
    cell.detailTextLabel.text = @"Cell辅助显示内容";
    return cell;
}

但是如果重用导致了内容覆盖、错乱的情况,可以试试下面的方案

3.1、复用之前清除子视图

可以在子类,比如ProfileTableViewCell中的这个函数

- (void)prepareForReuse
{
    [super prepareForReuse];
}

中清除子视图。

也可以在

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ }

中清除子视图。

NSArray *subviews = [[NSArray alloc] initWithArray:cell.contentView.subviews];  
for (UIView *subview in subviews) {  
    [subview removeFromSuperview];  
}  

注意,这个子视图是加载cell.contentView上的,如果是cell自己的imageView的话是不会清除的

比如设置了一个图片

[cell.imageView setImage:[UIImage imageNamed:@"home_data_custom"]];

那么重新加载清除这个图片的时候应该将他的内容置空,而不是上面的清除掉cell自己的imageView

if(cell.imageView.image)
{
     [cell.imageView setImage:nil];
}

弊端:这个虽然解决了子视图覆盖的问题,但是清除了子视图也会是子视图暂存的内容丢失,比如textFeild中,一复用,里面已经输入过的内容就没了,所以输入之后就需要保存下来,再次加载的时候取值

3.2、取消cell的重用机制

通过indexPath来创建cell,不使用cell的重用机制也可以解决重复显示问题

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    ProfileTableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    if (!cell) {
        cell = [[ProfileTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text = @"Cell显示内容";
    cell.detailTextLabel.text = @"Cell辅助显示内容";
    return cell;
}

弊端:上面一样,其实也是每次都创建一次新的,也是子视图暂存内容就丢失了

3.3、为每个cell都创建一个标示用来单独复用

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    NSString *str= [NSString stringWithFormat:@"kJLXProfileTableViewCell%ld",(long)indexPath.row];
    ProfileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:str];
    if (!cell) {
        cell = [[ProfileTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    
    cell.textLabel.text = @"Cell显示内容";
    cell.detailTextLabel.text = @"Cell辅助显示内容";
    return cell;
}

弊端:就是每次去对应标示的cell复用,这样虽然占用内存大了

优点:就是子视图的内容不会丢

3.4、当页面拉动需要显示新数据的时候,把最后一个cell进行删除

当页面拉动需要显示新数据的时候,把最后一个cell进行删除,就有可以自定义cell,然后重新赋值

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    static NSString *cellIdentifier = @"cellID";
    
    ProfileTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    if (!cell) {
        cell = [[ProfileTableViewCell alloc] initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:cellIdentifier];
    }
    else//当页面拉动的时候 当cell存在并且最后一个存在 把它进行删除就出来一个独特的cell我们在进行数据配置即可避免
    {
        while ([cell.contentView.subviews lastObject] != nil) {
            [(UIView *)[cell.contentView.subviews lastObject] removeFromSuperview];
        }
    }
    
    cell.textLabel.text = @"Cell显示内容";
    cell.detailTextLabel.text = @"Cell辅助显示内容";
    return cell;
}

弊端:删除之后需要对最后一个重新进行数据赋值,也就是说数据会丢失

优点:节省内存

四、总结

这几个方案都是主要应对在cell中自定义了视图,当重新复用的时候,有时候会导致视图的重复叠加和错乱。所以主要推荐后面两种,如果想简单,那么就是每个cell定义一个不同的标识,各自取各自的cell,如果想节省内存,那么就采取最后这个,然后重新赋值即可。

如果在cell中没有使用自定义视图,或者tableview的大小不足以滚动,只是显示在界面上并不滚动,不会调用dequeueReusableCellWithIdentifier,那么就老老实实使用dequeueReusableCellWithIdentifier:CellIdentifier即可

五、参考文章

Last modification:March 11th, 2017 at 10:50 am
如果看了这个文章可以让你少加会班,可以请我喝杯可乐
已打赏名单
微信公众号

Leave a Comment