美文网首页
tableView原理分析

tableView原理分析

作者: 简_爱SimpleLove | 来源:发表于2017-06-14 17:53 被阅读201次

    分析原生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
    

    相关文章

      网友评论

          本文标题:tableView原理分析

          本文链接:https://www.haomeiwen.com/subject/qfuofxtx.html