UITableView的核心思想是:cell的重用机制。UITbleView只会创建一屏幕(或一屏幕多一点)的cell, 每当cell滑出屏幕时,就会放倒一个集合(或数组)中(这里相当于一个重用池),当要显示某一个位置的cell时,会先根据ReuseIdentifier去集合和数组中去取,如果有直接拿来用,如果没有的话,才会去创建,这样极大地减少了内存的开销。
UITableView的两个重要的回调方法是:tableView:cellForRowAtIndexPath:和tableView:heightForRowAtIndexPath:。由于UITableView是继承自UIScrollView的,需要先确定它的contentSize及每个Cell的位置,然后才会把重用的cell放置到对应的位置。UITableView的回调顺序是先多次调用tableView:heightForRowAtIndexPath:来确定cell的位置,然后才会调用tableView:cellForRowAtIndexPath:从而来显示在当前的屏幕的cell。
举个例子来说:如果现在要显示100个Cell,当前屏幕显示5个。那么刷新(reload)UITableView时,UITableView会先调用100次tableView:heightForRowAtIndexPath:方法,然后调用5次tableView:cellForRowAtIndexPath:方法;滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:、tableView:cellForRowAtIndexPath:方法。
1:把赋值和计算布局分离。这样让tableView:cellForRowAtIndexPath:只负责赋值,tableView:heightForRowAtIndexPath:只负责计算高度。注意,这两个方法各司其职。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
return[ContacterTableCell cellHeightOfInfo:dict];
}
基于上面的思路,我们可以在获得数据后,直接根据数据源计算出对应的布局,并缓存到数据源中,这样在tableView:heightForRowAtIndexPath:方法中就直接返回高度,而不需要每次都计算了。
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
NSDictionary *dict = self.dataList[indexPath.row];
CGRect rect = [dict[@"frame"] CGRectValue];
returnrect.frame.height;
}
2:上面的方案并不是最佳的方案,可以满足简单的界面!如果像朋友圈那样的图文混排,这种方案其实扛不住的,自定义cell的绘制。在cell上添加系统的控件时,实质系统都需要调用底层的接口进行绘制,如果大量的添加,对资源的开销会很大,所以我们可以直接绘制,提高效率。
首先自定义cell的draw方法,(也可以重写drawRect)然后在方法中实现
//异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
CGRect rect = [_data[@"frame"] CGRectValue];
UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);//<CoreGraphics/CoreGraphics.h>
CGContextRef context = UIGraphicsGetCurrentContext();
//整个内容的背景
[[UIColor colorWithRed:250/255.0 green:250/255.0 blue:250/255.0 alpha:1] set];
CGContextFillRect(context, rect);
//转发内容的背景
if([_data valueForKey:@"subData"]) {
[[UIColor colorWithRed:243/255.0 green:243/255.0 blue:243/255.0 alpha:1] set];
CGRect subFrame = [_data[@"subData"][@"frame"] CGRectValue];
CGContextFillRect(context, subFrame);
[[UIColor colorWithRed:200/255.0 green:200/255.0 blue:200/255.0 alpha:1] set];
CGContextFillRect(context, CGRectMake(0, subFrame.origin.y, rect.size.width, .5));
}
{
//名字
float leftX = SIZE_GAP_LEFT+SIZE_AVATAR+SIZE_GAP_BIG;
float x = leftX;
float y = (SIZE_AVATAR-(SIZE_FONT_NAME+SIZE_FONT_SUBTITLE+6))/2-2+SIZE_GAP_TOP+SIZE_GAP_SMALL-5;
[_data[@"name"] drawInContext:context withPosition:CGPointMake(x, y) andFont:FontWithSize(SIZE_FONT_NAME)
andTextColor:[UIColor colorWithRed:106/255.0 green:140/255.0 blue:181/255.0 alpha:1]
andHeight:rect.size.height];
//时间+设备
y += SIZE_FONT_NAME+5;
float fromX = leftX;
float size = [UIScreen screenWidth]-leftX;
NSString *from = [NSString stringWithFormat:@"%@ %@", _data[@"time"], _data[@"from"]];
[from drawInContext:context withPosition:CGPointMake(fromX, y) andFont:FontWithSize(SIZE_FONT_SUBTITLE)
andTextColor:[UIColor colorWithRed:178/255.0 green:178/255.0 blue:178/255.0 alpha:1]
andHeight:rect.size.height andWidth:size];
}
//将绘制的内容以图片的形式返回,并调主线程显示
UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
dispatch_async(dispatch_get_main_queue(), ^{
if(flag==drawColorFlag) {
postBGView.frame = rect;
postBGView.image = nil;
postBGView.image = temp;
}
}
//内容如果是图文混排,就添加View,用CoreText绘制
[self drawText];
}}
大体的思路是这个情况,各个信息都是根据之前算好的布局进行绘制的。这里是需要异步绘制,但如果在重写 drawRect方法就不需要用GCD异步线程了,因为drawRext本来就是异步绘制的。
3:进行异步绘制这样的话,UITableView的效率提高了一个等级!不过我们还可以从UIScrollerView的角度出发,再次找到突破口
滑动UITableView时按需加载对应的内容:
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset{
NSIndexPath *ip = [self indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
NSIndexPath *cip = [[self indexPathsForVisibleRows] firstObject];
NSInteger skipCount = 8;
if(labs(cip.row-ip.row)>skipCount) {
NSArray *temp = [self indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.width, self.height)];
NSMutableArray *arr = [NSMutableArray arrayWithArray:temp];
if(velocity.y<0) {
NSIndexPath *indexPath = [temp lastObject];
if(indexPath.row+33) {
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-3 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-2 inSection:0]];
[arr addObject:[NSIndexPath indexPathForRow:indexPath.row-1 inSection:0]];
}
}
[needLoadArr addObjectsFromArray:arr];
}
}
记得在tableView:cellForRowAtIndexPath:方法中加入判断:
if(needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
[cell clear];
return;
}
滚动很快时,只加载目标范围内的Cell,这样按需加载,极大的提高流畅度。
总结:UITableView的优化主要从三个方面入手
(1)提前计算并缓存好高度,因为heightForRowAtIndexPath:是调用最频繁的方法
(2)异步绘制,遇到复杂的界面,遇到性能瓶颈时,可能就是突破点
(3)滑动时按需加载,这个在大量的图片展示,网络加载的时候很管用!
除了以上最主要的三个方面,还有其他的优化点:
•正确使用reuseIdentifier来重用cells
•尽量使所有的view opaque,包括cell自身。
opaque属性提示绘制系统如何处理view。如果opaque设置为YES,绘图系统会将view看为完全不透明,这样会绘图系统就可以优化一些绘制操作以提升性能。如果设置为NO,那么绘图系统结合其它的内容来处理view。默认情况下,这个属性是YES.
•尽量少用或不用透明图层
•如果cell内的显示的内容来自web,使用异步加载,缓存请求结果
•减少subviews的数量
•在heightForRowAtIndexPath:中尽量不使用cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
•尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显示