UITableView的复用机制:
复用机制大体是这样:
UITableView
首先加载一屏幕(假设UITableView的大小是整个屏幕的大小)所需要的UITableViewCell
,具体个数要根据每个cell
的高度而定,总之肯定要铺满整个屏幕,更准确说当前加载的cell
的高度要大于屏幕高度。然后你往上滑动,想要查看更多的内容,那么肯定需要一个新的cell放在已经存在内容的下边。这时候先不去生成,而是先去UITableView
自己的一个资源池里去获取。这个资源池里放了已经生成的而且能用的cell
。如果资源池是空的话才会主动生成一个新的cell。那么这个资源池里的cell
又来自哪里呢?当你滑动时视图是,位于最顶部的cell
会相应的往上滑动,直到它彻底消失在屏幕上,消失的cell
去了哪里呢?你肯定想到了,是的,它被UITableView
放到资源池里了。其他cell
也是这样,只要一滑出屏幕就放入资源池。这样,有进有出,总共需要大约一屏幕多一两个的cell
就够了。相对于1000来说节省的资源就是指数级啊,完美解决了性能问题。
复用机制是一个很好的机制,但是不正确的使用却会给我们的程序带来很多问题。下面拿 UITableView 复用 Cell 来举例:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"UITableViewCell";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
// 偶数行 Cell 的 textLabel 的文字颜色为红色。
if (indexPath.row % 2 == 0) {
[cell.textLabel setTextColor:[UIColor redColor]];
}
}
cell.textLabel.text = @"Title";
// 偶数行 Cell 的 detailTextLabel 显示 Detail 文字。
if (indexPath.row % 2 == 0) {
cell.detailTextLabel.text = @"Detail";
}
return cell;
}
我们本来是希望只有偶数行的 textLabel
的文字颜色为红色,并且显示 Detai
l 文字,但是当你滑动 TableView
的时候发现不对了,有些奇数行的 textLabel
的文字颜色为红色,而且还显示了 Detail
文字,很奇怪。其实造成这个问题的原因就是「复用」,当一个 Cell
被拿来复用时,它所有被设置的属性(包括样式和内容)都会被拿来复用,如果刚好某一个的 Cell
你没有显式地设置它的属性,那么它这些属性就直接复用别的 Cell
的了。就如上面的代码中,我们并没有显式地设置奇数行的 Cell
的 textLabel
的文字颜色以及 detailTextLabel
的文字,那么它就有可能复用别的 Cell
的这些属性了。此外,还有个问题,对偶数行 Cell
的 textLabel
的文字颜色的设置放在了初始一个 Cell
的 if 代码块里,这样在复用的时候,逻辑走不到这里去,那么也会出现复用问题。所以,上面的代码需要改成这样:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"UITableViewCell";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
}
cell.textLabel.text = @"Title";
if (indexPath.row % 2 == 0) {
[cell.textLabel setTextColor:[UIColor redColor]];
cell.detailTextLabel.text = @"Detail";
} else {
[cell.textLabel setTextColor:[UIColor blackColor]];
cell.detailTextLabel.text = nil;
}
return cell;
}
总之在复用的时候需要记住:
设置
Cell
的存在差异性的那些属性(包括样式和内容)时,有了 if 最好就要有 else
,要显式的覆盖所有可能性。
设置 Cell 的存在差异性的那些属性时,代码要放在初始化代码块的外部。
上面的代码中,我们展示了- [UITableView dequeueReusableCellWithIdentifier:];
的用法。下面看看另几个 API 的用法:
@property (weak, nonatomic) IBOutlet UITableView *myTableView;
- (void)viewDidLoad {
[super viewDidLoad];
// Setup table view.
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
[self.myTableView registerClass:[MyTableViewCell class] forCellReuseIdentifier:@"MyTableViewCell"];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"MyTableViewCell";
UITableViewCell *cell = nil;
cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
cell.textLabel.text = @"Title";
if (indexPath.row % 2 == 0) {
[cell.textLabel setTextColor:[UIColor redColor]];
} else {
[cell.textLabel setTextColor:[UIColor blackColor]];
}
return cell;
}
我们可以看到:
- [UITableView dequeueReusableCellWithIdentifier:forIndexPath:]
;
必须搭配- [UITableView registerClass:forCellReuseIdentifier:];
或者- [UITableView registerNib:forCellReuseIdentifier:];
使用。当有可重用的Cell
时,前者直接拿来复用,并调用- [UITableViewCell prepareForReuse]
方法;当没有时,前者会调用Identifier
对应的那个注册的UITableViewCell
类的- [UITableViewCell initWithStyle:reuseIdentifier:]
方法来初始化一个,这里省去了你自己初始化的步骤。当你自定义了一个UITableViewCell
的子类时,你可以这样来用。
UITableView的优化:
优化 UITableView:
UITableView
是我们最常用来展示数据的控件之一,并且通常需要UITableView
在承载较多内容的同时保证交互的流畅性,对UITableView
的性能优化是我们开发应用程序必备的技巧之一。
在UITableView的复用机制
一节,已经提到了UITableView
的复用机制。现在就来看看UITableView
在复用时最主要的两个回调方法:- [UITableView tableView:cellForRowAtIndexPath:]
和- [UITableView tableView:heightForRowAtIndexPath:]
。UITableView
是继承自UIScrollView
,所以在渲染的过程中它会先确定它的contentSize
及每个Cell
的位置,然后才会把复用的Cell
放置到对应的位置。比如现在一共有 50 个Cell
,当前屏幕上显示 5 个。那么在第一次创建或reloadData
的时候,UITableView
会先调用 50 次- [UITableView tableView:heightForRowAtIndexPath:]
确定contentSize
及每个Cell
的位置,然后再调用 5 次- [UITableView tableView:cellForRowAtIndexPath:]
来渲染当前屏幕的 Cell。在滑动屏幕的时候,每当一个Cell
进入屏幕时,都需要调用一次- [UITableView tableView:cellForRowAtIndexPath:]
和- [UITableView tableView:heightForRowAtIndexPath:]
方法。
了解了 UITableView
的复用机制以及相关回调方法的调用次序,这里就对 UITableView
的性能优化方案做一个总结:
- 通过正确的设置
reuseIdentifier
来重用 Cell。 - 尽量减少不必要的透明
View
。 - 尽量避免渐变效果、图片拉伸和离屏渲染。
- 当不同的行的高度不一样时,尽量缓存它们的高度值。
- 如果
Cell
展示的内容来自网络,确保用异步加载的方式来获取数据,并且缓存服务器的response
。 - 使用
shadowPath
来设置阴影效果。 - 尽量减少
subview
的数量,对于subview
较多并且样式多变的Cell
,可以考虑用异步绘制或重写drawRect
。
*尽量优化- [UITableView tableView:cellForRowAtIndexPath:]
方法中的处理逻辑,如果确实要做一些处理,可以考虑做一次,缓存结果。 - 选择合适的数据结构来承载数据,不同的数据结构对不同操作的开销是存在差异的。
- 对于
rowHeight
、sectionFooterHeight
、sectionHeaderHeight
尽量使用常量。