前言
去年11月自己写学习过这个部分的内容,但是不仅有错误,自己理解也不好,现在想要对于tableView的复用机制进行详细的学习,中间遇到了不少问题,索性整理了一篇博客
对象池设计模式
UITableViewCell的复用机制并非什么黑魔法,而是设计模式中的一种创造型设计模式–对象池设计模式
对象池模式是一种创造型设计模式,使用一个已初始化对象的集合,并能随时从”池”中拿出来使用.以此避免对象的创建销毁所带来的开销.一个客户端的池会请求池中的对象,然后在返回的对象上执行操作.当这个客户端结束时,代替原本销毁操作,取而代之的是将对象返回到池中.这个操作可以手动执行,也可以自动执行。也就是对象池设计模式的本质是我们获取一个‘新的’对象,而不管它真的是一个新的对象还是循环使用的对象。
对象池主要是用来提升性能,在某些情况下,对象池对性能有极大的帮助。但是还有一点需要注意,对象池会增加对象生命周期的复杂度,这是因为从对象池获取的对象和返还给对象池的对象都没有真正的创建或者销毁。
cell的注册与不注册的情况的区别
如果是注册的情况
[tableView dequeueReusableCellWithIdentifier:@"cell" forIndexPath:indexPath]
复制代码
非注册:
[tableView dequeueReusableCellWithIdentifier:@"cell"]
复制代码
对于这两种方法的解释
注册带forIndexPath:
newer dequeue method guarantees a cell is returned and resized properly, assuming identifier is registered
更新的出队列方法保证返回一个单元格并正确调整大小(假设已注册标识符)
复制代码
非注册不带forIndexPath:
Used by the delegate to acquire an already allocated cell, in lieu of allocating a new one
委托用于获取已分配的单元格,而不是分配一个新单元格。
复制代码
我们点击打开Developer Documentation中的官方文档:
注册带forIndexPath:
非注册不带:
很明显 注册的方法会自动帮我们完成分配新单元格这个工作 而非注册则需要我们自己手动创建新的单元格
总结
所以,注册不注册其实指:【有没有为某一identifier 注册一个Class】
或者理解为:有没有把一个identifier和一个Class相互绑定。
如果发生绑定,当标识符为identifier 的Cell队列中没有可复用的cell时,系统会自动创建一个绑定的Class类型的cell。
如果没有绑定Class,那么我们要手动判定是否无可复用的cell,并手动新建一个cell。
一句话总结:注册后就不用管cell是否需要新建了。
所以各有各的好,如果样式单一,使用注册就可以很方便的解决的话,注册就可以达到效果。不过如果想要查询是否复用,或者详细的复用方面的问题,判空,可以查看是否重新建立了,来判断更加细致,不注册可能会更好。但大部分情况我们都使用注册的情况。具体使用还是应该分情况而定。
cell的复用问题
通常情况下理解的概念:
通俗地说一下。假设我们的tableview中有10个cell,窗口只容得下前4个,每个cell都是一样的,复用id也一样。从初始位置开始慢镜头,把cell前上滑动一点点,此时第一个cell的一部分消失了,第五个cell露出了一部分,这时第一个cell并没有进入到复用池,池子是空的,第五个cell自然也就不能在复用池中找到可复用的cell,第五个cell执行了如下代码:(先假定我们使用不注册的方法通过判空来详细了解复用池)
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cacheCellId];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellId];
}
复制代码
所以一个cell创建时候会先走if里面的
当第五个cell完全显示出来,第一个cell也已经完全退出了窗口,这时第一个cell被放入到复用池。我们继续向上滑动,第六个cell将显示出来,它也要走上面的那段代码,但是它不会进入到if里面去,因为第一个cell已经在复用池中了,第六个cell可以复用第一个cell,而不需重新创建对象。
这就是tableView的复用
遇到的问题:
但是当我自己在实验的时候发现了一些问题
通过打印cell的地址,我发现无论注册与不注册,系统第一步都是先创建所有的cell。
就比如说假设我们的tableview中有10个cell,窗口只容得下前6个,每个cell都是一样的,复用id也一样。从初始位置开始慢镜头,把cell前上滑动一点点,此时第一个cell的一部分消失了,第7个cell露出了一部分,这时第一个cell并没有进入到复用池,池子是空的,第7个cell自然也就不能在复用池中找到可复用的cell,按照之前的理解,其会重新创建 如果是未注册的话就会走到判空这一步
但是令人惊奇的是 无论注册还是不注册 后续都不会走这一步,第7个占用了最后一个的空间(地址一样)第8个占用了倒数第二个的空间,之后第一个和第二个完全消失在视野里,第8个和第9个才会占用第一个和第二个空间。
对比一下第一张图
第七个第八个会占用最后两个的空间,而不是之前的那种,走一遍判空,然后重新申请空间。判空也仅仅在第一次运行时才会进行。
结论
由于tableViewCell没有开源,我想总结cell复用从头到尾的规律很难 但是自己从中学到了不少东西
- 当屏幕展示不全所有的cell,且所有cell的总数小于20时(两个条件缺一不可)那么其就会创建全部的cell,并且把未显示到的添加到池中(这句话有问题)。 但这里有问题,我不清楚为什么第七个第八个会占用最后两个空间,发现是与cell的行高有关,将高度改为180,那么第七个会占用最后一个空间,第八个占用倒数第二个,与前面刚好相反(高度为150时,第7个占用倒数第二个,第8个占用最后一个)。如果将高度改为200,会第7个仅仅占用最后一个空间,当第8个出来时,第一个已经添加到缓存池中。如果按照其他博客,缓存池中包含一个队列的话,先进先出,那第7个自己空间和第8个自己空间应该是先进去的。如果是栈,也没法解释为什么高度180与高度150先后的顺序刚好相反。不过不考虑那么多,自己还是学到了不少东西的。
- 如果能显示全,无论总数,那么显示多少创建多少
- 如果显示不全,但是总数大于20,那么其只能创建前20个
在这种情况下才会遇到走if里面的情况 其只创建了前20个,第21个出来的时候,第一个还没有完全消失,这个时候就回到一开始熟悉的地方了。但是也仅仅是第一次的时候,如果第二次再下拉就不会出现了。
我十分期待有哪位大佬能帮我解决我上面没有总结完的信息 不过我也同样学到了很多东西 希望以后自己搞懂了可以再来修改这篇博客
一些小tips
- 写cell时对于标识符我们进行static静态局部变量进行声明,在整个程序的运行过程中,局部变量只占了一份内存并且cell名称要写完整才是正确的代码规范。
- 这种不使用tableView的复用这种写法简直睿智 强烈建议别这样写
如果因为复用问题某些控件重复使用
那么