UITableView是继承于UIScrollView的一个子类。当UITableView滚动时,如果不用重用机制会重复初始化原来已初始化的cell,用重用机制会节省性能。
UITableView重用机制的原理
UITableView为了做到显示和数据分离, 使用UITableViewCell的视图用来显示每一行的数据, 而tableView的重用机制就是每次需要去显示池和重用池去查找有没有可重用的cell,如果没有cell就创建一个新的,有就取出来直接返回。从而减少cell的大量创建和销毁,从而优化性能。
简单实现
只考虑一个section、没有headerView和footerView的情况。
实现分析:
- UITableView中必须要实现的两个代理方法
//返回cell的数量
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section;
//返回cell对象
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
而其它的代理方法如返回cell高度、返回section数量都有一个默认值。知道了cell的数量和cell的高度,由于是scrollView的子类,我们就确定了tablView的contentSize大小。第二个代理方法会返回一个cell对象,且每个cell的frame都是不一样的。我们可以使用一个数组保存这些cell的frame值,用于后面给cell布局。
- 显示cell,在重用机制中,有两个池,一个是显示池,即用于存放显示在屏幕中的cell;另一个就是重用池。
@property (nonatomic, readonly) NSArray<__kindof UITableViewCell *> *visibleCells;
该属性中保存当前屏幕中显示的cell。在-tableView: cellForRowAtIndexPath:方法中我们会调用
//这里以预加载cell的方式为例
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
从重用池中获取cell。根据contentOffset.y来确定tableView滑动的位置,从而确定当前屏幕显示cell的索引值。
- 如何获取可复用的cell?首先回去显示池查找,如果有就返回,没有就去重用池里面查找,如果重用池没有则new一个cell。
- 超出屏幕范围的cell,需要从显示池中删除,添加到重用池中,以便后续回滚调用。
上述完成了分析工作,下面以代码为主。
代码工作
创建一个CustomTableView类并集成自UIScrollView,我们从 reloadData方法开始着手,reloadData方法可以刷新tableView,我们一般调用reloadData方法的时候可能是有做分页操作,因此需要重新计算tableview.contentSize大小,以及新加入的cell的frame,并保证当前contentOffset没有发生变化
- 计算cell的frame、计算contentsize的值。
- 根据计算后的值刷新UI
根据上面实现分析基本可确定CustomTableView.h的头文件组成部分
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@class CustomTableView;
@protocol CustomTableViewDelegate <NSObject>
//返回cell的数量
- (NSInteger)tableView:(CustomTableView *)tableView numberOfRowsInSection:(NSInteger)section;
//返回cell的高度
- (CGFloat)tableView:(CustomTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
//返回cell的对象
- (UITableViewCell *)tableView:(CustomTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end
@interface CustomTableView : UIScrollView
//刷新tableview
-(void)reloadData;
@property (nonatomic, weak)id<CustomTableViewDelegate>delegate;
//获取可重用cell
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
@end
NS_ASSUME_NONNULL_END
在.m文件中,我们根据刚才的分析实现reloadData方法。1.计算cell的Frame和contentSize 2.布局工作放在了layoutSubviews中,因为,当屏幕滑动的时候,也会去调用layoutSubviews方法,我的理解是因为UIScorllView滑动是不断的修改自身的bounds从而改变布局会不断调用layoutSubviews方法,而tableView本质上也是scrollView, 因此滑动的时候会调用layoutSubviews。
-(void)reloadData{
//1.数据处理(1.1计算每个cell的frame,并存储,1.2 tableView的ContentSize)
[self calculateFrame];
//2.ui处理 在 -layoutSubviews方法中布局
[self setNeedsLayout];
}
1.计算cell的frame
-(void)calculateFrame{
//1.1获取cell的数量
NSInteger cellCount = [self.delegate tableView:self numberOfRowsInSection:0];
CGFloat startY = 0;
//1.2计算每个cell的frame值,并保存在一个数组中
for (int i = 0; i < cellCount; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
CGFloat cellHeight = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
//用一个model保存每个cell的y和高度 model类有两个属性y和height用于保存数据
CellFrameModel *model = [CellFrameModel new];
model.y = startY;
model.height = cellHeight;
startY += cellHeight;//下一个cell的y坐标
[_cellFrameModelArray addObject:model];
}
//1.3 设置contentSize大小
[self setContentSize:CGSizeMake(self.frame.size.width, startY)];
}
2.布局
如何去布局? 首先我们肯定是在layoutSubviews中去布局,由于是scrollview的子类,那一定是滑动的,我们可以根据contentOffset确定其要显示的cell的索引值,再根据显示区域的索引值获取cell对象并展示在view上。如下
-(void)layoutSubviews{
[super layoutSubviews];
//2.1 确定屏幕滑动的范围
CGFloat startY = self.contentOffset.y;
CGFloat endY = self.contentOffset.y + self.frame.size.height;
if (startY <0) {
startY = 0;
}
if (endY > self.contentSize.height) {
endY = self.contentSize.height;
}
//2.2 根据滑动的范围找到该范围应该显示的cell索引
NSInteger startIndex ;
NSInteger endIndex ;
for (int i = 0; i < _cellFrameModelArray.count; i++) {
CellFrameModel *model = _cellPosArr[i];
if(startY >= model.y && startY < model.y + model.height){
startIndex = i; // 找到界面上显示起始端的cell 的索引
break;
}
}
for (int i = 0; i < _cellFrameModelArray.count; i++) {
CellFrameModel *model = _cellPosArr[i];
if(endY > model.y && endY <= model.y + model.height){
endIndex = i; // 找到界面上显示最末端的cell 的索引
break;
}
}
//2.3 显示cell
for (NSInteger i = startIndex; i <= endIndex; i++) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
UITableViewCell * cell = [self.delegate tableView:self cellForRowAtIndexPath:indexPath];
CellFrameModel *model = _cellFrameModelArray[i];
//实际frame的肯定不是这样,这里是图方便
cell.frame = CGRectMake(0, model.y, self.frame.size.width, model.height);
if (![cell superview]) {
[self addSubview:cell];
}
}
}
至此,cell的布局就已经完成了,由于每次滑动就会调用layoutSubviews方法,从而会调用[self.delegate tableView:self cellForRowAtIndexPath:indexPath]获取cell对象,而在代该理方法中我们才真正涉及到tableView的重用机制即:
//获取可重用cell
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath;
3. 获取可重用cell
在实现分析的3中,我们分别要去显示池和重用池去查找cell。
//3 获取可重用的cell
-(UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath{
//3.1如果cell不为nil,即当前cell就在屏幕中
UITableViewCell *cell = _visibleDic[@(indexPath.row)];
if (!cell) {
//3.2 从重用池获取,没有就new一个
if (_reusableArray.count >0) {
cell = _reusableArray.firstObject;
}else{
cell =[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
}
//3.3 将该cell 移出重用池,存入显示池
[_reusableArray removeObject:cell];
[_visibleDic setObject:cell forKey:@(indexPath.row)];
}
return cell;
}
4.将滚动出屏幕的cell移除
滚动出界面的cell,肯定不能在显示池中了(根据3.1),所以需要移除,并将它放入重用池中。而移除cell的操作也可以放在SubViewLayout中,
// 4 数据清理
// 4.1 从现有池里面移走不在界面上的cell, 并移到重用池里(把不在可视区域的cell移动到重用池)
NSArray *visibleCellKey = _visibleDic.allKeys;
for (int i = 0; i < visibleCellKey.count; i++) {
NSInteger index = [visibleCellKey[i] integerValue];
if (index < startIndex || index > endIndex) { // 不在索引范围之间(startIndex -endIndex),就不在可视区域
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
UITableViewCell * cell = [self.delegate tableView:self cellForRowAtIndexPath:indexPath];
[cell removeFromSuperview];
[_reusableArray addObject:_visibleDic[visibleCellKey[i]]];
[_visibleDic removeObjectForKey:visibleCellKey[i]];
}
}
至此一个简单的TableView就实现了。
我们在控制器中实现
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
CustomTableView *tableView = [[CustomTableView alloc] initWithFrame:self.view.bounds];
tableView.delegate = self;
[tableView reloadData];
[self.view addSubview:tableView];
_cellArr = [NSMutableArray array];
}
#pragma mark - tableView delegate
-(NSInteger)tableView:(CustomTableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 40;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return 60.f;
}
- (UITableViewCell *)tableView:(CustomTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath];
cell.textLabel.text = [@(indexPath.row) description];
if (indexPath.row%2) {
cell.backgroundColor = [UIColor yellowColor];
}else{
cell.backgroundColor = [UIColor greenColor];
}
return cell;
}