UITableView 性能优化

UITableView 性能优化

最近在阅读 ibireme 文章时,YY 在异步绘制中提到了 VVeboTableViewDemo,该项目提供了一种滑动过程中 Cell 绘制性能提升的解决方案。本文在介绍 VVeboTableViewDemo 实现细节的同时会对现有 UITableView 已有优化方案进行介绍并分析其中优缺点。

UITableView 简介

UITableView 和其他控件相比最大的特点是:UITableViewCell 的重用机制。简而言之:UITableView 只会创建一屏(或一屏多点)的 UITableViewCell,每次显示时都是复用这些 Cell。当要显示某一位置的 Cell 时,会先去集合(或数组)中取,如果有,直接显示;如没有,创建 Cell 并放入缓存池。当 Cell 滑出屏幕时,该 Cell 就会被放入集合(或数组)中。

UITableView 在显示时会多次调用这两个方法:

  • - tableView:heightForRowAtIndexPath:
  • - tableView:cellForRowAtIndexPath:

通常情况下,我们会认为 UITableView 在显示的时候会先调用前者,再调用后者,这和我们创建控件的思路是一致的,先创建它,再设置布局。但实际使用时并非如此,UITableView 是继承自 UIScrollView,需要先确定 contentSize 及每个 Cell 的位置,然后才会把复用的 Cell 放到对应的位置。因此,UITableView 会先多次回调 - tableView:heightForRowAtIndexPath: 确定 contentSize 和 Cell 的位置,然后再调用 - tableView:cellForRowAtIndexPath: 来确定显示的 Cell。

举个例子:

现在要显示100个 Cell,一屏显示5个,那么刷新(reload)TableView 时,TableView 会先调用100次 - tableView:heightForRowAtIndexPath: 方法,然后调用5次 - tableView:cellForRowAtIndexPath: 方法;滑动屏幕时,当有新 Cell 滑入屏幕时,都会调用一次- tableView:heightForRowAtIndexPath:- tableView:cellForRowAtIndexPath: 方法。

UITableView 优化

上一节已对 TableView 的复用机制和核心方法进行了简要介绍,下面将基于示例来介绍现有 TableView 的优化方案。

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    ContacterTableCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ContacterTableCell"];
    if (!cell) {
        cell = (ContacterTableCell *)[[[NSBundle mainBundle] loadNibNamed:@"ContacterTableCell" owner:self options:nil] lastObject];
    }
    NSDictionary *dict = self.dataList[indexPath.row];
    [cell setContentInfo:dict];
    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
    return cell.frame.size.height;
}

上面代码是我们初次使用 TableView 时的常见写法,很多教程也是这么来写的。但基于上一节中的分析我们知道,在显示一屏 Cell 之前,我们需要计算全部 Cell 的高度。如果有1000行数据,就会调用1000+次 - cellForRowAtIndexPath:indexPath,而该方法非常重,我们会在该方法中对模型赋值,设置 Cell 布局等,每次调用开销很大,滑动过程中会卡顿,急需优化。

预计算高度并缓存

例子中代码存在两个问题:

  • 高度计算和 Cell 赋值耦合
  • 高度未缓存

高度计算和 Cell 赋值应当分离,TableView 的两个回调方法应各司其职,不应存在依赖关系。Cell 的高度计算过后就不会变更,此时可以将其缓存,下次使用时直接读取即可。

基于上述思路,从网络获取到数据后,根据数据计算出相应的布局,并缓存到数据源中,在 - tableView:heightForRowAtIndexPath: 方法中可直接返回高度,不需要重复计算。

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
        NSDictionary *dict = self.dataList[indexPath.row];
    CGRect rect = [dict[@"frame"] CGRectValue];
        return rect.frame.height;
}

本方案在一般的场景下可以满足性能的要求,但是在像朋友圈图文混排需求面前,依旧会有卡顿现象出现。究其原因:本方案中所有 Cell 的绘制都放在主线程,当 Cell 非常复杂主线程绘制不及时就会出现卡顿。

异步绘制

上一个方案中所有 Cell 的绘制都在主线程中进行,如将绘制任务提交到后台线程,则主线程任务会显著减少,滑动性能会显著提升。

首先为自定义的 Cell 添加 draw 方法,在方法体中实现绘制任务:

// 异步绘制
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
   CGRect rect = [_data[@"frame"] CGRectValue];
   UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
   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;
            }
    }
    // 绘制文本
    [self drawText];
}}

Cell 中各项内容都根据之前算好的布局进行异步绘制,此时 TableView 的性能较之前又提高了一个等级。

条件绘制

但 TableView 的优化之路仍未停止,在 TableView 高速滑动时,滑动过程中的多数 Cell 对用户来说都是无用的,用户只关心滑动停止时附近的几个 Cell。滑动时,用户松开手指后,立刻计算出滑动停止时 Cell 的位置,并预先绘制那个位置附近的几个 Cell。这个方法比较有技巧性,并且对滑动性能来说提升巨大,唯一的缺点就是快速滑动中会出现大量空白内容。

//按需加载 - 如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后指定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+3<datas.count) {
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+1 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+2 inSection:0]];
                [arr addObject:[NSIndexPath indexPathForRow:indexPath.row+3 inSection:0]];
            }
        } else { // 上滑
            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row>3) { 
                [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,按需绘制,提高 TableView 流畅度。

总结

通过介绍上述几个优化方案,TableView 的优化可以从下面几方面入手:

  • 提前计算并缓存高度
  • 异步渲染内容到图片
  • 滑动时按需加载

除了上述大方向外,TableView 还有很多大家都熟知的优化点:

  • 正确使用 reuseIdentifier 来重用Cells
  • 尽量使所有的 view opaque,包括Cell自身
  • 尽量少用或不用透明图层
  • 如果 Cell 内现实的内容来自 web,使用异步加载,缓存请求结果
  • 减少 subviews 的数量
  • 在heightForRowAtIndexPath:中尽量不使用 cellForRowAtIndexPath:,如果你需要用到它,只用一次然后缓存结果
  • 尽量少用 addView 给 Cell 动态添加 View,可以初始化时就添加,然后通过hide来控制是否显示

参考文章:

  1. iOS 保持界面流畅的技巧
  2. VVeboTableViewDemo
  3. UITableView优化技巧
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容