【iOS】—— UITableView的复用机制及其所带来问题的解决方法

文章总结于iOS开发之UI学习-UITableView的复用机制TableViewcell 复用机制和解决可能因复用引起的问题

UITabelViewCell复用机制简述

UITableView(表视图) 是在开发中经常使用的控件,而且有时处理的内容量也是非常巨大的,这就需要考虑表视图的性能优化,而最基本的性能优化则是单元格的复用。

在UITableView(表视图)显示的时候,会创建 (视图中可看的单元格个数+1)个单元格,一旦单元格因为滑动的而消失在我们的视野中的时候,消失的单元格就会进入缓存池(或叫复用池),当有新的单元格需要显示的时候,会先从缓存池中取可用的单元格,获取成功则使用获取到的单元格,获取失败则重新创建心的单元格,这就是整个的复用机制。

简单来说,当创建多个cell并且屏幕显示不了所有的cell时,系统会将多余没有显示到的cell放入复用池(reuse pool)中,当需要新建cell并且标识符与复用池中的标识符存在相同的情况,就会调用复用池中的cell,并且会使用该cell的所有UI控件。

状态如图(红色的为显示到屏幕的cell,蓝色为没有显示到屏幕的cell):
复用

创建6个cell并且将前6个显示到了屏幕中。

复用

向上拉cell的过程中,因为原本的复用池为空,所以第7行的cell是直接创建得到的,而不是在复用池中得到的,此时第1行的cell滚出了屏幕范围,因此第一行的cell进入了复用池中,以便下次的cell获取。

复用

继续上拉cell,此时第8行开始出现,因为之前的第1行已经进入了复用池中,因此不需要再对cell进行创建,而是直接使用复用池中原来第一行的cell使用,同时第2行进入到了复用池中。

使用复用的两种方式:

第一种方式:

自己手动创建新的单元格

#import "ViewController.h"  
  
#define width  [UIScreen mainScreen].bounds.size.width  
#define height [UIScreen mainScreen].bounds.size.height  
  
static NSString* identifying = @"标识";  
  
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>  
  
@property (nonatomic, strong) UITableView *tableView;  
  
@end  
  
@implementation ViewController  
  
- (void)viewDidLoad {  
    [super viewDidLoad];  
      
    //创建表视图  
    /* 
     UITableViewStylePlain     平铺类型 
     UITableViewStyleGrouped   分组类型 
     */  
    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, width, height) style:UITableViewStylePlain];  
    //让当前控制器成为表视图的代理  
    _tableView.delegate = self;  
    _tableView.dataSource = self;  
    [self.view addSubview:_tableView];  
}  
  
#pragma mark - UITableViewDelegate 代理方法  
//设置cell的行高  
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{  
      
    return 100;  
}  
  
#pragma mark - UITableViewDataSource 数据源  
//设置cell的个数  
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{  
      
    return 20;  
}  
  
//设置cell  
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{  
      
    //1.根据标识去缓存池中去取cell  
    UITableViewCell *cell = [_tableView dequeueReusableCellWithIdentifier:identifying];  
      
    //2.根据是否取到了可用的cell来判断是否需要重新创建cell(手动创建新的单元格)  
    if (cell == nil){   
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifying];  
    }  
    //3.设置单元格的显示数据  
    cell.textLabel.text = [NSString stringWithFormat:@"第%ld行",indexPath.row];  
      
    //通过打印cell的地址,我们来查看是否复用了cell  
    NSLog(@"address --- %p ----- 第%ld行",cell, indexPath.row);  
      
    return cell;  
}  
  
@end  

根据手机的机型不同,运行后编译器一次显示的行数地址也不相同。
复用

由打印出来的地址可以看到,当前几行不显示在屏幕上时,它就出现了复用的现象。

第二种方式:

通过注册单元格类的方式,由表视图自己创建单元格

#import "ViewController.h"  
  
#define width  [UIScreen mainScreen].bounds.size.width  
#define height [UIScreen mainScreen].bounds.size.height  
  
static NSString *identifying = @"标识";  
  
@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>  
  
@property (nonatomic, strong) UITableView *tableView;  
  
@end  
  
@implementation ViewController  
  
- (void)viewDidLoad {  
    [super viewDidLoad];  
      
    //创建表视图  
    /* 
     UITableViewStylePlain     平铺类型 
     UITableViewStyleGrouped   分组类型 
     */  
    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, width, height) style:UITableViewStylePlain];  
    //让当前控制器成为表视图的代理  
    _tableView.delegate = self;  
    _tableView.dataSource = self;  
    [self.view addSubview:_tableView];  
      
    //注册单元格的类型  
    [_tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:identifying];  
}  
  
#pragma mark - UITableViewDelegate 代理方法  
//设置cell的行高  
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{  
  
    return 100;  
}  
  
#pragma mark - UITableViewDataSource 数据源  
//设置cell的个数  
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{  
  
    return 20;  
}  
  
//设置cell  
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{  
  
    //1.根据标识去缓存池中去取cell  
    UITableViewCell *cell = [_tableView dequeueReusableCellWithIdentifier:identifying];  
  
    //2.倘若根据ID标识来判断有没有对应的cell类型,当缓存池中没有可以复用的cell的时候,会根据注册的类型自动创建cell  
      
    //3.设置单元格的显示数据  
    cell.textLabel.text = [NSString stringWithFormat:@"第%ld行",indexPath.row];  
      
    //通过打印cell的地址,我们来查看是否复用了cell  
    NSLog(@"address --- %p ----- 第%ld行",cell, indexPath.row);  
      
    return cell;  
}  
  
@end  

复用

由打印出来的地址可以看到,当前几行不显示在屏幕上时,它也出现了复用的现象。

由复用可能引起的展示问题

UITableView中的cell可以有很多,一般会通过重用cell来达到节省内存的目的:通过为每个cell指定一个重用标识符(reuseIdentifier),即指定了单元格的种类,当cell滚出屏幕时,会将滚出屏幕的单元格放入复用池中,当某个未在屏幕上的单元格要显示的时候,就从这个复用池中取出单元格进行重用。

但对于多变的自定义cell,有时这种重用机制会出错。比如,当一个cell含有另外一个cell不包含的控件内容,当复用机制起作用时,可能正好读到有额外控件的cell从而展示出来,造成界面上的错乱,下面是针对这一问题的几种解决方法。

处理UITableView复用出现的问题

一.弃用重用机制 - 从indexpath每次获取新的cell

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

   UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];

   if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
   }
   //...其他代码  
   return cell;
}

二.弃用重用机制 - 为每一个cell设置不同的重用id,因为UITableView只用相同重用id的cell才会被重用,实质上也是舍弃了重用

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

    NSString *CellIdentifier = [NSString stringWithFormat:@"Cell%d%d", [indexPath section], [indexPath row]];//以indexPath来唯一确定cell 
    
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];//出列可重用的cell 
    
    if (cell == nil) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
    }

   //...其他代码 
   return cell;
}

三.清除重用cell的所有的子控件(这种方式不能通过注册nib方式,只能自定义view中复写其中的init方法而达到效果)

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

    staticNSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];//出列可重用的cell 

    if (cell == nil) {
      cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];

    } else {
        //删除cell的所有子视图 
        while([cell.contentView.subviews lastObject] != nil) {
        [(UIView*)[cell.contentView.subviews lastObject] removeFromSuperview];
        }
    }
    //...其他代码 
    return cell;
}

view的部分代码,重写父类UITableView的方法

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface MyTableViewCell : UITableViewCell

//view部分的自定义实现的部分,而不使用nib
@property (nonatomic, strong) UIImageView* myImageView;

@end

NS_ASSUME_NONNULL_END

#import "MyTableViewCell.h"

@implementation MyTableViewCell

//这里简单复写了父类方法来添加自定义的view
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.myImageView = [[UIImageView alloc] initWithFrame:CGRectMake(10, 0, 60, 60)];
        [self addSubview:self.myImageView];
    }
    return self;
}

@end

四.清除重用子控件的内容(支持代码或者nib文件的实现)

    [self.tableView registerNib:[UINib nibWithNibName:@"MyTableViewCell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"myImageCell"];


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

    DownloadImageTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"myImageCell" forIndexPath:indexPath];

    //清除所有的内容而不是控件
    [cellclearContents];
    
    //其他操作
    
    return cell;
}

五.通过UITableView对应的复用方法来做内容的清空

cell被重用如何提前知道?

重写cell的prepareForReuse官方头文件中有说明.当前已经被分配的cell如果被重用了(通常是滚动出屏幕外了),会调用cell的prepareForReuse通知cell.注意这里重写方法的时候,注意一定要调用父类方法[super prepareForReuse] .这个在使用cell作为网络访问的代理容器时尤其要注意,需要在这里通知取消掉前一次网络请求.不要再给这个cell发数据了.

- (void)prepareForReuse {

   [super prepareForReuse];

}

自定义UITableViewCell的方法有很多,多数人都会遇到自己定义的cell里面图片错乱的问题,这个问题往往是因为没有实现prepareForReuse这个方法导致的.

UITableViewCell在向下滚动时复用, 得用的cell就是滑出去的那些, 而滑出去的cell里显示的信息就在这里出现了 解决的方法就是在UITableViewCell的子类里实现perpareForReuse方法, 把内容清空掉。

六.重写UITableViewCell(推荐)

为了提高应用的性能,我们往往在动态显示表格数据时,复用 tableView 中的 cell,这可以分为注册和不注册两种方式来实现。

需要先创建继承 UITableViewCell 的子类。

注册

注册需要提前将要复用的 cell 类注册,而不需要在后续获取 cell 时手动判断 dequeue 的结果是否为 nil。

在 .m 的 - (void)viewDidLoad 中添加如下代码:

    [_tableView registerClass:[FirstTableViewCell class] forCellReuseIdentifier:@"first"];

其中 _tableView 是在 .h 文件中创建的属性,FirstTableViewCell是继承 UITableViewCell 的子类,@“first” 中的 first 为标识符。

不注册

不注册需要在后续获取 cell 时手动判断 dequeue 的结果是否为nil。

在 .m 的获取 cell 之后添加如下代码:

if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
    }
注册和不注册的区别

代码上的区别在于,注册的方法需要提前将要复用的 Cell 类注册,而不需要在获取 Cell 时手动判断 dequeue 的结果是否为nil,这是因为
dequeueReusableCellWithIdentifier:identifier forIndexPath: 在内部处理了这个过程,使得最后返回的一定是可用的 Cell,参见头文件中的注释:

// Beginning in iOS 6, clients can register a nib or class for each cell.
// If all reuse identifiers are registered, use the newer -dequeueReusableCellWithIdentifier:forIndexPath: to guarantee that a cell instance is returned.
// Instances returned from the new dequeue method will also be properly sized when they are returned.
- (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);

- (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);

重写步骤:

建立MyTableViewCell文件继承于UITableViewCell,并定义你所需要的若干属性。

子类的 .h 文件

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface FirstTableViewCell : UITableViewCell

@property (nonatomic, strong) UILabel* label;
@property (nonatomic, strong) UIImageView* aImageView;

@end

NS_ASSUME_NONNULL_END

子类的 .m 文件

#import "FirstTableViewCell.h"

@implementation FirstTableViewCell

//重写初始化,内容根据自己的需求进行设置
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if ([self.reuseIdentifier isEqualToString:@"first"]) {
        _label = [[UILabel alloc] init];
        [self.contentView addSubview:_label];
        _label.text = @"帅哥哥";
        
        _aImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"firstPhoto.jpeg"]];
        [self.contentView addSubview:_aImageView];
    } else {
        _label = [[UILabel alloc] init];
        [self.contentView addSubview:_label];
        _label.text = @"丑哥哥";
        
        _aImageView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"secondPhoto.jpeg"]];
        [_aImageView.layer setMasksToBounds:YES];
        [_aImageView.layer setCornerRadius:5.0];
        [self.contentView addSubview:_aImageView];
    }
    
    return self;
}

//设置布局,可以根据自己的需求设计
- (void)layoutSubviews {
    _label.frame = CGRectMake(100, 25, 300, 50);
    _aImageView.frame = CGRectMake(10, 10, 80, 80);
}
@end

需要自定义 UITableViewCell 的视图的 .h 文件:

#import <UIKit/UIKit.h>

@interface ViewController : UIViewController <
//实现数据视图的普通协议
//数据视图的普通事件处理
UITableViewDelegate,
 
//实现数据视图的数据代理协议
//处理数据视图的数据代理
UITableViewDataSource>

//定义数据视图对象,数据视图用来显示大量相同格式的大量信息的视图
@property (nonatomic, strong) UITableView* tableView;


@end

需要自定义 UITableViewCell 的视图的 .m 文件:

#import "ViewController.h"
//添加子类 FirstTableViewCell 和 SecondTableViewCell
#import "FirstTableViewCell.h"
#import "SecondTableViewCell.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //创建数据视图并初始化位置
    _tableView = [[UITableView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) style:UITableViewStylePlain];
    
    //设置代理
    _tableView.delegate = self;
    _tableView.dataSource = self;
    
    //将数据视图对象添加到视图上
    [self.view addSubview:_tableView];
    
    //注册两个子类
    [_tableView registerClass:[FirstTableViewCell class] forCellReuseIdentifier:@"first"];
    [_tableView registerClass:[SecondTableViewCell class] forCellReuseIdentifier:@"second"];
}

//每组的行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 10;
}

//组数
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
    return 1;
}

//每个 cell 的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    return 100;
}

//设置单元格
- (UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    
    if (indexPath.row % 2) {
        FirstTableViewCell* cell = [_tableView dequeueReusableCellWithIdentifier:@"first" forIndexPath:indexPath];
        return cell;
    } else {
        SecondTableViewCell* cell = [_tableView dequeueReusableCellWithIdentifier:@"second" forIndexPath:indexPath];
        return cell;
    }
}
@end

运行之后的效果图:
自定义cell

总结

UITableView故名思议是一种表格控件,是iOS应用中常用的表格控件。

UITableView继承于UIScrollview,因此它默认支持垂直滚动(只支持垂直滚动)。

UITableView性能极佳,它可以出色的完成我们工作中的很多功能。

UITableView一共有两种样式:UITableViewStylePlain 和 UITableViewStyleGroup。

复用是系统提供的一种高性能的方法,它可以大大减少应用所占用内存和流畅度,上述的取消复用不建议使用,因为这将增加应用的所占用内存和流畅度。

  • 3
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值