UITableView 性能优化

4,192 阅读5分钟

优化一、Cell重用、标识重用

使用 static 修饰重用标识名称能够保证这个标识只会创建一次,提高性能。接着调用dequeueReusableCellWithIdentifier:方法获取缓存池中的Cell。如果没有就调用 initWithStyle:ReusIdentifier:方法创建一个新的Cell。注意事先需要调用registerNib/registerClass方法为TableView注册一下重用标识。

优化二、设置预估行高,预先缓存动态行高

设置预估行高

UITableView通过设置UITableView代理方法heightForRowAtIndexPath:方法来设置行高。自从iOS8.0之后,苹果新增了self-sizing cell的概念,也是cell可以自己计算行高,使用需要满足三个条件:

  1. 使用Autolayout进行UI布局约束
  2. 指定TableView的estimatedRowHeight属性的默认值
  3. 指定TableView的rowHeight的属性为UITableViewAutomaticDimension。

TableView在加载数据时会先通过estimatedHeightForRowAtIndexPath处理全部数据,此时我们只需要提供一个粗略的高度,待到cell对象创建之后再去设置cell的真实高度。而且只会处理当前屏幕范围内的cell,这样子会显著的提升加载的性能。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {  
    return 50.0;  
}  

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {  
    return 30.0;  
} 

预先计算并缓存行高

iOS8.0之后获取cell对象之后会再次调用heightForRowAtIndexPath: 方法获取行高,这也就意味着我们其实可以先创建cell对象,之后再提供行高。具体方法我们可以在cell类中添加layoutAttribute属性,记录相应的UIEdgeInsets,然后在设置cell真实高度的时候返回。iOS7.0之前则是必须在cell对象创建之前先获得所有Cell的高度。

优化三、减少Subviews层级、异步绘制、避免离屛渲染、使用hidden隐藏图层

  1. 减少图层层级数

  2. 异步绘制。通过重写本身是异步的drawReact:方法,调用Core Graphics 框架中的API 进行异步绘制,提高效率。另外drawRect:中大量的绘制操作也会造成内存的增长,可以使用CAShapeLayer来代替。

  3. 减少多于的绘制操作。在实现drawRect:方法的时候,他的参数rect就是我们需要绘制的区域,在rect范围之外的区域不要绘制,否则会消耗相当大的资源。

  4. 图片异步加载并及时释放缓存。在Cell类中添加图片应该避免使用imageWithName:方法,因为该方法会将图片缓存到内存中。而是应该使用imageWithContensOfFile:方法来替换,该方法在图片使用完后系统会自动释放资源,并不会缓存下来。另外结合SDWebImage框架的使用可以显著的提高图片加载的性能。

  5. 避免动态添加图层。在初始化cell的时候一并将所有图层预先创建好,通过hidden属性控制子图层的显示或隐藏,因为单纯的显示操作要比创建快的多。

  6. 避免离屛渲染。开启离屛渲染的代价就是需要新开辟一块新的缓冲区,在渲染的过程中还会多次的切换上下文,这些都是很消耗性能的。以下情况均会造成离屛渲染。

  • 为图层设置遮罩(layer.mask)
  • 设置图层的 layer.masksToBounds/view.clipsToBounds属性为True
  • 设置图层的 layer.allowsGroupOpacity的属性为True和layer.opacity小于1.0
  • 设置图层阴影(layer.shadow)
  • 设置图层的 layer.shouldRasterize的属性为True
  • 具有 layer.cornerRadius, layer.edgeAntialiasingMask, layer.allowsAntialiasing的图层
  • 文本(任何种类,包括UILabel、CATextLayer、Core Text等)
  • 使用CGContext在drawReact:方法中绘制
  1. 图片圆角优化

使用贝塞尔曲线 + Core Graphics框架设置圆角

- (void)setImageCircularEdge:(UIImageView *)imageView {  

    //开始对imageView进行画图  
    UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);  
    //使用贝塞尔曲线画出一个圆形图  
    [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];  
    [imageView drawRect:imageView.bounds];  
    imageView.image = UIGraphicsGetImageFromCurrentImageContext();  
    //结束画图  
    UIGraphicsEndImageContext();  
} 

使用贝塞尔曲线 + CAShapeLayer 设置圆角

- (void)setImageCircularEdge2:(UIImageView *)imageView {  

    UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];  
    CAShapeLayer *maskLayer=[[CAShapeLayer alloc] init];  
    //设置大小  
    maskLayer.frame = imageView.bounds;  
    //设置图形样子  
    maskLayer.path = maskPath.CGPath;  
    imageView.layer.mask = maskLayer;  
}  
  1. 图片阴影优化
- (void)setImageShadow:(UIImageView *)imageView {  

    imageView.layer.shadowColor = [UIColor grayColor].CGColor;  
    imageView.layer.shadowOpacity = 1.0;  
    imageView.layer.shadowRadius = 2.0;  
    UIBezierPath *path=[UIBezierPath bezierPathWithRect:imageView.frame];  
    imageView.layer.shadowPath = path.CGPath;  
}

优化四、分页加载数据,预先异步请求数据

优化五、滑动TableView时,按需加载内容

有些情况下我们可能会去快速的滑动列表,这时候其实会有大量的cell对象被创建、被重用,但其实我们可能只是去浏览列表停止的那一页的上下一定范围内的信息,前面快速划过的那些信息对我们来说都是无用的。此时我们可以通过ScrollView的代理方法

scrollViewWillEndDragging: withVelocity: targetContentoffset:

来按需加载内容。

#pragma mark - UIScrollViewDelegate  
//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。  
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {  

    NSIndexPath *targetPath = [_myTableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];  
    NSIndexPath *firstVisiblePath = [[_myTableView indexPathsForVisibleRows] firstObject];  
    NSInteger skipCount = 8;  
    if (labs(firstVisiblePath.row - targetPath.row)>  skipCount) {  
        NSArray *temp = [_myTableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, _myTableView.frame.size.width, _myTableView.frame.size.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]];  
            }  
        }  
        [_dataList addObjectsFromArray:arr];  
    }  
}  

targetContentOffset 是TableView减速到停止的地方, velocity 表示速度向量。

优化六、Cell类中应该避免请求网络加载数据

如果确实有需求不可避免,可以将网络加载任务添加到Runloop中,设置DefaultRunloopModule模式。这样子可以起到延迟加载的作用。

优化七、在willDisplayCell:forRowAtIndexPath:代理方法中绑定数据

很多人都喜欢在cellForRowAtIndexPath:方法中绑定数据,然后此时的Cell其实还未显示,该方法中包含了大量的布局、绘制相关的操作。我们应该在该方法中尽量简化我们自身的逻辑操作。这时我们可以使用在willDisplayCell:forRowAtIndePath:方法中绑定数据。

#pragma mark - UITableViewDataSource  
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {  
    static NSString *cellIdentifier = @"MyTableViewCell";  
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];  
    if (!cell) {  
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];  
    }  

    return cell;  
}  

#pragma mark - UITableViewDelegate  
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {  

    NSDictionary *dict = self.dataList[indexPath];  
    [cell updateData:dict];  
}