如何优化UITableView?

优化UITableView常用的方式有:Cell重用缓存Cell高度Cell数据资源缓存渲染减少视图数目减少多余的绘制操作不要给cell动态添加subview异步化UI,不要阻塞主线程滑动时按需加载对应的内容

1. Cell重用机制

这是UITableView的基本使用

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    static NSString *Identifier = @"cell";
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: Identifier];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:ID];
    }
    return cell;
}

这样并不完美,我们经常在- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath中为每个cell绑定数据,实际在调用- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath的时候cell还没有被显示出来。为了提高效率,我们应该把数据绑定的操作放在cell即将显示的时候执行,即在- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;方法中绑定数据。
注意- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath;在cell展示在tableView之前就会调用,此时cell的实例已经生成,所以不能更改cell的结构,只能改动cell上的UI的一些属性,如:label的内容等。

2. 缓存Cell高度

这里将Cell分成两种:一种是定高的Cell,一种是动态高度的Cell。

  1. 定高的Cell,应该用如下的方式:
    self.tableView.rowHeight = 88;
    这个方法指定了所有cell的高度都是88,rowHeight默认是44,对于定高的Cell直接使用该方法设定高度,不需要实现tableView:heightForRowAtIndexPath:以节省不必要的计算和开销。
  2. 动态高度的Cell
    我们需要实现tableView的代理,给出高度:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
    //
    return xxx;
}

这个代理方法实现后,上面的rowHeight的设置将会变成无效。在这个方法中,我们需要提高Cell高度的计算效率,来节省时间。
自从iOS8之后,有了self-sizing Cell的概念,Cell可以自己算出高度,使用self-sizing cell需要满足以下三个条件:
(1)使用AutoLayout进行UI布局约束,要求cell.contentView的四条边都与内部元素有约束关系;
(2)指定TableView的estimatedRowHeight属性的默认值;
(3)指定TableView的rowHeight属性为UITableViewAutomaticDimension;

- (void)viewDidLoad {
    self.tableView.estimatedRowHeight = 44.0;
    self.tableView.rowHeight = UITableViewAutomaticDimension;
}

除了提高Cell的计算效率之外,对于已经计算出的高度,我们需要进行缓存,对于已经计算过的高度,没有必要计算第二次。
创建viewModel,计算并存储Cell的UI尺寸信息

@interface YZZYCellHeightViewModel : NSObject
@property (strong, nonatomic) YZZYModel *dataModel; // 原始数据模型
@property (assign, nonatomic) CGFloat cellHeight; // Cell高度
- (void)calculateCellHeight; // 计算高度

这里需要注意:
在iOS中,系统是先调用- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath获取每个Cell即将显示的高度,确定整个UITableView的布局,然后才调用- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath获取Cell。因此,使用了ViewModel来保存UI信息,Cell高度的计算和使用时机需要特别留意。

3. Cell数据资源缓存

提前处理Cell需要显示的数据资源,在Cell显示之前,将从服务器加载获取到的原始数据在viewModel中提前处理,一般包括:图片的加载和压缩、富文本的多样化显示(NSString->NSAttributeString).
这时的viewModel可能是这样的

@interface YZZYViewModel : NSObject
@property (strong, nonatomic) YZZYModel *dataModel; //原始数据模型
@property (assign, nonatomic) CGFloat cellHeight; // Cell高度
/* 需要展示的内容*/
@property (strong, nonatomic) NSAttributeString *titleToShow;
@property (strong, nonatomic) NSAttributeString *contentToShow;
- (void)calculateCellHeight; // 计算高度
- (void)handleSourceDataModel;
@end

4. 渲染

为了保证UITableView的流畅,当快速滑动的时候,Cell必须被快速的渲染出来。提高Cell渲染速度的方法有以下三种:

  1. 当有图像时,预渲染图像,在bitmap context先将其画一遍,导出成UIImage对象,然后再绘制到屏幕,这会大大提高渲染速度。具体内容可以自行查找“利用预渲染加速显示iOS图像”相关资料。
  2. 渲染最耗时的操作之一就是混合(blending),所以不要使用透明背景,将Cell的opaque(不透明)的值设为YES,背景色不要使用clearColor,尽量不要使用阴影渐变等。
  3. 由于混合操作是使用GPU来执行,我们可以用CPU来渲染,这样混合操作就不再执行,可以在UIView的drawRect方法中自定义绘制。

5. 减少视图的数目

我们在Cell上添加系统控件的时候,实际上系统都会调用底层接口进行绘制,大量添加控件时,会消耗很大的资源并且也会影响渲染的性能。当使用默认的UITableViewCell并且在它的ContentView上面添加控件时会相当消耗性能。所以,目前最佳的方法还是继承UITableViewCell,并重写drawRect方法。

5. 减少多余的绘制操作

在实现drawRect方法的时候,它的参数rect就是我们需要绘制的区域,在rect范围之外的区域不要进行绘制,否则会消耗相当大的资源。

6. 不要给Cell动态添加subView

在初始化Cell的时候就将所有需要展示的控件添加完毕,然后根据需要来设置hide属性显示和隐藏控件。

7. 异步化UI,不要阻塞主线程

加载时整个页面卡住不动,怎么点都没用,仿佛死机了一般,原因就是主线程被阻塞了。所以,对于网络请求或图片的加载,可以开启多线程将耗时的操作放在子线程中进行,异步操作。

8. 滑动时按需加载对应的内容

如果目标行与当前行相差超过指定行数,只在目标滚动范围的前后制定3行加载。

- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inoutCGPoint *)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;
}

9. 离屏渲染

  1. 以下的操作会引发离屏渲染:
    (1)为图层设置遮罩(layer.mask)
    (2)将图层的layer.maskToBounds/view.clipsToBounds属性设置为true
    (3)将图层layer.allowsGroupOpacity属性设置为YES和layer.opacity小于1.0
    (4)将图层设置阴影(layer.shadow)
    (5)为图层设置layer.shouldRasterize=true
    (6)具有layer.cornerRadius、layer.edgeAntialiasingMask、layer.allowsEdgeAntialiasing的图层
    (7)任何种类的文本,包括:UILabel、CATextLayer和Core Text等
    (8)使用CGContext在drawRect:方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。
  2. 优化方案
    官方对离屏渲染产生的性能问题也进行了优化,iOS 9.0之前UIImageView跟UIButton设置圆角都会触发离屏渲染;iOS 9.0之后UIButton设置圆角会触发离屏渲染,而UIImageView里png图片设置圆角不会触发离屏渲染了,如果设置其他阴影效果之类的还是会触发离屏渲染。
    (1)圆角优化
    在APP开发中,圆角优化经常出现。如果一个界面中只有少量圆角图片或许对性能没有非常大的影响,但是当圆角图片比较多的时候就会对APP的性能产生明显的影响。
    开发者设置圆角一般通过如下方式:
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;

这样处理的渲染机制是GPU在当前屏幕缓冲区外新开辟一个渲染缓冲区进行工作,也就是离屏渲染,这会给我们带来额外的性能损耗。如果这样的圆角操作达到一定数量,会触发缓冲区的频繁合并和上下文的频繁切换,性能的代价体现在用户体验上就是掉帧。
优化方案1:使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
// 开始对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();
[self.view addSubview:imageView];

优化方案2:使用CAShapeLayer和UIBezierPath设置圆角

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
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;
[self.view addSubview:imageView];

对于方案2需要解释的是:

  • CAShapeLayer继承于CALayer,可以使用CALayer的所有属性;
  • CAShapeLayer需要贝塞尔曲线配合使用才有效果;
  • 使用CAShapeLayer(属于Core Animation)与贝塞尔曲线可以实现不在view的drawRect方法中画出一些想要的图片;
  • CAShapeLayer动画渲染直接提交到手机的GPU中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。
    总的来说就是CAShapeLayer的内存消耗较少,渲染速度快,建议使用优化方案2
    (2)shadow优化
    对于shadow,如果图层是个简单的几何图形或者圆角图形,我们可以通过设置shadowPath来优化性能,能大幅提高性能。示例如下:
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;

开发者可以通过设置shouldRasterize属性为YES来强制开启离屏渲染,其实就是光栅化(Rasterization)。
既然离屏渲染这么不好,为什么还要强制开启呢?
当一个图像混合了多个图层,每次移动时,每一帧都要重新合成这些图层,十分消耗性能。当开启光栅化后,会在首次产生一个位图缓存,当再次使用时就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存。所以这个功能一般不能用于UITableViewCell中,Cell的复用反而降低了性能。最好用于图层较多的静态内容的图形。而且产生的位图缓存大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除,所以我们要根据使用场景而定。
(3)其他一些优化建议

  • 当我们需要圆角效果时,可以使用一张中间透明的图片蒙上去;
  • 使用ShadowPath指定layer阴影效果路径
  • 使用异步进行layer渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
  • 设置layer的opaque值为YES,减少复杂图层合成
  • 尽量使用不包含透明(alpha)通道的图片资源
  • 尽量设置layer的大小值为整型值
  • 直接让美工把图片切成圆角进行显示,这是效率最高的一种方案
  • 很多情况下,用户上传图片进行显示,可以让服务端处理圆角
  • 使用代码手动生成圆角Image设置到要显示的view上,利用UIBezierPath(Core Graphics框架)画出圆角图片
    (4)Core Animation工具检测离屏渲染
    对于离屏渲染的监测,苹果为我们提供了一个测试工具Core Animation,可以在Xcode->Open Developer Tools->Instruments中找到,如下图:

    Core Animation工具用来监测Core Animation性能,提供可见的FPS值,并且提供几个选项来测量渲染性能,如下图:

    下面我们来说明每个选项的功能
  • Color Blended Layers:这个选项如果勾选,你能看到哪个layer是透明的,GPU正在做混合计算。显示红色的就是透明的,绿色就是不透明的。
  • Color Hits Green and Misses Red:如果勾选你这个选项,且当我们代码中有设置shouldRasterize为YES,那么红色代表没有复用离屏渲染的缓存,绿色则表示复用了缓存,开发者当然希望能够复用。
  • Color Copied Images:按照官方的说法,当图片的颜色格式GPU不支持的时候,Core Animation会拷贝一份数据让CPU进行转化。例如:从网络上下载了TIFF格式的图片,则需要CPU进行转化,这个区域会显示成蓝色。还有一种情况会触发Core Animation的copy方法,就是字节不对齐的时候,如下图:
  • Color Immediately:默认情况下Core Animation工具以每毫秒10次的频率更新图层调试颜色,如果勾选这个选项则移除10ms的延迟。对某些情况需要这样,但是有可能影响正常帧数的测试。
  • Color Misaligned Images:勾选此项,如果图片需要缩放则标记为黄色,如果没有像素对齐则标记为紫色。像素对齐我们已经在上面有所介绍。
  • Color Offscreen-Rendered Yellow:用来检测离屏渲染的,如果显示黄色,表示有离屏渲染。当然还要结合Color Hits Green and Misses Red来看,是否复用了缓存。
  • Color OpenGL Fast Path Blue:这个选项对那些使用OpenGL的图层才有用,像是GLKView或者 CAEAGLLayer,如果不显示蓝色则表示使用了CPU渲染,绘制在了屏幕外,显示蓝色表示正常。
  • Flash Updated Regions:当对图层重绘的时候回显示黄色,如果频繁发生则会影响性能。可以用增加缓存来增强性能。

其他

  1. Cell中的View尽量不使用透明
  2. 减少子视图的层级关系
  3. 图片载入在后台进程进行,滚出可视范围的载入进程cancel掉
  4. 图片资源尽可能使用PNG

参考来源:

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

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,067评论 1 32
  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,251评论 8 265
  • 1、通过CocoaPods安装项目名称项目信息 AFNetworking网络请求组件 FMDB本地数据库组件 SD...
    X先生_未知数的X阅读 15,960评论 3 119
  • 鹰飞翔在大海上空, 听海浪的咆哮; 鹰落在礁石上, 看乱石穿空的暴躁。 它血液里是桀骜不驯的遗传, 它血液里是凌驾...
    赵建铜阅读 393评论 0 1
  • 薄暮烟村雨,孤云万里风。别来箫管惹飞鸿。铁马春秋几度?且共从戎。 晓箭雕弓响,流年类转蓬。旅思夤夜枕香梦。兵作虬炉...
    幽小窗阅读 543评论 21 35