什么是UITableViewCell?
UITableView可以说是UIKit中最重要的一个组件,在主流App中基本都会出现(聊天列表,个人设置等)。它用来展示数据列表,还可以灵活使用进行页面的布局。UITableView继承自UIScrollView,可上下滑动,可以作为跟视图也可以作为子视图组件。而UITableViewCell,是UITableView中的数据载体,也就是我们见到表格中的一个个小单元。
对象池模式
对象池模式是iOS设计模式中的一种创建型模式,通过回收不再使用的对象,避免创建和销毁对象时代价高昂的获取和释放资源的过程。
对象池模式定义一个已初始化对象的集合为“池”,并能随时从”池”中拿出对象进行使用,以此避免对象创建销毁带来的时间空间开销。一个客户端的池会请求池中的对象,然后在返回的对象上执行操作。当这个客户端结束时,将对象返回到池中而不是销毁它。这个操作可以手动执行,也可以自动执行。对象池设计模式的本质是当需要时从池中寻找我们要的对象,没有可以使用的对象再进行新建。
对象池主要是用来提升性能,在某些情况下,对象池对性能有极大的帮助。但是还有一点需要注意,对象池会增加对象生命周期的复杂度,这是因为从对象池获取的对象和返还给对象池的对象都没有真正的创建或者销毁。
UITableViewCell的重用原理
UITableView 首先加载可视区内的 UITableViewCell(具体个数要根据每个 cell 的高度而定)。
然后,当我们往上滑动时,需要一个新的 cell 放置在 UITableView 的下方。此时,我们不创建新的 cell ,而是先从 UITableView 的复用池里去获取,该池存放了已经生成的、能够复用的同类 cell 。如果复用池为空,才会主动创建一个新的 cell 。
复用池的 cell 是这样被添加至该池的:当我们向上滑动视图时(向下滑动同理),位于最顶部的 cell 会相应地往上运动,直至消失在屏幕上。当其消失在视图中时,会被添加至当前 UITableView 的复用池。
因此,在渲染海量数据的列表时,并不需要创建很多 cell 导致内存爆炸,这归功于 UITableView 的复用机制。
注册和非注册cell
① 非注册cell dequeueReusableCellWithIdentifier:
使用方法:
[tableView dequeueReusableCellWithIdentifier:@"myCell"]
官方文档:
翻译:
出于性能原因,当UITableview的DataSource将cell按行分配给tableView:cellForRowAtIndexPath:
方法时,通常应该重用UITableViewCell
对象。UITableView
对象维护一个当前复用的cell队列(或链表)。当要求为UITableview提供新cell时,从DataSource对象调用此方法。如果现有cell可用,此方法将其出列,或者使用之前注册的类或nib文件创建新cell。如果没有可重用的cell,并且您没有注册类或nib文件,则此方法返回nil。
如果为指定的Identifier注册了一个Class,并且必须创建一个新的cell,则此方法通过调用其initWithStyle:reuseIdentifier:
方法来初始化单元格。对于基于nib的单元,此方法从提供的nib文件加载cell。如果现有cell可供重用,此方法将调用该cell的prepareForReuse
方法。
重点:
使用UITableView时应该利用重用机制,UITableView对象维护着一个cell复用池,其中cell以队列或链表的形式存储。
当需要新cell时使用此方法,有可用cell就从复用池中拿出该cell,没有可用cell就返回nil。
因此使用此方法 需要手动判断返回的cell是否为nil,若为nil需要手动创建新cell。
② 注册cell dequeueReusableCellWithIdentifier:forIndexPath:
使用方法:
[tableView dequeueReusableCellWithIdentifier:@"myCell" forIndexPath:indexPath]
官方文档:
翻译:
仅从UITableview的DataSource对象的tableView:cellForRowAtIndexPath:
调用此方法。此方法返回指定类型的现有cell(如果有),或者使用前面提供的Class或storyboard创建并返回新cell。不要在DataSource的tableView:cellForRowAtIndexPath:
之外调用此方法。如果您需要在其他时间创建cell,请改为调用dequeueReusableCellWithIdentifier:
方法。
很重要:必须在storyboard文件中指定具有匹配Identifier的cell。也可以使用registerNib:forcellReuseIdentier:
或registerClass:forCellReuseidentier:
方法注册class或nib文件,但必须在调用此方法之前注册。
从storyboard或nib文件创建新cell时,此方法加载cell对象,并使用其initWithCoder:
方法对其进行初始化。从注册的类创建cell时,此方法创建cell并通过调用其initWithStyle:reuseIdentifier:
方法对其进行初始化。对于基于nib的cell,此方法从提供的nib文件加载cell对象。如果现有cell可供重用,此方法将调用该cell的prepareForReuse
方法。
重点:
如果你为一个Identifier注册了一个类,那么调用此方法,且要在使用方法之前创建。
该方法会自动为你创建一个该类的cell并初始化,不需要手动判断nil并创建了。
总结
两个方法区别在于:有没有为某一 Identifier 注册一个 Class。
如果一个 Identifier 绑定了一个 Class,当标识符为 Identifier 的 cell 复用池中没有可复用的 cell 时,系统会自动创建一个绑定的 Class 类的 cell 。
如果没有绑定 Class ,那么我们要手动判断复用池中是否有可复用的 cell:有就从池中拿出使用,没有则手动新建一个 cell 。
cell中的reuseIdentifier
官方文档:
翻译:
这个复用标识关联UITableviewCell对象,在tableview代理中创建带有“标识符”来复用cell对象,而且作为tableview多行显示的原型(性能的原因)。通过initWithFrame:reuseIdentifier:
来指定一个cell对象,而且在调用这个方法之后就不能修改了。在UITableView对象维护一个当前复用的cell队列(或链表),并且每一个cell都拥有自己的标识符,并这些cell能在代理对象的dequeueReusableCellWithIdentifier:的方法中获取。
重点:
在tableview新建的时候,会新建一个复用池pool,这个pool可能是一个队列或链表,保存着当前待复用的的cell。pool中的对象的复用标识符就是reuseIdentifier,标识着不同种类的cell。所以调用dequeueReusableCellWithIdentifier:
方法获取cell时,从pool中取出来的cell都是之前在tableview中展示的原型。
复用问题的最好解决方案:无论cell之前有什么状态,在prepareForReuse
方法中全部重置为nil。
prepareForReuse
官方文档:
翻译:
如果UITableViewCell对象具有重用标识符,则表视图会在从UITableView方法dequeueReusableCellWithIdentifier:
返回对象之前调用此方法。为了避免潜在的性能问题,应该只重置与内容无关的单元格属性,例如alpha、编辑和选择状态。重复使用单元格时,tableView:cellForRowAtIndexPath:
中表视图的委托应始终重置所有内容。
如果单元格对象没有关联的重用标识符,或者如果您使用reconfigureRowsAtIndexPaths:
更新现有单元格的内容,则表视图不会调用此方法。
如果重写此方法,则必须确保调用超类实现。
重点:
实现此方法需要调用[super prepareForReuse]
!
当用户拖动tableView上拉,顶部的cell消失时,如果该cell有重用标识符,则它会调用dequeueReusableCellWithIdentifier:
方法,并唤醒prepareForReuse方法。其目的是让用户重置一些参数,比如 alpha, editing, 或 selection state。并且我们只能设置cell原本的属性,而如果cell是我们自定义的,我们无法重置我们自己添加的属性。
遇到的问题
在打印cell地址时,开始挺正常的:
当前状态下上有第1行,下有第10行,第0行和第11行是已经创建好的待用cell。因此此时下滑tableview,第0行消失、第12行出现时,第0行cell进入复用池,然后被第12行使用。继续下滑tableview,第13行复用第1行,第14行复用第2行…
但当我改变下滑速度时,出现了以下的情况(均是在重启进入视图,第一次下滑产生的):
- 提前复用
- 延后跳过第二个再复用
- 提前无序复用
- 延后无序复用
自己推测的原因:
- 参考博客对于dequeueReusableCellWithIdentifier:的理解,有以下理解:
表视图里可能有不同类型的单元,复用是在相同类型单元里发生的。至于队列里有多少单元,这个系统自己控制,如果内存紧张,就会动态释放掉一些,当条件改善的时候,就会重新获取这些单元便于复用。一个极端的情况,可复用的单元没有了,那么又会调用initWithStyle:reuseIdentifier:来产生新的单元。理解表视图的机制,不能把思维定在严格的队列机制里,它有些动态的因素需要考虑。
如果在资源紧缺的时候,这个池会自动清理多余的UITableViewCell对象,则可能无法返回对象,但如果资源丰富,则会保存一些UITableViewCell对象,在需要调用的时候迅速的返回,而不用创建。
- cell在进入复用池到从复用池中取出的生命周期内,有某些因素让它并不能成为一个合格的可被复用的cell;或者在需要从复用池中拿取cell时,该cell被复用的时机还不到(生命周期未走完)?
- 官方文档中提到复用池可能是一个队列或链表(a queue or a list),或许因为混合数据结构的布局导致复用顺序不完全是队列模式?
总而言之,由于UITableViewCell没有开源,其重用机制只能从官方提供的几个方法中分析,其中详细的实现细节还有待挖掘。