UITableViewCell的重用机制原理

重用机制原理

每当有一个cell从屏幕消失,就将其放到缓存池中,如果有新的cell出现,就去缓存池中取,如果缓存池中没有,再创建。

首先来做一个验证
代码如下:

/**
 打印屏幕可见Cell总数和可见Cell最后一个的Tag
 */
- (void)printVisibleCellInfo
{
    NSArray *visibleCells = self.tableView.visibleCells;
    NSLog(@"visibleCells count:%zd",visibleCells.count);
    
    UITableViewCell *lastCell = [visibleCells lastObject];
    NSLog(@"last Cell Tag is:%zd",lastCell.tag);
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return 60;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *cellIdentifier = @"myCell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
    
    if (cell == nil) {
        cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"Cell Index--%zd",indexPath.row];
    
    cell.tag = 1000+indexPath.row;
    
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 120;
}


- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
    [self scrollViewEndScroll:scrollView];
}

- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
    if ( !decelerate ) {
        [self scrollViewEndScroll:scrollView];
    }
}

- (void)scrollViewEndScroll:(UIScrollView *)scrollView {
    // do something ...
    [self printVisibleCellInfo];
}

在iPhone X下运行效果如下:


UITableViewCell的重用机制原理_第1张图片
Simulator Screen Shot - iPhone X - 2018-10-22 at 09.46.11_gaitubao_com_562x1124.png

此时屏幕显示可见Cell数为6个,打印visibleCells.count:7,last Cell Tag is:10007,滚动TableView,visibleCells.count不变。

由此可推断

TableVie并不会为每个要显示的数据都创建一个Cell,一般情况下只创建屏幕可显示的最大的cell个数+1,每当有一个cell从屏幕消失,就将其放到缓存池中,如果有新的cell出现,就去缓存池中取,如果缓存池中没有,再创建。

有个疑问:存放Cell重用标识的缓存原理?
参考initWithFrame: style: 方法源码如下

- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle
{
    if ((self=[super initWithFrame:frame])) {
        _style = theStyle;

        //_cachedCells 用于保存正在显示的Cell对象的引用
        _cachedCells = [[NSMutableDictionary alloc] init];

        //在计算完每个 section 包含的 section 头部,尾部视图的高度,和包含的每个 row 的整体高度后,
        //使用 UITableViewSection 对象对这些高度值进行保存,并将该 UITableViewSection 对象的引用
        //保存到 _sections中。在指定完 dataSource 后,至下一次数据源变化调用 reloadData 方法,
        //由于数据源没有变化,section 相关的高度值是不会变化,只需计算一次,所以需要缓存起来。
        _sections = [[NSMutableArray alloc] init];

        //_reusableCells用于保存存在但未显示在界面上的可复用的Cell
        _reusableCells = [[NSMutableSet alloc] init];
        self.separatorColor = [UIColor colorWithRed:.88f green:.88f blue:.88f alpha:1];
        self.separatorStyle = UITableViewCellSeparatorStyleSingleLine;
        self.showsHorizontalScrollIndicator = NO;
        self.allowsSelection = YES;
        self.allowsSelectionDuringEditing = NO;
        self.sectionHeaderHeight = self.sectionFooterHeight = 22;
        self.alwaysBounceVertical = YES;

        if (_style == UITableViewStylePlain) {
            self.backgroundColor = [UIColor whiteColor];
        }
        [self _setNeedsReload];
    }
    return self;
}
UITableView绘制
- (void)layoutSubviews
{
    //对子视图进行布局,该方法会在第一次设置数据源调用 setNeedsLayout 方法后自动调用。
    //并且 UITableView 是继承自 UIScrollview ,当滚动时也会触发该方法的调用
    _backgroundView.frame = self.bounds;
    
    //在进行布局前必须确保 section 已经缓存了所有高度相关的信息
    [self _reloadDataIfNeeded]; 
    
    //对 UITableView 的 section 进行布局,包含 section 的头部,尾部,每一行 Cell
    [self _layoutTableView];
    
    //对 UITableView 的头视图,尾视图进行布局
    [super layoutSubviews];
}

需要注意的是由于 UITableView 是继承于 UIScrollView,所以在 UITableView 滚动时会自动调用该方法.

_reloadDataIfNeeded 的源码如下
- (void)_reloadDataIfNeeded
{
    if (_needsReload) {
        [self reloadData];
    }
}

- (void)reloadData
{
    //当数据源更新后,需要将所有显示的UITableViewCell和未显示可复用的UITableViewCell全部从父视图移除,
    //重新创建
    [[_cachedCells allValues] makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [_reusableCells makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [_reusableCells removeAllObjects];
    [_cachedCells removeAllObjects];

    _selectedRow = nil;
    _highlightedRow = nil;
    
    // 重新计算 section 相关的高度值,并缓存起来
    [self _updateSectionsCache];
    [self _setContentSize];
    
    _needsReload = NO;
}

其中 _updateSectionsCashe 方法是最重要的,该方法在数据源更新后至下一次数据源更新期间只能调用一次

- (void)_updateSectionsCache
{
    //该逆向源码只复用了 section 中的每个 UITableViewCell,并没有复用每个 section 的头视图和尾视图,
    //UIKit肯定是实现了所有视图的复用
    // remove all previous section header/footer views
    for (UITableViewSection *previousSectionRecord in _sections) {
        [previousSectionRecord.headerView removeFromSuperview];
        [previousSectionRecord.footerView removeFromSuperview];
    }
    
    // clear the previous cache
    [_sections removeAllObjects];
    
    //如果数据源为空,不做任何处理
    if (_dataSource) {
        // compute the heights/offsets of everything
        const CGFloat defaultRowHeight = _rowHeight ?: _UITableViewDefaultRowHeight;
        const NSInteger numberOfSections = [self numberOfSections];
        for (NSInteger section=0; section 0 && _delegateHas.viewForHeaderInSection)? [self.delegate tableView:self viewForHeaderInSection:section] : nil;
            sectionRecord.footerView = (sectionRecord.footerHeight > 0 && _delegateHas.viewForFooterInSection)? [self.delegate tableView:self viewForFooterInSection:section] : nil;

            // make a default section header view if there's a title for it and no overriding view
            if (!sectionRecord.headerView && sectionRecord.headerHeight > 0 && sectionRecord.headerTitle) {
                sectionRecord.headerView = [UITableViewSectionLabel sectionLabelWithTitle:sectionRecord.headerTitle];
            }
            
            // make a default section footer view if there's a title for it and no overriding view
            if (!sectionRecord.footerView && sectionRecord.footerHeight > 0 && sectionRecord.footerTitle) {
                sectionRecord.footerView = [UITableViewSectionLabel sectionLabelWithTitle:sectionRecord.footerTitle];
            }

            if (sectionRecord.headerView) {
                [self addSubview:sectionRecord.headerView];
            } else {
                sectionRecord.headerHeight = 0;
            }
            
            if (sectionRecord.footerView) {
                [self addSubview:sectionRecord.footerView];
            } else {
                sectionRecord.footerHeight = 0;
            }
            
            //section 中每个 row 的高度使用了数组指针来保存
            CGFloat *rowHeights = malloc(numberOfRowsInSection * sizeof(CGFloat));
            CGFloat totalRowsHeight = 0;
            
            //每行 row 的高度通过数据源实现的协议方法 heightForRowAtIndexPath: 返回,
            //若数据源没有实现该协议方法则使用默认的高度
            for (NSInteger row=0; row

上面方法主要是记录每个 Cell 的高度和整个 section 的高度,并把结果同过 UITableViewSection 对象缓存起来。

_layoutTableView 的源码实现如下
- (void)_layoutTableView
{
    //这里实现了 UITableViewCell 的复用
    const CGSize boundsSize = self.bounds.size;
    const CGFloat contentOffset = self.contentOffset.y;
    
    //由于 UITableView 继承于 UIScrollview,所以通过滚动偏移量得到当前可视的 bounds
    const CGRect visibleBounds = CGRectMake(0,contentOffset,boundsSize.width,boundsSize.height);
    CGFloat tableHeight = 0;
    
    //若有头部视图,则计算头部视图在父视图中的 frame
    if (_tableHeaderView) {
        CGRect tableHeaderFrame = _tableHeaderView.frame;
        tableHeaderFrame.origin = CGPointZero;
        tableHeaderFrame.size.width = boundsSize.width;
        _tableHeaderView.frame = tableHeaderFrame;
        tableHeight += tableHeaderFrame.size.height;
    }
    
    //_cashedCells 用于记录正在显示的 UITableViewCell 的引用
    //avaliableCells 用于记录当前正在显示但在滚动后不再显示的 UITableViewCell(该 Cell 可以复用)
    //在滚动后将该字典中的所有数据都添加到 _reusableCells 中,
    //记录下所有当前在可视但由于滚动而变得不再可视的 Cell 的引用
    NSMutableDictionary *availableCells = [_cachedCells mutableCopy];
    const NSInteger numberOfSections = [_sections count];
    [_cachedCells removeAllObjects];
    
    for (NSInteger section=0; section 0) {
                //在滚动时,如果向上滚动,除去顶部要隐藏的 Cell 和底部要显示的 Cell,中部的 Cell 都可以
                //根据 indexPath 直接获取
                    UITableViewCell *cell = [availableCells objectForKey:indexPath] ?: [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];
                    if (cell) {
                        [_cachedCells setObject:cell forKey:indexPath];
                        
                        //将当前仍留在可视区域的 Cell 从 availableCells 中移除,
                        //availableCells 中剩下的即为顶部已经隐藏的 Cell
                        //后面会将该 Cell 加入 _reusableCells 中以便下次取出进行复用。
                        [availableCells removeObjectForKey:indexPath];
                        
                        cell.highlighted = [_highlightedRow isEqual:indexPath];
                        cell.selected = [_selectedRow isEqual:indexPath];
                        cell.frame = rowRect;
                        cell.backgroundColor = self.backgroundColor;
                        [cell _setSeparatorStyle:_separatorStyle color:_separatorColor];
                        [self addSubview:cell];
                    }
                }
            }
        }
    }

    //把所有因滚动而不再可视的 Cell 从父视图移除并加入 _reusableCells 中,以便下次取出复用
    for (UITableViewCell *cell in [availableCells allValues]) {
        if (cell.reuseIdentifier) {
            [_reusableCells addObject:cell];
        } else {
            [cell removeFromSuperview];
        }
    }

    //把仍在可视区域的 Cell(但不应该在父视图上显示) 但已经被回收至可复用的 _reusableCells 中的 Cell从父视图移除
    NSArray* allCachedCells = [_cachedCells allValues];
    for (UITableViewCell *cell in _reusableCells) {
        if (CGRectIntersectsRect(cell.frame,visibleBounds) && ![allCachedCells containsObject: cell]) {
            [cell removeFromSuperview];
        }
    }
    
    if (_tableFooterView) {
        CGRect tableFooterFrame = _tableFooterView.frame;
        tableFooterFrame.origin = CGPointMake(0,tableHeight);
        tableFooterFrame.size.width = boundsSize.width;
        _tableFooterView.frame = tableFooterFrame;
    }
}

这里使用了三个容器 _cachedCells, availableCells, _reusableCells 完成了 Cell 的复用。

具体实现
1.在第一次设置了数据源调用该方法时,三个容器的内容都为空,
2.在调用完该方法后 _cachedCells 包含了当前所有可视 Cell 与其对应的indexPath 的键值对。
3.availableCells 与 _reusableCells 仍然为空。只有在滚动起来后 _reusableCells 中才会出现多余的未显示可复用的 Cell。

刚创建 UITableView 时的状态如下图(红色为屏幕内容即可视区域,蓝色为超出屏幕的内容,即不可视区域):

UITableViewCell的重用机制原理_第2张图片
WechatIMG27.jpeg

如图,当前 _cachedCells 的元素为当前可视的所有 Cell 与其对应的 indexPath 的键值对。

向上滚动一个 Cell 的过程中,由于 availableCells 为 _cachedCells 的拷贝,所以可根据 indexPath 直接取到对应的 Cell,这时从底部滚上来的第7行,由于之前的 _reusableCells 为空,所以该 Cell 是直接创建的而并非复用的,由于顶部 Cell 滚动出了可视区域,所以被加入了 _reusableCells 中以便后续滚动复用。滚动完一行后的状态变为了 _cachedCells 包含第 2 行到第 7 行 Cell 的引用,_reusableCells 包含第一行 之前滚动出可视区域的第一行 Cell 的引用。

UITableViewCell的重用机制原理_第3张图片
WechatIMG29.jpeg

当向上滚动两个 Cell 的过程中,同理第 3 行到第 7 行的 Cell 可以通过对应的 indexPath 从 _cachedCells 中获取。这时 _reusableCells 中正好有一个可以复用的 Cell 用来从底部滚动上来的第 8 行。滚动出顶部的第 2 行 Cell 被加入 _reusableCells 中。


UITableViewCell的重用机制原理_第4张图片
WechatIMG31.jpeg

参考资料:
https://www.jianshu.com/p/5b0e1ca9b673

你可能感兴趣的:(UITableViewCell的重用机制原理)