UITableView优化思路与复用
单元格复用问题 demo 点击此处下载
UITableView最核心的思想就是UITableViewCell的重用机制。简单的理解就是:UITableView一开始只会创建屏幕上要显示的Cell,每当Cell滑出屏幕时,就会放入到一个叫“重用池”的里面,滑动中当要显示某一位置的Cell时,会先去“重用池”取,如果有,就直接拿来显示;如果没有,才会创建。这样做的好处就是极大的减少了内存的开销,不需要每个都创建了。那么我们首先对复用进行一个探究(以下探究方法是当初刚学习OC时候网上受到的启发,自己按照这个思路尝试)。
// 方法1
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"qwer"];
if (!cell) {
cell = [[UITableViewCell alloc]initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"qwer"];
}
// 方法2(xib),在GLImageCell.xib文件中identifier中输入GLImageCell,在GLImageCell.m中复写initWithStyle reuseIdentifier方法 复写内容见demo(loadNibNamed的方法)
GLImageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"GLImageCell"];
if (nil == cell) {
cell = [[GLImageCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"GLImageCell"];
}
// 方法3
Method3Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"method3"];
if (nil == cell) {
cell = [[[NSBundle mainBundle]loadNibNamed:@"Method3Cell" owner:self options:nil]lastObject];
cell.selectionStyle=UITableViewCellSelectionStyleNone;
[tableView registerNib:[UINib nibWithNibName:@"Method3Cell" bundle:[NSBundle mainBundle]] forCellReuseIdentifier:@"method3"];
}
return cell;
// 方法4
1.在viewdidload方法里面
UINib *firstNib = [UINib nibWithNibName:@"Method3Cell" bundle:nil];
[_tableView registerNib:firstNib forCellReuseIdentifier:@"Method3"];
2.在 dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath方法中
Method3Cell *cell = [tableView dequeueReusableCellWithIdentifier:@"Method3" forIndexPath:indexPath];
//在以下方法中这么写
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static int myCount = 0; //static变量只是在编译时候进行初始化
TableViewCell *cell;
cell = [tableView dequeueReusableCellWithIdentifier:@"test"];
if (nil == cell) {
// 此处调用方法1,2,3,4进行测试
myCount ++;
NSLog(@"count :%d ",myCount);
}
return cell;
}
测试输出结果如下
//方法1 输出为1,2,3,4,5,6,7,8,9
//方法2 输出为1,2,3,4,5,6,7,8,9
//方法3 输出为1
//方法4 输出为 (就是不输出)
为什么会这样?首先我们大胆猜测一下是不是这样的:方法1和方法2是因为一开始都没有对应这个Identifier的cell,所以每次都进入if(cell==nil)方法里面。方法3是因为第一次没有找到对应Identifier的cell,于是进入if(cell==nil)里面,然后调用的注册方法,所以只输出一个数。方法4是因为在最开始就已经注册过,所以不会进入if里面,所以就没有输出。
我们先来看这句 dequeueReusableCellWithIdentifier 是什么意思,我们查看官方文档
网上翻译一下大致意思:
出于性能的原因,一个表视图的数据源应该采用可复用的表视图单元对象。一个表视图维护着一个可复用单元的队列或者列表。当要显示一个新的单元的时候就调用这个方法,这个方法会出列一个已经存在的单元。假如没有可以复用的单元那么就返回nil。
从表视图的生命周期来说,一开始可复用队列为空,调用dequeueReusableCellWithIdentifier:肯定返回nil。然后就调用initWithStyle:reuseIdentifier:方法来产生并且标识复用记号的表视图单元。满屏显示的时候,滚动表视图,一侧的单元就会被移出屏幕,此时这个单元进入可复用单元队列,然后调用prepareForReuse方法准备一个即将出列的单元, dequeueReusableCellWithIdentifier:从可复用单元队列里出列一个可复用单元。
//修改一下例子
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static int myCount = 0; //static变量只是在编译时候进行初始化
TableViewCell *cell;
cell = [tableView dequeueReusableCellWithIdentifier:@"test"];
if (nil == cell) {
// 此处调用方法1,2,3,4进行测试
myCount ++;
NSLog(@"count :%d ",myCount);
}
//第二种输出
NSLog(@"+++++++++before:%@",cell.textLabel.text);
cell.textLabel.text = [NSString stringWithFormat:@"%ld",(long)indexPath.row];
NSLog(@"---------after:%@",cell.textLabel.text);
}
输出结果
/* 方法3的第二种输出
2016-09-08 16:49:18.066 TableViewTest[43861:2739078] time : 1
2016-09-08 16:49:18.068 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.068 TableViewTest[43861:2739078] ---------after:0
2016-09-08 16:49:18.071 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.071 TableViewTest[43861:2739078] ---------after:1
2016-09-08 16:49:18.072 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.072 TableViewTest[43861:2739078] ---------after:2
2016-09-08 16:49:18.072 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.073 TableViewTest[43861:2739078] ---------after:3
2016-09-08 16:49:18.073 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.073 TableViewTest[43861:2739078] ---------after:4
2016-09-08 16:49:18.074 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.098 TableViewTest[43861:2739078] ---------after:5
2016-09-08 16:49:18.099 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.099 TableViewTest[43861:2739078] ---------after:6
2016-09-08 16:49:18.100 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.100 TableViewTest[43861:2739078] ---------after:7
2016-09-08 16:49:18.101 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:18.101 TableViewTest[43861:2739078] ---------after:8
2016-09-08 16:49:19.966 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:19.966 TableViewTest[43861:2739078] ---------after:9
2016-09-08 16:49:32.542 TableViewTest[43861:2739078] +++++++++before:(null)
2016-09-08 16:49:32.543 TableViewTest[43861:2739078] ---------after:10
2016-09-08 16:49:37.408 TableViewTest[43861:2739078] +++++++++before:1
2016-09-08 16:49:37.409 TableViewTest[43861:2739078] ---------after:11
*/
/*方法1,方法2,方法4的第二种输出
2016-09-08 16:53:31.731 TableViewTest[43916:2742025] time : 1
2016-09-08 16:53:31.734 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.735 TableViewTest[43916:2742025] ---------after:0
2016-09-08 16:53:31.736 TableViewTest[43916:2742025] time : 2
2016-09-08 16:53:31.736 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.737 TableViewTest[43916:2742025] ---------after:1
2016-09-08 16:53:31.737 TableViewTest[43916:2742025] time : 3
2016-09-08 16:53:31.738 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.738 TableViewTest[43916:2742025] ---------after:2
2016-09-08 16:53:31.738 TableViewTest[43916:2742025] time : 4
2016-09-08 16:53:31.738 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.739 TableViewTest[43916:2742025] ---------after:3
2016-09-08 16:53:31.739 TableViewTest[43916:2742025] time : 5
2016-09-08 16:53:31.740 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.740 TableViewTest[43916:2742025] ---------after:4
2016-09-08 16:53:31.740 TableViewTest[43916:2742025] time : 6
2016-09-08 16:53:31.741 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.741 TableViewTest[43916:2742025] ---------after:5
2016-09-08 16:53:31.742 TableViewTest[43916:2742025] time : 7
2016-09-08 16:53:31.742 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.742 TableViewTest[43916:2742025] ---------after:6
2016-09-08 16:53:31.743 TableViewTest[43916:2742025] time : 8
2016-09-08 16:53:31.743 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.744 TableViewTest[43916:2742025] ---------after:7
2016-09-08 16:53:31.744 TableViewTest[43916:2742025] time : 9
2016-09-08 16:53:31.745 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:31.745 TableViewTest[43916:2742025] ---------after:8
2016-09-08 16:53:38.519 TableViewTest[43916:2742025] time : 10
2016-09-08 16:53:38.519 TableViewTest[43916:2742025] +++++++++before:(null)
2016-09-08 16:53:38.519 TableViewTest[43916:2742025] ---------after:9
2016-09-08 16:53:38.604 TableViewTest[43916:2742025] +++++++++before:0
2016-09-08 16:53:38.605 TableViewTest[43916:2742025] ---------after:10
2016-09-08 16:53:38.688 TableViewTest[43916:2742025] +++++++++before:1
2016-09-08 16:53:38.689 TableViewTest[43916:2742025] ---------after:11
2016-09-08 16:53:38.740 TableViewTest[43916:2742025] +++++++++before:2
2016-09-08 16:53:38.740 TableViewTest[43916:2742025] ---------after:12
2016-09-08 16:53:38.808 TableViewTest[43916:2742025] +++++++++before:3
2016-09-08 16:53:38.808 TableViewTest[43916:2742025] ---------after:13
*/
方法3中,可复用的cell的text从1开始,从xib中创建的第一个cell,它的identify还没有注册,所以identify值没有,不能复用。复用是从第二个单元格开始
方法1,方法2,方法4中,可复用的cell从第一个单元格开始。
以上的结果跟我们一开始的猜测应该是一样的吧!
在标题边上有github的demo下载地址,有需要的朋友可以下载来看一下 demo下载
UITableView优化问题
在实际开发中,有时候只使用单元格复用还是不够的,在滑动的时候也同样会有卡顿感,那么我们该怎么办呢?
以下是天哥根据多年网上的学习归纳总结的几点,不对的地方大家也请点出来:
1.单元格复用
这个我们前面探究的时候说过了,单元格复用能有效的减少内存的消耗。单元格复用的实现是最简单也是最重要的。
2.使用异步网络请求,缓存图片或者数据
如果我们要显示的内容跟之前的一样,每次都要从网上获取,是不是很浪费呢?我们要减少数据加载和逻辑处理的时间,从内存中获取就大大减少了这方面的消耗,我们要将。现在被大家广泛使用的AFNetworking就是异步网络请求。
3. 提前计算并缓存Cell的高度
在UITableView中,运行时首先调用的方法是”tableView:heightForRowAtIndexPath:”,如果我们在这个方法里面进行各种计算什么的,将会大大增加手机的负担。所以,我们尽量能不调用代理方法的就不要调用(在创建tableview的时候使用tableview.rowHeight来确定cell的高度)。必须调用的情况下(比如每个section所对应的cell不一样等等)也尽量不要在”tableView:heightForRowAtIndexPath:”这个方法中进行一系列的计算,而是提前计算好,直接将计算好的值使用到这个方法中。如果必须要在方法里面计算,那要尽力做最简单的计算。
4.异步绘制
在自定义的cell中,假设工作需求里面,我们要将cell上的某个Button的边框变成红色并且边框要有一点圆弧形。这个时候,我们对view外形的操作最好都放在类似于drawRect的方法里面。drawRect方法是异步绘制,可以大大的缓解主线程的压力。
5.滑动时,按需加载。图片加载时,异步加载(SDWebImage已经实现了异步加载图片,有兴趣的朋友可以去研究一下)。
大家考虑一个场景,如果我们滑动的很快,中间那些瞬间而过的内容是不是不需要显示?哪怕显示了也没用对吧?滑动的那么快我们怎么看得清呢?所以,按需加载就很重要了。我举的例子只是其中的一种情况,这里的“需”每个人都各不一样,有些可能是希望当滑动停止的时候才加载图片(如安卓版本的饿了么。但是iOS版本的饿了么并没有做任何处理,版本低点的iphone手机就会有卡顿感)还有百度外卖的滑动显示逻辑又不一样了(安卓版本的百度外卖在滑动时,要显示下一个图片就慢慢加载出来。iOS版本就没做这个处理,同样老一点的iphone手机也有卡顿感)这里卡顿感我都是用iphone5体验到的,我用iphone6s点外卖就没有卡顿感。按需加载最重要的一点是,我们充分利用好以下方法(并不绝对,看每个人的需求)
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
这些方法思想就是识别UITableView禁止或者减速滑动结束的时候,进行异步加载图片,快滑动过程中,只加载目标范围内的Cell,这样按需加载,极大的提高流畅度。而SDWebImage可以实现异步加载,与这条性能配合就完美了,尤其是大量图片展示的时候。
6.尽量减少subviews的个数和层级
有时候根据需求,我们不得不在cell上面添加button,image,label,textview等等。必须要加的东西,我们还是要加,但是我们尽量想办法减少这些层级数量。如果我们对某个view设置了alpha < 1,我们应该将这个view的opaque属性设置为NO。如果alpha = 1,则设置opaque为YES。默认情况下opaque为YES。 原因见官方文档,英语大致意思相对简单,我就不翻译了。
关于优化tableview的demo是我在以前学习网上知识下的demo上进行了修改并且增加了注释后完成的,便于大家理解。优化方法很多,这并不是最好的优化,但是可以给大家做一个参考。 优化demo下载