【iOS】性能优化总结 —— UITableView

优化一、善用重用标识

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

    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];  
    if (!cell) {  
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];  
    } 

需要注意的是我们在自定义Cell的时候需要尽量将相识度高的Cell合并为一种样式的Cell。我们知道运行在当前设备屏幕中的Cell数量是有限的,设为N个。如果有M种样式,那么基于Cell的重用机制,我们知道在缓存池中运行最多时将有M*N个Cell的实例被创建,这显然会占用大量的内存。

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

1. 设置预估行高

我们知道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;  
}  
2. 预先计算并缓存行高

自从iOS8.0之后,TableView的数据源的调用时序也发生了变化,下图左边为iOS7.0及之前的时序,右边为iOS8.0及之后的时序:

9073B542-D111-4D3C-A89F-B8AB87B4EF27.png

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

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

1. 减少图层层级数

当我们自定义了某个cell,并在cell上添加大量的系统控件后,在创建该cell对象时系统会调用底层接口进行绘制,大量的添加操作会消耗很大的资源同时会影响渲染的性能。

2. 异步绘制

解决因图层层级多造成的性能问题,我们可以通过重写drawReact:方法,调用Core Graphics框架中的API进行异步绘制,提高效率。drawRect:本身是异步的。另外drawRect:中大量的绘制操作也会造成内存的增长,可以使用CAShapeLayer来代替。

3. 减少多于的绘制操作

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

4. 图片加载时机选择

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

5. 避免动态添加图层

在cell中应该尽量避免动态创建图层。在初始化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的图层
  • 文本(任何种类,包括UILabelCATextLayerCore Text等)
  • 使用CGContextdrawReact:方法中绘制
    上述情况均会造成离屛渲染。

什么是离屛渲染?我们知道iOS底层的渲染框架使用的是OpenGL ESOpenGL中,GPU渲染屏幕方式有两种:当前屏幕渲染(On-Screen Rendering)和离屛渲染(Off-Screen Rendering)。它们的区别是当前屏幕渲染操作是在当前显示的屏幕缓冲区完成,而离屛渲染会在另外一个新开辟的缓冲区完成渲染操作。开启离屛渲染的代价就是需要新开辟一块新的缓冲区,在渲染的过程中还会多次的切换上下文,这些都是很消耗性能的。

7. 图片圆角优化
  • 使用贝塞尔曲线 + 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;  
}  
8. 图片阴影优化
- (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;  
}  

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

在我们的项目开发中列表视图的应用很多,有时数据比较多的时候我们不可能一次加载所有数据,这样子会导致内存的暴涨,同时用户不一定会浏览完所有的信息,造成资源浪费。这时我们可以通过分屏加载来解决这个问题,比如第一次加载10条数据,当我向上滑动列表的时候通常我们会再次去请求数据接口获取下一个10条数据。这个时候如果我们不做任何的处理,那么我们会发现每次划过10条数据的时候列表都需要停顿一下,等待数据加载。这样子我们的列表就表现的不是很流畅了,怎么解决这个问题呢?

提前异步预加载数据!第一次加载完10条数据之后我可以再预先加载下10条数据,当划过第10条数据时,我再请求下下10条数据。这样子我们的列表就表现的很流畅了。

优化五、滑动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:代理方法中绑定数据

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

推荐阅读更多精彩内容