文章总结于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
运行之后的效果图:
总结
UITableView故名思议是一种表格控件,是iOS应用中常用的表格控件。
UITableView继承于UIScrollview,因此它默认支持垂直滚动(只支持垂直滚动)。
UITableView性能极佳,它可以出色的完成我们工作中的很多功能。
UITableView一共有两种样式:UITableViewStylePlain 和 UITableViewStyleGroup。
复用是系统提供的一种高性能的方法,它可以大大减少应用所占用内存和流畅度,上述的取消复用不建议使用,因为这将增加应用的所占用内存和流畅度。