前言
iOS的自定义cell是我们在App开发中最常用到的一个控件,接下来笔者将从其结构以及复用机制等方面对其展开论述。
一、TableView协议
我们要使用自定义cell或是TableViewCell前,需要实现两个协议:
通过查看源码得知:
UITableViewDelegate主要用于显示单元格、设置单元格的行高与对指定的单元格进行操作以及设置头视图与脚视图
UITableViewDataSource主要用于设置TableView的section与row的数量
二、cell的复用方式
首先我们先来看一下cell的两种复用方式:
注册:
- (void)viewDidLoad
{
[super viewDidLoad];
// 如果使用 Nib 自定义 Cell
[self.tableView registerNib:[UINib nibWithNibName:@"CustomCell" bundle:nil] forCellReuseIdentifier:@"myCell"];
// 如果使用代码自定义 Cell
[self.tableView registerClass:[CustomCell class] forCellReuseIdentifier:@"myCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = @"mycell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier forIndexPath:indexPath];
// Configure the cell...
...
return cell;
}
非注册:
- (void)viewDidLoad
{
[super viewDidLoad];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *identifier = @"mycell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
// Configure the cell...
...
return cell;
}
三、两种复用方式的区别
区别1
上述代码的区别在于注册方法需要提前对我们要使用的cell类进行注册,如此一来就不需要在后续过程中对我们的单元格进行判空。
这是因为我们的注册方法:
- (void)registerClass:(nullable Class)cellClass forCellReuseIdentifier:(NSString *)identifier API_AVAILABLE(ios(6.0));
在调用过程中会自动返回一个单元格实例,如此一来我们就避免了判空操作
对注释进行翻译:
//从iOS 6开始,客户端可以为每个单元格注册一个类。
//如果所有重用标识符都已注册,请使用更新的-dequeueReusableCellWithIdentifier:forIndexPath:来保证返回一个单元实例。
//从新的出列方法返回的实例在返回时也将具有适当的大小。
区别2
我们可以注意到在获取 Cell 时,两种方式调用了不同的 method:
- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; // Used by the delegate to acquire an already allocated cell, in lieu of allocating a new one.
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0); // newer dequeue method guarantees a cell is returned and resized properly, assuming identifier is registered
这里直接给出结论:
第一个 method 用在了非注册的方式里,第二个 method 用在了需要注册的方式里。经过验证,第一个 method 也可以用在注册的方式里,但是第二个 method 如果用于非注册的方式,则会报错崩溃:
这里的原理笔者没有找到相关回答讲解,但是通过询问chatGPT得到了一个解答,但并不能保证其是否正确
通过查看代码得知 :
static NSString *identifier = @"mycell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
// Configure t
在我们非注册的方法中我们的cell确实没有在使用该方法前注册重用标识符
四、cell的复用原理
在讲解复用原理前我们需要先认识一下cell复用的三个容器
UITableView是继承自UIScrollView,其最核心的思想就是UITableViewCell的复用机制。初始化的时候他会先创建cell的缓存字典和section的缓存array,以及一个用于存放复用cell的mutableSet(可变的集合)。其中我们说的复用池实际上就是reusableCells
_reusableCells:
UITableView在滚动过程中会通过复用机制来管理单元格对象,避免频繁地创建和销毁单元格,提高性能和内存利用率。当某个单元格离开屏幕范围时,它会被回收并放入_reusableCells集合中,等待被重复使用。当新的单元格需要显示时,UITableView会首先尝试从_reusableCells中获取一个可复用的单元格对象,如果_reusableCells中没有可用的单元格,则会通过实例化新的UITableViewCell对象来满足需求。
在表视图显示的时候,会创建
(视图中可看的单元格个数+1)个单元格,一旦单元格因为滑动的而消失在我们的视野中的时候,消失的单元格就会进入缓存池(或叫复用池),当有新的单元格需要显示的时候,会先从缓存池中取可用的单元格,获取成功则使用获取到的单元格,获取失败则重新创建心的单元格,这就是整个的复用机制。但是如何进行复用,这里有两种方式:
接下来介绍一下UITableView实现的基本流程:
- 初始化:初始化cell单元格的一些基本属性与创建装载cell的容器
于是下一步便是layoutSubviews(布局我们的子视图)
- layoutSubviews
- (void)layoutSubviews
{
_backgroundView.frame = self.bounds;
[self _reloadDataIfNeeded];
[self _layoutTableView];
[super layoutSubviews];
}
通过layoutSubviews的一系列操作对cell进行布局
五、自定义cell
我们的UITableViewCell具有原生的控件,UITableViewCell内部有一个默认的子视图ContentView ,其作为父视图,有三个对应的子视图:textLabel、detailTextLabel、UIImageView。而我们需要用UITabelCellStyle属性去决定用哪个子视图(对应属性以及在ContentView中的位置)。其结构图如下:
但在实际app应用中这样的布局并不能满足我们的设计需要,因此我们就需要创建一个自定义UITableViewCell类来符合我们的想法,接下来给出一个自定义cell的例子:
- 这其中的每一行都是一个cell,其创建方法是新建一个UITableViewCell类
- 然后重写我们的新建类中的
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
方法
- 最后对cell中的控件进行布局
参考博客:
对tableView的复用机制原理以及TableViewCell理解
总结
本文简要介绍了cell的注册与非注册的复用方式以及区别和cell复用的原理,在后面如果有新的理解了会加以补充