iOS: UITableView性能优化

1,109 阅读5分钟

卡顿解决的主要思路

  • (1) 尽可能减少CPU、GPU资源消耗
  • (2) 按照60FPS的刷帧率,每隔16ms就会有一次VSync信号

从CPU和GPU两方面入手进行卡顿优化

减轻CPU负荷

我们知道CPU的主要负责快速调度任务,大量计算工作,所以在tableView快速滚动的过程中让CPU的计算量降低是优化应该考虑的方向.下面总结了三个方面来尽可能的降低CPU计算:

提前计算好cell的高度,缓存在相应的数据源模型中

我们知道tableView的代理回调方法中,先调用的是返回cell高度的方法,然后在返回实例化cell的方法.我们可以在返回cell高度时,提前计算好cell的高度,缓存到数据源模型中。

尽可能的减少storyboard,xib的使用

通过Interface知道xib或者storyboard本身就是一个xml文件,添加删除控件必然中间多了一个encode/decode过程,增加了cpu的计算量。并且还要避免臃肿的 XIB 文件,因为XIB文件在主线程中进行加载布局。当用到一些自定义View或者XIB文件时,XIB的加载会把所有内容加载进来,如果XIB里面的一些控件并不会用到,这就可能造成一些资源的消耗浪费。

比如使用纯代码布局,使用masonry约束布局或者手动计算布局。本人就是完全抛弃了storyboard和xib。

滑动过程中尽量减少重新布局

自动布局就是给控件添加约束,约束最终还是转换成frame。所以在满足业务需求情况下,如果图层层次较为复杂,要尽量减少自动布局约束,转为手动计算布局,大量的约束重叠也会增加cpu的计算量。

比如,如果内容相对固定,可以将UILabel的宽高写死,只是更新其文本内容即可

按需加载

当滑动 UITableView 时,按需加载对应的内容

#pragma mark - UIScrollViewDelegate

// 按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定3行加载。
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
    NSIndexPath *ip = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];   // 停止拖拽后,预计滑动停止后到的偏移量
    NSIndexPath *cip = [[self.tableView indexPathsForVisibleRows] firstObject]; // 当前可视区域内 cell 组
    NSInteger skipCount = 8;
    NSLog(@"targetContentOffset = %f",targetContentOffset->y);
    NSLog(@"indexPathForRowAtPoint = %@",ip);
    NSLog(@"visibleRows = %@",[self.tableView indexPathsForVisibleRows]);
    if (labs(cip.row - ip.row) > skipCount) {   // labs-返回 x 的绝对值,进入该方法,说明滑动太厉害了,预计停留位置与当前可视区域范围内差 8 个cell 以上了.
        // 拖拽停止滑动停止后,即将显示的 cell 索引组
        NSArray *temp = [self.tableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.tableView.width, self.tableView.height)];
        NSMutableArray *arrM = [NSMutableArray arrayWithArray:temp];
        NSLog(@"temp = %@",temp);
        if (velocity.y < 0) {   // 向上滑动-即加载更多数据
            NSIndexPath *indexPath = [temp lastObject];
            if (indexPath.row + 3 < self.dataSource.count) {    // 滑动停止后出现的 cell 索引仍在数据源范围之内
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row + 2 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row + 3 inSection:0]];
            }
        } else {    // 向下滑动-加载之前的数据
            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row > 3) {
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row - 3 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row - 2 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0]];
            }
        }
        
        [self.needLoadArray addObjectsFromArray:arrM];
    }
}

异步绘制

当视图层级比较多的时候,可以采用异步绘制的方式,通过UIGraphics将内容绘制然后生成一张图片进行展示。

实现步骤如下

  • 处理数据源当我们请求到了数据后,需要根据后端返回的数据,根据内容将布局计算出来,后面再进行绘制,
  • 异步绘制数据,在赋值数据模型里面进行内容的绘制

延时加载图片

Runloop每次循环都会对你界面上的UI绘制一遍,主要是速度快,我们看不出来,当界面中出现高清的图片时因为绘制的慢,就会导致卡顿。 所以我们监听runloop的状态,每次即将休眠的时候,即处于kCFRunLoopBeforeWaiting状态时才去绘制加载图片。

// 监听 runloop 状态
- (void)addRunloopObserver {
    // 获取当前 runloop
    //获得当前线程的runloop,因为我们现在操作都是在主线程,这个方法就是得到主线程的runloop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();

    //定义一个观察者,这是一个结构体
    CFRunLoopObserverContext context = {
        0,
        (__bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };

    // 定义一个观察者
    static CFRunLoopObserverRef defaultModeObsever;
    // 创建观察者
    defaultModeObsever = CFRunLoopObserverCreate(NULL,
                                                 kCFRunLoopBeforeWaiting,   // 观察runloop等待的时候就是处于NSDefaultRunLoopMode模式的时候
                                                 YES,   // 是否重复观察
                                                 NSIntegerMax - 999,
                                                 &Callback, // 回掉方法,就是处于NSDefaultRunLoopMode时候要执行的方法
                                                 &context);
    
    // 添加当前 RunLoop 的观察者
    CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
    //c语言有creat 就需要release
    CFRelease(defaultModeObsever);
}

// 每次 runloop 回调执行代码块
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    DelayLoadImgViewController *vc = (__bridge DelayLoadImgViewController *)(info);  // 这个info就是我们在context里面放的self参数
    
    if (vc.tasks.count == 0) {
        return;
    }
    
    BOOL result = NO;
    while (result == NO && vc.tasks.count) {
        NSLog(@"开始执行加载图片总任务数:%d",vc.tasks.count);
        // 取出任务
        RunloopBlock unit = vc.tasks.firstObject;
        // 执行任务
        result = unit();
        // d干掉第一个任务
        [vc.tasks removeObjectAtIndex:0];
    } 

将加载绘制图片丢到任务中

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NewsModel *model = [self.dataSource objectAtIndex:indexPath.row];
    DelayLoadImgCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.model = model;
    // 将加载绘制图片操作丢到任务中去
    [self addTask:^BOOL{
        [cell drawImg];
        return YES;
    }];
    return cell;
}

/// 绘制图片
- (void)drawImg {
    if (_model.imgs.count > 0) {
        __block float posX = 0;
        [_model.imgs enumerateObjectsUsingBlock:^(NSString *obj, NSUInteger idx, BOOL *stop) {
            UIImageView *imgView = [[UIImageView alloc] initWithFrame:CGRectMake(posX, 0, kImgViewWH, kImgViewWH)];
            [imgView sd_setImageWithURL:[NSURL URLWithString:obj]];
            imgView.layer.cornerRadius = 5;
            imgView.layer.masksToBounds = YES;
            
            [self.imgListView addSubview:imgView];
            posX += (5 + kImgViewWH);
            if (idx >= 3) {
                *stop = YES;
            }
        }];
    }
}
  • 监听 runloop 状态并且执行当 runloop 处于kCFRunLoopBeforeWaiting时需要执行的代码块
  • 每次当我们滑动时,图片不显示,因为这个时候runloop不处于kCFRunLoopBeforeWaiting状态。当我们停止拖拽滑动时,runloop 处于kCFRunLoopBeforeWaiting状态,然后加载绘制图片。