前言

大家都知道UITableView,最经典在于循环利用,这里我自己模仿UITableView循环利用,写了一套自己的TableView实现方案,希望大家看了我的文章,循环利用思想有显著提升。

效果如图:

研究UITableView底层实现

1.系统UITabelView的简单使用,这里就不考虑分组了,默认为1组。

// 返回第section组有多少行
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    NSLog(@"%s",__func__);
    return 10;
} // 返回每一行cell的样子
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"%s",__func__);
    static NSString *ID = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:ID];
    
    if (cell == nil) {
        
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:ID];
    }
    
    cell.textLabel.text = [NSString stringWithFormat:@"%ld",indexPath.row];
    
    return cell;
}
// 返回每行cell的高度
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"%s--%@",__func__,indexPath);
    return 100;
}

2.验证UITabelView的实现机制。

如图打印结果:

分析:底层先获取有多少cell(10个),在获取每个cell的高度,返回高度的方法一开始调用10次。

目的:确定tableView的滚动范围,一开始计算所有cell的frame,就能计算下tableView的滚动范围。

分析:tableView:cellForRowAtIndexPath:方法什么时候调用。

打印验证,如图:

一开始调用了7次,因为一开始屏幕最多显示7个cell

目的:一开始只加载显示出来的cell,等有新的cell出现的时候会继续调用这个方法加载cell。

3.UITableView循环利用思想

当新的cell出现的时候,首先从缓存池中获取,如果没有获取到,就自己创建cell。

当有cell移除屏幕的时候,把cell放到缓存池中去。

二、自定义UIScroolView,模仿UITableView循环利用

1.提供数据源和代理方法,命名和UITableView一致。

@class YZTableView;
@protocol YZTableViewDataSource @required // 返回有多少行cell
- (NSInteger)tableView:(YZTableView *)tableView numberOfRowsInSection:(NSInteger)section; // 返回每行cell长什么样子
- (UITableViewCell *)tableView:(YZTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath; @end @protocol YZTableViewDelegate // 返回每行cell有多高
- (CGFloat)tableView:(YZTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath; @end

2.提供代理和数据源属性

@interface YZTableView : UIScrollView

@property (nonatomic, weak) id dataSource;

@property (nonatomic, weak) id delegate;

@end

警告:

解决,在YZTableView.m的实现中声明。

原因:有人会问为什么我要定义同名的delegate属性,我主要想模仿系统的tableView,系统tableView也有同名的属性。

思路:这样做,外界在使用设置我的tableView的delegate,就必须遵守的我的代理协议,而不是UIScrollView的代理协议。

3.提供刷新方法reloadData,因为tableView通过这个刷新tableView。

@interface YZTableView : UIScrollView

@property (nonatomic, weak) id dataSource;

@property (nonatomic, weak) id delegate;

// 刷新tableView
- (void)reloadData; @end

4.实现reloadData方法,刷新表格

回顾系统如何刷新tableView

1).先获取有多少cell,在获取每个cell的高度。因此应该是先计算出每个cell的frame.

2).然后再判断当前有多少cell显示在屏幕上,就加载多少

// 刷新tableView
- (void)reloadData
{
    // 这里不考虑多组,假设tableView默认只有一组。
    
    // 先获取总共有多少cell
    NSInteger rows = [self.dataSource tableView:self numberOfRowsInSection:0];
    
    // 遍历所有cell的高度,计算每行cell的frame
    CGRect cellF;
    CGFloat cellX = 0;
    CGFloat cellY = 0;
    CGFloat cellW = self.bounds.size.width;
    CGFloat cellH = 0;
    CGFloat totalH = 0;
    
    for (int i = 0; i < rows; i++) {
       
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
        // 注意:这里获取的delegate,是UIScrollView中声明的属性
        if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
            cellH = [self.delegate tableView:self heightForRowAtIndexPath:indexPath];
        }else{
            cellH = 44;
        }
        cellY = i * cellH;
        
        cellF = CGRectMake(cellX, cellY, cellW, cellH);
        
        // 记录每个cell的y值对应的indexPath
        self.indexPathDict[@(cellY)] = indexPath;
        
        // 判断有多少cell显示在屏幕上,只加载显示在屏幕上的cell
        if ([self isInScreen:cellF]) { // 当前cell的frame在屏幕上
            // 通过数据源获取cell
            UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];
            
            cell.frame = cellF;
            
            [self addSubview:cell];
            
        }
        
        // 添加分割线
        UIView *divideV = [[UIView alloc] initWithFrame:CGRectMake(0, cellY + cellH - 1, cellW, 1)];
        divideV.backgroundColor = [UIColor lightGrayColor];
        divideV.alpha = 0.3;
        [self addSubview:divideV];
        
        // 添加到cell可见数组中
            [self.visibleCells addObject:cell];
        
        // 计算tableView内容总高度
        totalH += cellY + cellH;
    
    }
    
    // 设置tableView的滚动范围
    self.contentSize = CGSizeMake(self.bounds.size.width, totalH);
    
}

5.如何判断cell显示在屏幕上

  • 当tableView内容往下走

  • 当tableView内容往上走

// 根据cell尺寸判断cell在不在屏幕上
- (BOOL)isInScreen:(CGRect)cellF
{
    // tableView能滚动,因此需要加上偏移量判断
    
    // 当tableView内容往下走,offsetY会一直增加 ,cell的最大y值 < offsetY偏移量   ,cell移除屏幕
    // tableView内容往上走 , offsetY会一直减少,屏幕的最大Y值 <  cell的y值 ,Cell移除屏幕
    // 屏幕最大y值 = 屏幕的高度 + offsetY
    
    // 这里拿屏幕来比较,其实是因为tableView的尺寸我默认等于屏幕的高度,正常应该是tableView的高度。
    // cell在屏幕上, cell的最大y值 > offsetY && cell的y值 < 屏幕的最大Y值(屏幕的高度 + offsetY)
    
    CGFloat offsetY = self.contentOffset.y;
    
    return CGRectGetMaxY(cellF) > offsetY && cellF.origin.y < self.bounds.size.height + offsetY;     }

6.在滚动的时候,如果有新的cell出现在屏幕上,先从缓存池中取,没有取到,在创建新的cell.

分析:

  • 需要及时监听tableView的滚动,判断下有没有新的cell出现。

  • 大 家都会想到scrollViewDidScroll方法,这个方法只要一滚动scrollView就会调用,但是这个方法有个弊端,就是 tableView内部需要作为自身的代理,才能监听,这样不好,有时候外界也需要监听滚动,因此自身类最好不要成为自己的代理。(设计思想)

解决:

  • 重写layoutSubviews,判断当前哪些cell显示在屏幕上。

  • 因为只要一滚动,就会修改contentOffset,就会调用layoutSubviews,其实修改contentOffset,内部其实是修改tableView的bounds,而layoutSubviews刚好是父控件尺寸一改就会调用.具体需要了解scrollView底层实现

思路:

判断下,当前tableView内容往上移动,还是往下移动,如何判断,取出显示在屏幕上的第一次cell,当前偏移量 > 第一个cell的y值,往下走。

需要搞个数组记录下,当前有多少cell显示在屏幕上,在一开始的时候记录.

@interface YZTableView ()

@property (nonatomic, strong) NSMutableArray *visibleCells;

@end

@implementation YZTableView

@dynamic delegate;

- (NSMutableArray *)visibleCells
{     if (_visibleCells == nil) {
        _visibleCells = [NSMutableArray array];
    }
    return _visibleCells;
    
}
@end
  • 往下移动

1.如果已经滚动到tableView内容最底部,就不需要判断新的cell,直接返回.

2.需要判断之前显示在屏幕cell有没有移除屏幕

3.只需要判断下当前可见cell数组中第一个cell有没有离开屏幕

4.只需要判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上即可。

  // 判断有没有滚动到最底部
        if (offsetY + self.bounds.size.height > self.contentSize.height) {
            return;
        }
        
        // 判断下当前可见cell数组中第一个cell有没有离开屏幕
        if ([self isInScreen:firstCell.frame] == NO) { // 如果不在屏幕
            // 从可见cell数组移除
            [self.visibleCells removeObject:firstCell];
            
            // 删除第0个从可见的indexPath
            [self.visibleIndexPaths removeObjectAtIndex:0];
            
            // 添加到缓存池中
            [self.reuserCells addObject:firstCell];
            
            // 移除父控件
            [firstCell removeFromSuperview];
            
        }
        // 判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上
        // 这里需要计算下一个cell的y值,需要获取对应的cell的高度
        // 而高度需要根据indexPath,从数据源获取
        // 可以数组记录每个可见cell的indexPath的顺序,然后获取对应可见的indexPath的角标,就能获取下一个indexPath.
        
        // 获取最后一个cell的indexPath
        NSIndexPath *indexPath = [self.visibleIndexPaths lastObject];
        
        // 获取下一个cell的indexPath
        NSIndexPath *nextIndexPath = [NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0];
        
        // 获取cell的高度
        if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
            cellH = [self.delegate tableView:self heightForRowAtIndexPath:nextIndexPath];
        }else{
            cellH = 44;
        }
        
        // 计算下一个cell的y值
        cellY = lastCellY + cellH;
        
        // 计算下下一个cell的frame
        CGRect nextCellFrame = CGRectMake(cellX, cellY, cellW, cellH);
        
        if ([self isInScreen:nextCellFrame]) { // 如果在屏幕上,就加载
            
            // 通过数据源获取cell
            UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:nextIndexPath];
            
            cell.frame = nextCellFrame;
            
            [self insertSubview:cell atIndex:0];
            
            // 添加到cell可见数组中
            [self.visibleCells addObject:cell];
            
            // 添加到可见的indexPaths数组
            [self.visibleIndexPaths addObject:nextIndexPath];
            
            
        }
  • 往上移动

1.如果已经滚动到tableView最顶部,就不需要判断了有没有心的cell,直接返回.

2.需要判断之前显示在屏幕cell有没有移除屏幕

3.只需要判断下当前可见cell数组中最后一个cell有没有离开屏幕

4.只需要判断下可见cell数组中第一个cell的上一个cell显没显示在屏幕上即可

注意点:如果可见cell数组中第一个cell的上一个cell显示到屏幕上,一定要记得是插入到可见数组第0个的位置。

        // 判断有没有滚动到最顶部
        if (offsetY < 0) {
            return;
        }
        
        
        
        // 判断下当前可见cell数组中最后一个cell有没有离开屏幕
        if ([self isInScreen:lastCell.frame] == NO) { // 如果不在屏幕
            // 从可见cell数组移除
            [self.visibleCells removeObject:lastCell];
            
            // 删除最后一个可见的indexPath
            [self.visibleIndexPaths removeLastObject];
            
            // 添加到缓存池中
            [self.reuserCells addObject:lastCell];
            
            // 移除父控件
            [lastCell removeFromSuperview];
            
        }
        
        // 判断下可见cell数组中第一个cell的上一个cell显没显示在屏幕上
        // 获取第一个cell的indexPath
        NSIndexPath *indexPath = self.visibleIndexPaths[0];
        
        
        // 获取下一个cell的indexPath
        NSIndexPath *preIndexPath = [NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0];
        
        // 获取cell的高度
        if ([self.delegate respondsToSelector:@selector(tableView:heightForRowAtIndexPath:)]) {
            cellH = [self.delegate tableView:self heightForRowAtIndexPath:preIndexPath];
        }else{
            cellH = 44;
        }
        
        // 计算上一个cell的y值
        cellY = firstCellY - cellH;
        
        
        // 计算上一个cell的frame
        CGRect preCellFrame = CGRectMake(cellX, cellY, cellW, cellH);
        
        if ([self isInScreen:preCellFrame]) { // 如果在屏幕上,就加载
            
            // 通过数据源获取cell
            UITableViewCell *cell = [self.dataSource tableView:self cellForRowAtIndexPath:preIndexPath];
            
            cell.frame = preCellFrame;
            
            [self insertSubview:cell atIndex:0];
            
            // 添加到cell可见数组中,这里应该用插入,因为这是最上面一个cell,应该插入到数组第0个
            [self.visibleCells insertObject:cell atIndex:0];
            
            // 添加到可见的indexPaths数组,这里应该用插入,因为这是最上面一个cell,应该插入到数组第0个
            [self.visibleIndexPaths insertObject:preIndexPath atIndex:0];
            
        }
        
    }

问题1:

  • 判断下当前可见cell数组中最后一个cell的下一个cell显没显示在屏幕上

  • 这里需要计算下一个cell的frame,frame就需要计算下一个cell的y值,需要获取对应的cell的高度 cellY = lastCellY + cellH

  • 而高度需要根据indexPath,从数据源获取

解决:

  • 可以搞个字典记录每个可见cell的indexPath,然后获取对应可见的indexPath,就能获取下一个indexPath.

@interface YZTableView ()

// 屏幕可见数组
@property (nonatomic, strong) NSMutableArray *visibleCells; // 缓存池
@property (nonatomic, strong) NSMutableSet *reuserCells; // 记录每个可见cell的indexPaths的顺序
@property (nonatomic, strong) NSMutableDictionary *visibleIndexPaths; @end - (NSMutableDictionary *)visibleIndexPaths
{
    if (_visibleIndexPaths == nil) {
        _visibleIndexPaths = [NSMutableDictionary dictionary];
    }
    
    return _visibleIndexPaths;
}

注意:

  • 当cell从缓存池中移除,一定要记得从可见数组cell中移除,还有可见cell的indexPath也要移除.

        // 判断下当前可见cell数组中第一个cell有没有离开屏幕
        if ([self isInScreen:firstCell.frame] == NO) { // 如果不在屏幕
            // 从可见cell数组移除
            [self.visibleCells removeObject:firstCell];
            
            // 删除第0个从可见的indexPath
            [self.visibleIndexPaths removeObjectAtIndex:0];
            
            // 添加到缓存池中
            [self.reuserCells addObject:firstCell];
            
        }
        
 // 判断下当前可见cell数组中最后一个cell有没有离开屏幕
        if ([self isInScreen:lastCell.frame] == NO) { // 如果不在屏幕
            // 从可见cell数组移除
            [self.visibleCells removeObject:lastCell];
            
            // 删除最后一个可见的indexPath
            [self.visibleIndexPaths removeLastObject];
            
            // 添加到缓存池中
            [self.reuserCells addObject:lastCell];
            
        }

7.缓存池搭建,缓存池其实就是一个NSSet集合。

  • 搞一个NSSet集合充当缓存池.

  • cell离开屏幕,放进缓存池

  • 提供从缓存池获取方法,从缓存池中获取cell,记住要从NSSet集合移除cell.

@interface YZTableView ()

// 屏幕可见数组
@property (nonatomic, strong) NSMutableArray *visibleCells; // 缓存池
@property (nonatomic, strong) NSMutableSet *reuserCells; // 记录每个cell的y值都对应一个indexPath
@property (nonatomic, strong) NSMutableDictionary *indexPathDict; @end
@implementation YZTableView
- (NSMutableSet *)reuserCells
{
    if (_reuserCells == nil) {
        _reuserCells = [NSMutableSet set];
    }
    return _reuserCells;
} // 从缓存池中获取cell
- (id)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
    UITableViewCell *cell = [self.reuserCells anyObject];
    
    // 能取出cell,并且cell的标示符正确
    if (cell && [cell.reuseIdentifier isEqualToString:identifier]) {     
        // 从缓存池中获取
        [self.reuserCells removeObject:cell];
        
        return cell;
    }
    return nil;
} @end

8.tableView细节处理

原因:刷新方法经常要调用

解决:每次刷新的时候,先把之前记录的全部清空

// 刷新tableView
- (void)reloadData
{
    
    // 刷新方法经常要调用
    // 每次刷新的时候,先把之前记录的全部清空
    // 清空indexPath字典
    [self.indexPathDict removeAllObjects];
    // 清空屏幕可见数组
    [self.visibleCells removeAllObjects];
    ...
}

如果你喜欢这篇文章,可以继续关注我,微博:吖了个峥,欢迎交流。

点击这下载源代码
(PS:另外咱们公司小码哥,诚邀IT届有事业心,有能力,有拼劲,有干劲各路英豪加盟一起创业,详情可以点击小码哥,小码哥官方微博,或者微博私聊我)

实现UITableView循环利用的更多相关文章

  1. iOS边练边学--UITableView性能优化之三种方式循环利用

    一.cell的循环利用方式1: /** * 什么时候调用:每当有一个cell进入视野范围内就会调用 */ - (UITableViewCell *)tableView:(UITableView *)t ...

  2. 解决cell循环利用造成的重复勾选

    @interface ProfessionViewController (){ NSMutableArray *_professionArray;//cell模型数组 NSMutableArray * ...

  3. 关于UITableViewCell的循环利用--面向初学者

    UITableViewCell的重复利用机制有效地节省内存开销和提高程序性能. 1 原理 tableView拥有一个缓存池,存放未在使用(没有显示在界面)的cell. tableView有一行cell ...

  4. iOS开发:一个瀑布流的设计与实现(已实现缓存池功能,该功能使得瀑布流cell可以循环利用)

    一个瀑布流的实现有三种方式: 继承自UIScrollView,仿写UITableView的dataSource和delegate,创造一个缓存池用来实现循环利用cell 写多个UITableview( ...

  5. iOS开发小技巧--TableView中headerView的循环利用,以及自定义的headerView

    一.首先要搞清楚,tableView中有两种headerView,一个是tableHeaderView,另一个是headerView.前者就一个;后者根据session决定个数 headerView的 ...

  6. iOS开发UI篇—无限轮播(循环利用)

    iOS开发UI篇—无限轮播(循环利用) 一.无限轮播  1.简单说明 在开发中常需要对广告或者是一些图片进行自动的轮播,也就是所谓的无限滚动. 在开发的时候,我们通常的做法是使用一个UIScrollV ...

  7. [UGUI]滑动列表优化(循环利用)

    需要注意的有下面几点: 1. 区分好表现上的index和逻辑上的index.表现上的index是指这个go是go列表中的第几项,但实际上这个index的意义并不大,因为在滚动的过程中go列表是轮转的: ...

  8. cell的循环利用

    方式1 // 1.先根据cell的标识去缓存池中查找可循环利用的cell UITableViewCell *cell = [tableView dequeueReusableCellWithIdent ...

  9. iOS探究UITableView的内部代码,仿UITableView自定义

    大家都知道UITableView,最经典在于循环利用,这里我自己模仿UITableView循环利用,写了一套自己的TableView实现方案,希望大家看了我的文章,循环利用思想有显著提升. 研究UIT ...

随机推荐

  1. 解决MYSQL错误:ERROR 1040 (08004): Too many connections

    方法一: show processlist; show variables like 'max_connections'; show global status like 'max_used_conn ...

  2. 说说JSON和JSONP,也许你会豁然开朗,含jQuery用例 分类: JavaScript 2014-09-23 10:41 218人阅读 评论(1) 收藏

    前言: 由于Sencha Touch 2这种开发模式的特性,基本决定了它原生的数据交互行为几乎只能通过AJAX来实现. 当然了,通过调用强大的PhoneGap插件然后打包,你可以实现100%的Sock ...

  3. android 入门-安装环境

    1.安装jdk 相关链接 2.安装adt 里面包含eclipse 3.下载androidsdk 4.打开eclipse 找到windows -> 属性 -> android 主目录 复制 ...

  4. 【leetcode】Remove Duplicates from Sorted Array

    题目描述: Given a sorted array, remove the duplicates in place such that each element appear only once a ...

  5. STL Map的使用

    Map是STL的一个关联容器,它提供一对一(其中第一个可以称为关键字,每个关键字只能在map中出现一次,第二个可能称为该关键字的值)的数据处理能力.下面就通过示例记录一下map的使用: 一.向map中 ...

  6. HR外包系统 - 客户公司薪资规则 报表需求 记入系统

    1 薪酬规则,包括 常用薪资项目 2 报表需求,特别是报表排序规则 3 特殊项说明记录 另外包括客户公司监控的日期设置

  7. CoreLocation 下的定位跟踪测速

    #import "ViewController.h" #import <CoreLocation/CoreLocation.h> @interface ViewCont ...

  8. 映射一对多双向关联关系 cascade、inverse、属性

    当类与类之间建立了关联,就可以方便的从一个对象导航到另一个对象.或者通过集合导航到一组对象.例如: 对于给定的Emp对象,如果想获得与它关联的Dept对象,只要调用如下方法 Dept dept=emp ...

  9. 用户视角 vs 系统视角 看性能

    如何评价性能的优劣: 用户视角 vs. 系统视角 对于最终用户(End-User)来说,评价系统的性能好坏只有一个字——“快”.最终用户并不需要关心系统当前的状态——即使系统这时正在处理着成千上万的请 ...

  10. JMeter中的关联-正则表达式提取(1)

    运用Jmeter正则提取器,可以从请求的响应结果中取到需要的内容,从而实现关联. jmeter之关联 的个人理解: 关联是请求与请求之间存在数据依赖关系,需要从上一个请求获取下一个请求需要回传回去的数 ...