tableView原理分析

分析原生TableView数据加载过程

图一

当tableView走reloadData方法的时候,走的方法顺序如下:

  • numberOfSectionsInTableView
  • numberOfRowsInSection
  • heightForRowAtIndexPath
  • cellForRowAtIndexPath
  • heightForRowAtIndexPath

会先实现一个tableView中有几组数据,然后每组有多少行,然后再每组默认的高度,然后再加载cell,最后再重新返回一次准确的高度,并且加载的也是当前页面的cell,超过页面的不会加载。
其中当第一次运行tableView的时候,即一开始进来,tableView展示数据,而不是走reloadData方法的时候,上面图一中红色框的方法会多走两遍,而且就算是有多组,每组也会多走两遍。
如果每个cell的高度不一,需要动态计算cell的高度,我们可以第一次算出来过后将cell的高度保存,避免每次滑动tableView,或者reloadData的时候都再计算一次cell的高度。

模仿tableView

由系统的可知,要实现一个tableView,需要继承自UIScrollView,并且定义一个tableView代理和代理中必须实现的方法

#import <UIKit/UIKit.h>
#import "EOCTableViewCell.h"

@class EOCTableView;
@protocol EOCTableViewDelegate <NSObject>
@required
- (NSInteger)eocTableView:(EOCTableView *)tableView numberOfRowsInSection:(NSInteger)section;
- (CGFloat)eocTableView:(EOCTableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
- (EOCTableViewCell *)eocTableView:(EOCTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
@end

@interface EOCTableView : UIScrollView{
    NSMutableArray *_rowModeAry;                  //装cell的起始Y值和高度的模型数组
    NSMutableArray *_reuseCellPoolArr;            //重用池
    NSMutableDictionary *_visibleCellPoolDict;    //现有池
}

@property (nonatomic, weak)id<EOCTableViewDelegate> eocDelegate;
// 从重用池拿cell的方法
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier;
- (void)reloadData;
@end

还需要自定义一个默认有label的tableViewCell,而且还必须要绑定identifier属性

#import <UIKit/UIKit.h>

@interface EOCTableViewCell : UIView

- (id)initWithIdentifier:(NSString*)identifier;
@property (nonatomic, strong)UILabel *textLabel;
@property (nonatomic, strong)NSString *identifier;

@end
#import "EOCTableViewCell.h"

@implementation EOCTableViewCell

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        _textLabel = [[UILabel alloc] init];
        [self addSubview:_textLabel];
    }
    return self;
}
- (id)initWithIdentifier:(NSString*)identifier
{
    self = [super init];
    if (self) {
        _identifier = identifier;
    }
    return self;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    [_textLabel setFrame:CGRectMake(15, 0, self.frame.size.width-15, self.frame.size.height)];
}
@end

还需要一个用于保存每个cell的identifier和起始Y坐标和cell高度的模型Model

#import <Foundation/Foundation.h>

@interface RowInfoModel : NSObject
@property (nonatomic, strong)NSString *identifi;
@property (nonatomic, assign)float originY;
@property (nonatomic, assign)float sizeHeight;
@end

.m文件中不做什么操作

reloadData方法的内部实现
- (void)reloadData{
    
    [self countRowPosition];// 数据准备
    [self setNeedsLayout];// setNeedsLayout不会在当前事件循环操作,会放在下一个事件循环
}

// 保存postion数据
- (void)countRowPosition{
    // 先清空原数据,避免叠加
    [_rowModeAry removeAllObjects];
    float addUpHigh = 0;
    
    for (int i = 0; i < [_eocDelegate eocTableView:self numberOfRowsInSection:0]; i++) {
       
        NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:0];
        //有多少cell就要先走多少遍heightForRowAtIndexPath这个方法
        float cellHigh = [_eocDelegate eocTableView:self heightForRowAtIndexPath:path];
        RowInfoModel *rowModel = [[RowInfoModel alloc] init];
        rowModel.originY = addUpHigh;   //记录每个cell的初始Y值
        rowModel.sizeHeight = cellHigh; //记录每个cell的高度
        [_rowModeAry addObject:rowModel];  //添加到cell的数据数组中
        addUpHigh += cellHigh;    // 累积得到所有cell加在一起的高度,即contentSize
    }
    //重新设置tableView的contentSize
    [self setContentSize:CGSizeMake(self.frame.size.width, addUpHigh)];
}

setNeedsLayout方法并不是同步的,它不会在本次的事件循环中进行操作,它会在下次的事件循环中进行操作,所以当你刚刚调用了tableView的reloadData方法后,去获取tableView的contentSize之类的会往往不准确,从而造成一些BUG。

setNeedsLayout中layoutSubviews中进行的操作
- (void)layoutSubviews{
    [super layoutSubviews];
    //contentOffset的y值小于0就取0,从0开始添加cell
    float startY = (self.contentOffset.y < 0)?0:self.contentOffset.y;
    // (startY + self.frame.size.height)>self.contentSize.height 即内容没有超过屏幕,所以取内容的高
    float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
    
    RowInfoModel *rowStartModel = [RowInfoModel new];
    rowStartModel.originY = startY;
    
    RowInfoModel *rowEndModel = [RowInfoModel new];
    rowEndModel.originY = endY;
    //二分查找法找出可视区域内的第一个和最后一个cell
    NSInteger startIndex =  [self binarySearchFromAry:_rowModeAry object:rowStartModel];
    NSInteger endIndex   = [self binarySearchFromAry:_rowModeAry object:rowEndModel];
    
    //NSRange visibleCellRange = [self visibleRowRange];
    NSRange visibleCellRange = NSMakeRange(startIndex, endIndex-startIndex+1);
    
    // 系统的二分查找法
    startIndex = [_rowModeAry indexOfObject:rowStartModel inSortedRange:NSMakeRange(0, _rowModeAry.count-1) options:NSBinarySearchingInsertionIndex usingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        RowInfoModel *rowStartModel = obj1;
        RowInfoModel *rowEndModel = obj2;
        
        if (rowStartModel.originY > rowEndModel.originY &&rowStartModel.originY > rowEndModel.originY + rowEndModel.sizeHeight) {
            return NSOrderedSame;
        }else if (rowStartModel.originY < rowEndModel.originY) {
            return NSOrderedDescending;
        }else{
            return NSOrderedAscending;
        }
    }];
    
   // NSLog(@"%@--%@", NSStringFromRange(visibleCellRange), NSStringFromRange(binaryVisibleCellRange));
    
    //加载cell界面
    /*
     1,不在可视界面的cell,remove -->  重用池
     2. 加载即将出现的cell。  首先判断重用池里面是否有cell,有就重用,没有就申请新的
        重用池(保存不在界面上的)   现有池(保存在界面上的cell)
     */
    
    //visibleCellRange.location 即是startIndex   visibleCellRange.location + visibleCellRange.length 即是endIndex
    for (NSInteger i = visibleCellRange.location; i < visibleCellRange.location + visibleCellRange.length; i++) {
        
        //如果cell已经在可视界面中,就不需要再重新加载了
        EOCTableViewCell *cell = _visibleCellPoolDict[@(i)];
        if (!cell) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
            cell = [_eocDelegate eocTableView:self cellForRowAtIndexPath:indexPath];
            //保存到现有池中
            [_visibleCellPoolDict setObject:cell forKey:@(i)];
            //从复用池中移除,一定要先保存在现有池中再remove,不然很有可能就被释放掉了
            [_reuseCellPoolArr removeObject:cell];
        }
        
        RowInfoModel *rowInfoM = _rowModeAry[i];
        cell.frame = CGRectMake(0, rowInfoM.originY, self.frame.size.width, rowInfoM.sizeHeight);
        
        //cell的父视图还不存在,即cell还没有被添加,这时就应该添加cell
        if (![cell superview]) {
            [self addSubview:cell];
        }
    }
    
    // 移除不在可见试图上的cell
    NSArray *vCellKeyAry = [_visibleCellPoolDict allKeys];
    for (int i = 0; i < vCellKeyAry.count; i++) {
        // 哪些不在界面上移除到 重用池
        NSInteger indexKey = [vCellKeyAry[i] integerValue];
        if (indexKey < visibleCellRange.location || indexKey > visibleCellRange.location + visibleCellRange.length) {  //不在可见cell范围内
            
            //添加到复用池
            [_reuseCellPoolArr addObject:_visibleCellPoolDict[@(indexKey)]];
            //从现有池中移走
            [_visibleCellPoolDict removeObjectForKey:@(indexKey)];
        }
    }
}
dequeueReusableCellWithIdentifier方法的内部实现
- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier{
    //遍历复用池数组,查找是否有identifier相关的cell
    for (int i = 0; i < _reuseCellPoolArr.count; i++) {
        EOCTableViewCell *cell = _reuseCellPoolArr[i];
        if ([cell.identifier isEqual:identifier]) {
            return cell;
        }
    }
    return nil;
}

即是在复用池中查找有相同identifier的cell,如果有就取出来重用,没有就返回nil,下一步进行创建

二分查找算当前可见区域的cell(第一个和最后一个cell是多少)

二分法需要一个有序的数组,而且查找的特别快,适合数量比较大的查找

/*
 二分算法  数组有序
  3
 
 1 2 3 4 5 6  7 8 9 10 11 12
 123   456
 
 1 - 1024    1024 = 2*8
 1-512  512-1024
 
 1-256  256-512
 
 */

/* a[1024]   x = 0  y = 1024  v = 123   a是长度为1024的数组,x是最小值,y是最大值,v是中间值*/
// C语言的写法
int binarySearch(int * a, int min, int max, int v)//半开区间[x,y)
{
    int mid;
    while(min < max)
    {
        mid = min + (max-min)/2; // mid = 512
        if(v == a[mid]) {
            return  mid;//找到了
        }
        else if(v < a[mid]){
            max = mid;//在左边
        }
        else {
            min = mid+1;//在右边
        }
    }
    return -1;
}
//OC的写法
- (NSInteger)binarySearchFromAry:(NSArray*)arry object:(RowInfoModel*)targetModel{
    NSInteger min = 0;
    NSInteger max = arry.count -1;
    NSInteger mid;
    while (min < max) {
        mid = min + (max - min)/2;
        RowInfoModel *midModel = arry[mid];
        if (targetModel.originY >= midModel.originY && targetModel.originY < midModel.originY + midModel.sizeHeight) {
            return mid;
        }else if(targetModel.originY < midModel.originY){  // 在左边
            max = mid;
            if (max - min == 1) {  // 在左边的时候,只剩两个的时候,返回小的那个
                return min;
            }
        }else{                                            // 在右边
            min = mid;
            if (max - min == 1) {  // 在右边的时候,只剩两个的时候,返回大的那个
                return max;
            }
        }
    }
    return -1;
}

最后附上整个tableView的.m文件

#import "EOCTableView.h"
#import "EOCTableViewCell.h"
#import "RowInfoModel.h"

@implementation EOCTableView{
    
}

- (instancetype)initWithFrame:(CGRect)frame{
    
    self = [super initWithFrame:frame];
    if (self) {
        _rowModeAry = [NSMutableArray array];
        _reuseCellPoolArr = [NSMutableArray array];
        _visibleCellPoolDict = [NSMutableDictionary dictionary];
    }
    return self;
}
/*
 先配置好数据 
 再配置试图
 
 先计算所有cell高度 
 再计算每个cell位置Y
 
 */

- (void)layoutSubviews{
    [super layoutSubviews];
    //contentOffset的y值小于0就取0,从0开始添加cell
    float startY = (self.contentOffset.y < 0)?0:self.contentOffset.y;
    // (startY + self.frame.size.height)>self.contentSize.height 即内容没有超过屏幕,所以取内容的高
    float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
    
    RowInfoModel *rowStartModel = [RowInfoModel new];
    rowStartModel.originY = startY;
    
    RowInfoModel *rowEndModel = [RowInfoModel new];
    rowEndModel.originY = endY;
    //二分查找法找出可视区域内的第一个和最后一个cell
    NSInteger startIndex =  [self binarySearchFromAry:_rowModeAry object:rowStartModel];
    NSInteger endIndex   = [self binarySearchFromAry:_rowModeAry object:rowEndModel];
    
    //NSRange visibleCellRange = [self visibleRowRange];
    NSRange visibleCellRange = NSMakeRange(startIndex, endIndex-startIndex+1);
    
    // 系统的二分查找法
    startIndex = [_rowModeAry indexOfObject:rowStartModel inSortedRange:NSMakeRange(0, _rowModeAry.count-1) options:NSBinarySearchingInsertionIndex usingComparator:^NSComparisonResult(id  _Nonnull obj1, id  _Nonnull obj2) {
        RowInfoModel *rowStartModel = obj1;
        RowInfoModel *rowEndModel = obj2;
        
        if (rowStartModel.originY > rowEndModel.originY &&rowStartModel.originY > rowEndModel.originY + rowEndModel.sizeHeight) {
            return NSOrderedSame;
        }else if (rowStartModel.originY < rowEndModel.originY) {
            return NSOrderedDescending;
        }else{
            return NSOrderedAscending;
        }
    }];
    
   // NSLog(@"%@--%@", NSStringFromRange(visibleCellRange), NSStringFromRange(binaryVisibleCellRange));
    
    //加载cell界面
    /*
     1,不在可视界面的cell,remove -->  重用池
     2. 加载即将出现的cell。  首先判断重用池里面是否有cell,有就重用,没有就申请新的
        重用池(保存不在界面上的)   现有池(保存在界面上的cell)
     */
    
    //visibleCellRange.location 即是startIndex   visibleCellRange.location + visibleCellRange.length 即是endIndex
    for (NSInteger i = visibleCellRange.location; i < visibleCellRange.location + visibleCellRange.length; i++) {
        
        //如果cell已经在可视界面中,就不需要再重新加载了
        EOCTableViewCell *cell = _visibleCellPoolDict[@(i)];
        if (!cell) {
            NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
            cell = [_eocDelegate eocTableView:self cellForRowAtIndexPath:indexPath];
            //保存到现有池中
            [_visibleCellPoolDict setObject:cell forKey:@(i)];
            //从复用池中移除,一定要先保存在现有池中再remove,不然很有可能就被释放掉了
            [_reuseCellPoolArr removeObject:cell];
        }
        
        RowInfoModel *rowInfoM = _rowModeAry[i];
        cell.frame = CGRectMake(0, rowInfoM.originY, self.frame.size.width, rowInfoM.sizeHeight);
        
        //cell的父视图还不存在,即cell还没有被添加,这时就应该添加cell
        if (![cell superview]) {
            [self addSubview:cell];
        }
    }
    
    // 移除不在可见试图上的cell
    NSArray *vCellKeyAry = [_visibleCellPoolDict allKeys];
    for (int i = 0; i < vCellKeyAry.count; i++) {
        // 哪些不在界面上移除到 重用池
        NSInteger indexKey = [vCellKeyAry[i] integerValue];
        if (indexKey < visibleCellRange.location || indexKey > visibleCellRange.location + visibleCellRange.length) {  //不在可见cell范围内
            
            //添加到复用池
            [_reuseCellPoolArr addObject:_visibleCellPoolDict[@(indexKey)]];
            //从现有池中移走
            [_visibleCellPoolDict removeObjectForKey:@(indexKey)];
        }
    }
}

- (EOCTableViewCell *)dequeueReusableCellWithIdentifier:(NSString*)identifier{
    //遍历复用池数组,查找是否有identifier相关的cell
    for (int i = 0; i < _reuseCellPoolArr.count; i++) {
        EOCTableViewCell *cell = _reuseCellPoolArr[i];
        if ([cell.identifier isEqual:identifier]) {
            return cell;
        }
    }
    return nil;
}

/* section = 0*/

- (void)reloadData{
    
    [self countRowPosition];// 数据准备
    [self setNeedsLayout];// setNeedsLayout不会在当前事件循环操作,会放在下一个事件循环
}

// 保存postion数据
- (void)countRowPosition{
    // 先清空原数据,避免叠加
    [_rowModeAry removeAllObjects];
    float addUpHigh = 0;
    
    for (int i = 0; i < [_eocDelegate eocTableView:self numberOfRowsInSection:0]; i++) {
       
        NSIndexPath *path = [NSIndexPath indexPathForRow:i inSection:0];
        //有多少cell就要先走多少遍heightForRowAtIndexPath这个方法
        float cellHigh = [_eocDelegate eocTableView:self heightForRowAtIndexPath:path];
        RowInfoModel *rowModel = [[RowInfoModel alloc] init];
        rowModel.originY = addUpHigh;   //记录每个cell的初始Y值
        rowModel.sizeHeight = cellHigh; //记录每个cell的高度
        [_rowModeAry addObject:rowModel];  //添加到cell的数据数组中
        addUpHigh += cellHigh;    // 累积得到所有cell加在一起的高度,即contentSize
    }
    //重新设置tableView的contentSize
    [self setContentSize:CGSizeMake(self.frame.size.width, addUpHigh)];
}
// 计算当前可见区域的 cell
- (NSRange)visibleRowRange{
    
    //contentOffset的y值小于0就取0,从0开始添加cell
    float startY = (self.contentOffset.y < 0)?0:self.contentOffset.y;
    // (startY + self.frame.size.height)>self.contentSize.height 即内容没有超过屏幕,所以取内容的高
    float endY = (startY + self.frame.size.height)>self.contentSize.height?self.contentSize.height:startY + self.frame.size.height;
    
    //开始索引
    NSInteger startIndex = -1;
    //结束索引
    NSInteger endIndex =  - 1;
    for (int i = 0; i < _rowModeAry.count; i++) {
        
        RowInfoModel *rowInfoM = _rowModeAry[i];
        if (startIndex == -1) {
            //找到可见区域内的第一个cell
            if (startY >= rowInfoM.originY && startY < rowInfoM.originY + rowInfoM.sizeHeight) {
                startIndex = i;
            }
        }else{
            //找到可见区域内的最后一个cell
            if (endY >= rowInfoM.originY && endY < rowInfoM.originY + rowInfoM.sizeHeight) {
                endIndex = i;
            }
        }
    }
    // 需要 +1  是因为startIndex是从0开始的
    return NSMakeRange(startIndex, endIndex-startIndex+1);
}

/*
 二分算法  数组有序
  3
 
 1 2 3 4 5 6  7 8 9 10 11 12
 123   456
 
 1 - 1024    1024 = 2*8
 1-512  512-1024
 
 1-256  256-512
 
 */

/* a[1024]   x = 0  y = 1024  v = 123  a是长度为1024的数组,x是最小值,y是最大值,v是中间值*/
int binarySearch(int * a, int min, int max, int v)//半开区间[x,y)
{
    int mid;
    while(min < max)
    {
        mid = min + (max-min)/2; // mid = 512
        if(v == a[mid]) {
            return  mid;//找到了
        }
        else if(v < a[mid]){
            max = mid;//在左边
        }
        else {
            min = mid+1;//在右边
        }
    }
    return -1;
}


- (NSInteger)binarySearchFromAry:(NSArray*)arry object:(RowInfoModel*)targetModel{
    
    NSInteger min = 0;
    NSInteger max = arry.count -1;
    NSInteger mid;
    while (min < max) {
        mid = min + (max - min)/2;
        RowInfoModel *midModel = arry[mid];
        if (targetModel.originY >= midModel.originY && targetModel.originY < midModel.originY + midModel.sizeHeight) {
            return mid;
        }else if(targetModel.originY < midModel.originY){  // 在左边
            max = mid;
            if (max - min == 1) {  // 在左边的时候,只剩两个的时候,返回小的那个
                return min;
            }
        }else{                                            // 在右边
            min = mid;
            if (max - min == 1) {  // 在右边的时候,只剩两个的时候,返回大的那个
                return max;
            }
        }
    }
    return -1;
}
@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,312评论 5 473
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,578评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,337评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,134评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,161评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,303评论 1 280
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,761评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,421评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,609评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,450评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,504评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,194评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,760评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,836评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,066评论 1 257
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,612评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,178评论 2 341

推荐阅读更多精彩内容