UITableView实践(一):实现原理

通过查看UITableView的源码来查看实现原理

Posted by Ted on April 25, 2018

一、综述

UITableView应该是iOS中最经典也是最常见的一个控件了。使用很普遍

UITableView *tableView = [[UITableView alloc] initWithFrame:frame style:UITableViewStyleGrouped];   
tableView.delegate = self;
tableView.dataSource = self;
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"MyCellReuseIdentifier"];
[self.view addSubview:tableView];

#pragma mark - UITableViewDelegate
// 行高
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    return 50.0f;
}

#pragma mark - UITableViewDataSource
// Cell复用
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellReuseIdentifier"];
    return cell;
}

// 行数
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return self.dataArray.count;
}

Chameleon项目

If you’re an iOS developer, you’re already familiar with UIKit, the framework used to create apps for the iPhone, iPod and iPad. Chameleon is a drop in replacement for UIKit that runs on Mac OS X. In many cases, your iOS code doesn’t need to change at all in order to run on a Mac.

This new framework is a clean room implementation of the work done by Apple for iOS. The only thing Chameleon has in common with UIKit are the public class and method names. The code is based on Apple’s documentation and does not use any private APIs or other techniques disallowed by the Mac App Store.

我们知道在iOS上开发的视图使用UIKit,Mac OS则没有。Chameleon项目就是将UIKit的代码也可以运行在macOS上。我们可以通过Chameleon项目的源码来一探究竟,UITableView是如何实现的。

二、初始化

1、init

initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle是最常用的初始化方式,那么Chameleon项目是怎么做的呢?

- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)theStyle
{
    if ((self=[super initWithFrame:frame])) {
        _style = theStyle;
       // 已生成Cell的缓存
        _cachedCells = [[NSMutableDictionary alloc] init];
       // Secitons的缓存
        _sections = [[NSMutableArray alloc] init];
       // 复用的Cells
        _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];
        }
        
      	// setNeedsLayout
        [self _setNeedsReload];
    }
    return self;
}

- (void)_setNeedsReload
{
    _needsReload = YES;
    [self setNeedsLayout];
}

初始化大概分为三部分:

  • 用来装载实例的容器初始化
  • 基本属性的默认值赋值
  • 标记需要setNeedsLayout

我们知道,iOS的交互流程是这样的

img

所以,接下来就是layoutSubviews

2、layoutSubviews

- (void)layoutSubviews
{
    _backgroundView.frame = self.bounds;
    [self _reloadDataIfNeeded];
    [self _layoutTableView];
    [super layoutSubviews];
}

img

reloadData

- (void)reloadData
{
    // clear the caches and remove the cells since everything is going to change
    [[_cachedCells allValues] makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [_reusableCells makeObjectsPerformSelector:@selector(removeFromSuperview)];
    [_reusableCells removeAllObjects];
    [_cachedCells removeAllObjects];

    // clear prior selection
    _selectedRow = nil;
    _highlightedRow = nil;
    
    // trigger the section cache to be repopulated
    [self _updateSectionsCache];
    [self _setContentSize];
    
    _needsReload = NO;
}

因为需要重新加载数据,所以将缓存以及复用的Cell都清空掉,SectionsCache也更新掉

layoutTableView

- (void)_layoutTableView
{
    // lays out headers and rows that are visible at the time. this should also do cell
    // dequeuing and keep a list of all existing cells that are visible and those
    // that exist but are not visible and are reusable
    // if there's no section cache, no rows will be laid out but the header/footer will (if any).
    
    const CGSize boundsSize = self.bounds.size;
    const CGFloat contentOffset = self.contentOffset.y;
    const CGRect visibleBounds = CGRectMake(0,contentOffset,boundsSize.width,boundsSize.height);
    CGFloat tableHeight = 0;
    
    if (_tableHeaderView) {
        CGRect tableHeaderFrame = _tableHeaderView.frame;
        tableHeaderFrame.origin = CGPointZero;
        tableHeaderFrame.size.width = boundsSize.width;
        _tableHeaderView.frame = tableHeaderFrame;
        tableHeight += tableHeaderFrame.size.height;
    }
    
    // layout sections and rows
    NSMutableDictionary *availableCells = [_cachedCells mutableCopy];
    const NSInteger numberOfSections = [_sections count];
    [_cachedCells removeAllObjects];
    
    for (NSInteger section=0; section<numberOfSections; section++) {
        CGRect sectionRect = [self rectForSection:section];
        tableHeight += sectionRect.size.height;
        if (CGRectIntersectsRect(sectionRect, visibleBounds)) {
            const CGRect headerRect = [self rectForHeaderInSection:section];
            const CGRect footerRect = [self rectForFooterInSection:section];
            UITableViewSection *sectionRecord = [_sections objectAtIndex:section];
            const NSInteger numberOfRows = sectionRecord.numberOfRows;
            
            if (sectionRecord.headerView) {
                sectionRecord.headerView.frame = headerRect;
            }
            
            if (sectionRecord.footerView) {
                sectionRecord.footerView.frame = footerRect;
            }
            
            for (NSInteger row=0; row<numberOfRows; row++) {
                NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:section];
                CGRect rowRect = [self rectForRowAtIndexPath:indexPath];
                if (CGRectIntersectsRect(rowRect,visibleBounds) && rowRect.size.height > 0) {
                    UITableViewCell *cell = [availableCells objectForKey:indexPath] ?: [self.dataSource tableView:self cellForRowAtIndexPath:indexPath];
                    if (cell) {
                        [_cachedCells setObject:cell forKey:indexPath];
                        [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];
                    }
                }
            }
        }
    }
    
    // remove old cells, but save off any that might be reusable
    for (UITableViewCell *cell in [availableCells allValues]) {
        if (cell.reuseIdentifier) {
            [_reusableCells addObject:cell];
        } else {
            [cell removeFromSuperview];
        }
    }
    
    // non-reusable cells should end up dealloced after at this point, but reusable ones live on in _reusableCells.
    
    // now make sure that all available (but unused) reusable cells aren't on screen in the visible area.
    // this is done becaue when resizing a table view by shrinking it's height in an animation, it looks better. The reason is that
    // when an animation happens, it sets the frame to the new (shorter) size and thus recalcuates which cells should be visible.
    // If it removed all non-visible cells, then the cells on the bottom of the table view would disappear immediately but before
    // the frame of the table view has actually animated down to the new, shorter size. So the animation is jumpy/ugly because
    // the cells suddenly disappear instead of seemingly animating down and out of view like they should. This tries to leave them
    // on screen as long as possible, but only if they don't get in the way.
    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;
    }
}

这一步操作主要是将已经初始化的Cells重新布局,以及其他布局如HeadView,FootView的设置

三、Cell复用

cell在初始化的时候会绑定一个Identifier用以以后复用

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    if ((self=[self initWithFrame:CGRectMake(0,0,320,_UITableViewDefaultRowHeight)])) {
        _style = style;
        _reuseIdentifier = [reuseIdentifier copy];
    }
    return self;
}

如上文,在UITableView初始化的时候,会初始化一个空的集合用来装载可复用的Cell。这是一个可变的集合

_reusableCells = [[NSMutableSet alloc] init];

在UITableView重载数据reloadData时,会将里面的cell清空

[_reusableCells makeObjectsPerformSelector:@selector(removeFromSuperview)];
[_reusableCells removeAllObjects];

在TableView滑动或者做了其他更新布局layoutTableView,将绑定了Identifier的cell装入集合以便复用

    // remove old cells, but save off any that might be reusable
    for (UITableViewCell *cell in [availableCells allValues]) {
        if (cell.reuseIdentifier) {
            [_reusableCells addObject:cell];
        } else {
            [cell removeFromSuperview];
        }
    }

下面是UITableview数据源协议的复用代码

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellReuseIdentifier"];
    return cell;
}

根据Identifier将cell从集合中取出

- (UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier
{
    for (UITableViewCell *cell in _reusableCells) {
        if ([cell.reuseIdentifier isEqualToString:identifier]) {
            UITableViewCell *strongCell = cell;
            
            // the above strongCell reference seems totally unnecessary, but without it ARC apparently
            // ends up releasing the cell when it's removed on this line even though we're referencing it
            // later in this method by way of the cell variable. I do not like this.
            [_reusableCells removeObject:cell];
            return strongCell;
        }
    }
    
    return nil;
}

Cell复用的三个容器

  • NSMutableDictionary 类型 _cachedCells:用来存储当前屏幕上所有 Cell 与其对应的 indexPath。以键值对的关系进行存储。
  • NSMutableDictionary 类型 availableCells:当列表发生滑动的时候,部分 Cell 从屏幕移出,这个容器会对 _cachedCells 进行拷贝,然后将屏幕上此时的 Cell 全部去除。即最终取出所有退出屏幕的 Cell。
  • NSMutableSet 类型 _reusableCells:用来收集曾经出现过此时未出现在屏幕上的 Cell。当再出滑入主屏幕时,则直接使用其中的对象根据 CGRectIntersectsRect Rect 碰撞试验进行复用。

img

当到状态 ② 的时候,我们发现 _reusableCells 容器中,已经出现了状态 ① 中已经退出屏幕的 Cell 0。而当我们重新将 Cell 0 滑入界面的时候,在系统 addView 渲染阶段,会直接将 _reusableCells 中的 Cell 0 立即取出进行渲染,从而代替创建新的实例再进行渲染,简化了时间与性能上的开销。

参考:

https://github.com/BigZaphod/Chameleon

http://www.desgard.com/TableView-Reuse/