UITableView性能优化|青训营笔记

571 阅读14分钟

UITableView性能优化|青训营笔记

这是我参与「第四届青训营 」笔记创作活动的第4天

1.关于cell

1.1cell复用池机制

1.2willDisplayCell:forRowAtIndexPath:代理方法中绑定数据

  • cellForRowAtIndexPath的数据源方法中包含了大量的布局、绘制相关操作,我们应当在其中尽量的简化自己的操作,在其中创建cell就好了,cell绑定数据的操作可以放置在cell即将展示时的代理方法里去做
  • willDisplayCell:forRowAtIndePath:的方法中cell已经被创建,在其中进行cell的数据绑定也不会有任何异常

1.3设置cell的预估高度,预先缓存cell的动态行高

关于cellForRow和heightForRow的一些说明

  • 我们认为的两者的调用顺序:理想上我们是会认为UITableView会先调用前者,再调用后者,因为这和我们创建控件的思路是一样的,先创建它,再设置它的布局。
  • 实际上两者的调用关系:UITableView是继承自UIScrollView的,需要先确定它的contentSize及每个Cell的位置,然后才会把重用的Cell放置到对应的位置。所以事实上,UITableView的回调顺序是先多次调用tableView:heightForRowAtIndexPath:以确定contentSize及Cell的位置,然后才会调用tableView:cellForRowAtIndexPath:,从而来显示在当前屏幕的Cell。
  • 举个例子:如果现在要显示100个Cell,当前屏幕显示5个。那么刷新(reload)UITableView时,UITableView会先调用100次tableView:heightForRowAtIndexPath:方法,然后调用5次tableView:cellForRowAtIndexPath:方法;滚动屏幕时,每当Cell滚入屏幕,都会调用一次tableView:heightForRowAtIndexPath:tableView:cellForRowAtIndexPath:方法。

所以为了避免每次都在heightForRow里面计算来损耗性能,可以先缓存cell的动态行高,在heigntForRow里面返回欲先缓存的高度就行了。

方法一:单独创建一个数组用来保存cell的高度

  • 每次滑动table,出现cell的时候,出现新的cell就会调用- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath方法,并在里面进行cell的高度计算。可以预先将cell的高度计算出来,然后在此方法里面返回一个具体的数值,就可以增加table的滚动流畅行
  • cell的高度是由cell的数据决定的,而cell的数据可以在数据源数组得到,所以可以预先就拿到数据源数组,计算出里面所有的高度,存放在一个数组里面
  • 为了避免一下拿到所有的数据然后去计算,造成短时间内计算量过大,所以应当动态的拿到数据源数组里面的去计算。具体做法是在高度的代理方法里进行逻辑判断,如果cell的行数大于高度数组的height,那么就计算高度,并加入高度数组。但是这样的缺点是会上滑刷出新的数据的时候并不会增加滚动的流畅行,上滑的时候才会增加流畅性。

缓存cell高度参考文章 通过文本计算label高度

方法二:将cell的高度放置在cell的model里面

  • 1.为cellModel添加多个可变高度的属性和一个不变高度的属性。可变高度的属性是用来保存cell中高度变化的view的高度,不可变高度属性用来保存cell中固定的view的高度总和

  • 2.重写model的可变数据,例如文本或者图片的数量,在对其进行设置的时候,计算出需要的高度,并将其对应的高度属性进行赋值。

  • 3.在heightForRow代理方法里面直接返回model的高度属性的相加的总和

  • 优点:

    • 相较于方法一,可以实时得到最新的cell的高度,即使是在本地改变了数据,也能得到适应的cell高度
    • 如果利用分页的方式去获取table,那么table的数据源数组不会一下特别多,也不会瞬间计算大量的数据

1.4减少视图空间,简化层级关系

1.5拒绝动态添加控件

所有在cell的控件应当一次性绘制完成,然后利用视图的hideen属性来设置它的视图层级

2.关于滑动

2.1滑动tableView时按需加载内容

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

scrollViewWillEndDragging: withVelocity: targetContentoffset:

具体实现代码,其中targetContentOffset 是TableView减速到停止的地方, velocity 表示速度向量。

- (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+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];
  }
}
if (needLoadArr.count>0&&[needLoadArr indexOfObject:indexPath]==NSNotFound) {
  [cell clear];
   return;
}

2.2滑动不加载

滑动不加载是告诉TableView,在滑动的时候不去网络请求加载cell中的图片,等到滑动停止以后再去网络加载图片。这样在滑动的时候流畅行会更高,也不会产生table上不显示图片的情况。主要会运用到scrollerView的两个代理方法:

注意:拖拽结束不等于滑动结束

//滑动结束
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView; 
//拖拽结束
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate;

1.在model中的.h文件中添加一个BOOL属性,是否添加网络图片

/**是否加载网络图片,如果是YES就在cellForRow里面加载图片*/
@property(nonatomic,assign)BOOL isLoad;
  • 2.在自定义cell的.h文件中声明一个方法

    //加载model的图片
    -(void)setImageWithModel:(HomeViewModel *)model;
    

    在cell的.m文件中实现:

    //如果传入的model为nil,说明是在滑动中,就设置一个占位图,否则就是滑动结束,就加载图片
    -(void)setImageWithModel:(HomeViewModel *)model
    {
      if (model == nil) {
          [self.iconImgView setImage:[UIImage imageNamed:@"约单头像加载"]];
    }else{
    [self.iconImgView wxl_setImageWithURL:[OSSImageKit scale_w80_h80:model.headPhotoUrl] placeholder:@"约单头像加载" completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
      [_iconImgView setContentScaleFactor:[[UIScreen mainScreen] scale]];
       _iconImgView.contentMode =  UIViewContentModeScaleAspectFill;
       _iconImgView.clipsToBounds  = YES;
       
     //设置model的是否加载网络图片属性为YES
      [model setIsLoad:YES];
    }];
    }
    
 
- 3.在controller里面请求到数据并建立model的时候设置model的是否加载图片属性:
​
```objc
for (NSDictionary *homeDic in dic[@"data"][@"lease"]) {
 
          HomeViewModel *model =[[HomeViewModel alloc] init];
 
          [model setValuesForKeysWithDictionary:homeDic];
 
          model.isLoad = NO;//装到数组中,先不下载
 
          [self.dataSource addObject:model];
           
  //此方法是为了第一次显示table的数据,以避免因为willDisplay方法里面的判断逻辑导致table的数据只在拖拽的时候才会去加载数据
          dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
          //让tableView准备好后,再显示
              [self loadShowCells];
          });
      }
  • 4.在willDisplayCell里面为cell绑定数据,判断是加载图片还是加载占位图:

    在cellForRow里面只进行cell的创建等必要逻辑操作

    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
    {
       HomePublishSkillViewCell * cell = [tableView dequeueReusableCellWithIdentifier:homePublishIdentifier];
     
         HomeViewModel * model = self.dataSource[indexPath.row];
     
       return cell;
    }
    

    在willDisplayCell方法里判断是否从网络加载数据

    - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath{
        //如果model里面是要加载图片,则加载图片,否则就加载占位图
          if (model.isLoad) {
               [cell setImageWithModel:model];
          }else{
               [cell setImageWithModel:nil];
          }
    }
    
  • 5.设置scollview的两个代理方法,监听滚动事件

    拖拽后,停止滚动时加载图片

    - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    ZZLog(@"----%d",decelerate);
    	
      //停止滑动了,加载图片
      if (!decelerate) {
           [self loadShowCells];
       }
    }
    

    停止滚动后即在图片

    -(void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{
    
                 [self loadShowCells];
    }
    
  • 6.记载cell中的图像

    -(void)loadShowCells{
      //获取当前正在显示的tableView的indexPath.row
          NSArray * array = [self.mainTableView indexPathsForVisibleRows];
      for (NSIndexPath *indexPath in array) {
    
       HomePublishSkillViewCell * cell = (HomePublishSkillViewCell *)[self.mainTableView cellForRowAtIndexPath:indexPath];
            HomeViewModel * model = self.dataSource[indexPath.row];
            [cell setImageWithModel:model];
        
      }
    }
    

参考文章1 参考文章2 手把手带你优化一个滚动流畅的cell

2.3 动态预加载

采用按页加载的机制,根据滑动动态的预先加载下一页的数据,具体技巧的使用请参考传送门

3.渲染

3.1减少离屏渲染

3.1.1应当了解的几个关于离屏渲染知识点

3.1.1.1卡顿原因:

通常来说,计算机系统中 CPU、GPU、显示器是协同工作的:CPU 负责计算显示的内容,如视图的创建、布局计算、图片解码、文本绘制等,完成后提交到 GPU;GPU 负责对 CPU 提交的内容进行变换、合成、渲染,完成后将渲染结果放入帧缓冲区;视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

3.1.1.2什么是当前屏幕渲染:

是指GPU的渲染操作是在当前用于显示的屏幕缓冲区进行。

3.1.1.3什么是离屏渲染以及为什么会影响性能
  • 离屏渲染:

    • 定义:指的是GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作
    • 作用:有些效果被认为不能直接呈现于屏幕,而需要在别的地方做额外的处理预合成
  • 对性能的影响:离屏渲染会创建新的缓冲区,且在离屏渲染的整个过程,需要多次切换上下文环境。这些操作的开销很大(涉及到 OpenGL 的 pipelines 和 barrier 等),尤其是当有大量离屏渲染的情况时。

3.1.1.4哪些情况会造成离屏渲染
  • 为图层设置遮罩(layer.mask)
  • 设置图层的 layer.masksToBounds或view.clipsToBounds属性为YES,并且在图层上添加图片、背景颜色、绘制内容或者有图像信息的子视图时,加上对图层设置圆角或者剪裁时都会触发离屏渲染
  • 设置图层的 layer.allowsGroupOpacity的属性为YES或者layer.opacity小于1.0
  • 设置图层阴影(layer.shadow)
  • 设置图层的开启光栅化 :设置layer.shouldRasterize的属性为YES
  • 为图层设置圆角 layer.cornerRadius,
  • 为图层设置抗锯齿性:layer.edgeAntialiasingMask,
  • layer.allowsAntialiasing的图层
  • 文本(任何种类,包括UILabel、CATextLayer、Core Text等)
  • 使用CGContext在drawReact:方法中绘制的大部分情况下会导致离屏渲染

3.1.2解决方案

离屏渲染发生在图层混合的时候,尽可能避免离屏渲染在高频率显示的地方出现,可以就昂某些图形绘制交由CPU完成,给GPU减负,降低性能峰值

3.1.2.1:圆角优化
  • 方案一:使用中间透明,四个角有背景色的图片代替圆角效果

  • 方案二:使用CAShapeLayer配合UIBezierPath画圆角:

    - (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;  
    }  
    
  • 方案三:使用Core Graphics框架配合UIBezierPath画圆角

    - (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(属于CoreAnimation)与贝塞尔曲线可以实现不在view的drawRect(继承于CoreGraphics走的是CPU,消耗的性能较大)方法中画出一些想要的图形
    • CAShapeLayer动画渲染直接提交到手机的GPU当中,相较于view的drawRect方法使用CPU渲染而言,其效率极高,能大大优化内存使用情况。
3.1.2.2阴影优化

利用贝塞尔曲线,得到ShadowPath,使用其提前告诉CoreAnimation待渲染图层的形状

(layer.shadowPath = [UIBezierPath bezierPathWithRect:layer.bounds]);

- (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;  
}
3.1.2.3设置 layer.opaque = YES,减少复杂图层合成
  • 关于opaque

    • UIView的默认值是YES,但UIButton等子类的默认值都是NO
    • opaque 表示当前UIView是否不透明,不过搞笑的是事实上 它却决定不了当前UIView是不是不透明
  • 为什么设置opaque属性为YES

    当有图层重叠后,如果将opaque属性设置为YES后,GPU将不会考虑这个图层的下方(注意此处的下方和我们从看模拟器传统的视图上下方不一样)的任何东西,因为那些东西都由上方的图层遮住了,因此GPU将不会做任何的计算合成(view重叠部分即默认opaque属性为NO时会计算的合成部分),这节省了GPU相当大的计算量

  • 注意点:

    当opaque属性被设为YES时,GPU就不会再利用图层颜色合成公式去合成真正的色值。因此, 如果opaque被设置成YES,而对应UIView的alpha属性不为1.0的时候,就会有不可预料的情况发生,这一点苹果爸爸在官方文档中有明确的说明。

若想知道更详细的关于opaque,请阅读参考文章

3.1.2.4 关闭cell的光栅化 shouldRasterize = NO

  • 光栅化:

    • 定义:将图转化为一个个栅格组成的图象。

    • 特点:每个元素对应帧缓冲区中的一像素

    • 用途:在其他属性触发离屏渲染的同时,会将首次光栅化后创建一个位图(包括各种阴影遮罩等效果),并缓存起来,如果对应的layer及其sublayers没有发生改变即图层未发生改变,在下一帧的时候可以直接复用缓存。,从而减少渲染的频度(不是矢量图)。

      即:相当于光栅化是把GPU的操作转到CPU上了,生成位图缓存,直接读取复用

    • 使用场景:最好用于图层较多的静态内容的图形。而且产生的位图缓存的大小是有限制的,一般是2.5个屏幕尺寸。在100ms之内不使用这个缓存,缓存也会被删除。所以我们要根据使用场景而定。

    • 使用技巧:可以开启“Color Hits Green and Misses Red”来检查该场景下光栅化操作是否是一个好的选择。绿色表示缓存被复用,红色表示缓存在被重复创建。

  • 为什么关闭cell的光栅化:

    cell的重绘很频繁,又因为其内容不断变化,所以cell就需要不断的重绘,如果此时设置了cell.layer可光栅化。则会造成大量的离屏渲染,降低图形性能。

光栅化参考文章

3.1.2.5 尽量使用不包含透明(alpha)通道的图片资源

展示半透明的view的时候,设备会讲当前图层和背景图层进行alpha叠加,这很耗费性能,如果动画中每一帧都做叠加,性能损耗会很大。

建议:

  • UIView、UIImageView使用时避免半透明

  • 控件贴图的时候不使用带alpha通道的图片,可以要求视觉那边给出的图片不带alpha通道,怎么做看此回答 如何去除图片的alpha通道

  • 如果要必须要使用Alpha,则主动去使用Alpha,提前与背景色合成不含alpha的图片。满足要求:针对同一场景图片合成只需要一次,一次合成,长期使用

    <注>:关于主动使用alpha现在还不是很了解,也不是很清楚,网上没搜到,后面有时间去实践

3.1.2.6 尽量设置layer的大小值为整形值

离屏渲染优化参考文章

2.异步渲染

我们在Cell上添加系统控件的时候,实质上系统都需要调用底层的接口进行绘制,当我们大量添加控件时,对资源的开销也会很大,所以我们可以索性直接绘制,提高效率,并且将绘制的过程放在子线程异步操作,绘制好之后再在主线程进行UI的操作。在项目 VVeboTableViewDemo 里面,作者把很多cel里面需要显示的内容异步绘制成图片再显示,并实现一个异步绘制的label。也有一个强大的第三方库可以异步绘制的来显示文字控件:YYLabel,它可以像UILabel一摸一样的使用,也可以通过赋值它的textLayout(一个YYTextLayout对象)来显示内容,第二种方式拥有更高的性能。

参考文章

UITableView性能优化

iOS UITableView性能优化

上述两篇很有多相似之处

iOS 保持界面流畅的技巧 此篇作者为YYKit创作者所撰写,建议认真阅读