前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UITableView实践(一):实现原理

UITableView实践(一):实现原理

作者头像
Helloted
发布2022-06-07 14:36:44
8850
发布2022-06-07 14:36:44
举报
文章被收录于专栏:Helloted

一、综述

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

代码语言:javascript
复制
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项目是怎么做的呢?

代码语言:javascript
复制
- (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
img

所以,接下来就是layoutSubviews

2、layoutSubviews
代码语言:javascript
复制
- (void)layoutSubviews
{
    _backgroundView.frame = self.bounds;
    [self _reloadDataIfNeeded];
    [self _layoutTableView];
    [super layoutSubviews];
}
img
img

reloadData

代码语言:javascript
复制
- (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

代码语言:javascript
复制
- (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用以以后复用

代码语言:javascript
复制
- (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。这是一个可变的集合

代码语言:javascript
复制
_reusableCells = [[NSMutableSet alloc] init];

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

代码语言:javascript
复制
[_reusableCells makeObjectsPerformSelector:@selector(removeFromSuperview)];
[_reusableCells removeAllObjects];

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

代码语言:javascript
复制
    // 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数据源协议的复用代码

代码语言:javascript
复制
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCellReuseIdentifier"];
    return cell;
}

根据Identifier将cell从集合中取出

代码语言:javascript
复制
- (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
img

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

参考:

https://github.com/BigZaphod/Chameleon

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

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、综述
    • Chameleon项目
    • 二、初始化
      • 1、init
        • 2、layoutSubviews
        • 三、Cell复用
          • Cell复用的三个容器
          相关产品与服务
          容器服务
          腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档