ios UITableView重用机制和简单自定义tableView

UITableView是继承于UIScrollView的一个子类。当UITableView滚动时,如果不用重用机制会重复初始化原来已初始化的cell,用重用机制会节省性能。

UITableView重用机制的原理

UITableView为了做到显示和数据分离, 使用UITableViewCell的视图用来显示每一行的数据, 而tableView的重用机制就是每次需要去显示池和重用池去查找有没有可重用的cell,如果没有cell就创建一个新的,有就取出来直接返回。从而减少cell的大量创建和销毁,从而优化性能。

简单实现

只考虑一个section、没有headerView和footerView的情况。

实现分析:

  1. 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布局。

  2. 显示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的索引值。

  3. 如何获取可复用的cell?首先回去显示池查找,如果有就返回,没有就去重用池里面查找,如果重用池没有则new一个cell。

  4. 超出屏幕范围的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;
}

Demo地址https://github.com/lijsrn/CustomTableViewDemo

猜你喜欢

转载自blog.csdn.net/a1034386099/article/details/87771401