关于iOS离屏渲染的深入研究

1.离屏渲染是什么

首先我们要知道图像渲染的基本原理:由CPU计算好显示内容,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 HSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

如果在当前用于显示的屏幕缓冲区中进行渲染操作,那就是当前屏幕渲染,如果是在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作,那就是离屏渲染

2.如何触发离屏渲染

我们经常看到,圆角会触发离屏渲染。以最近做的IM项目举例,在聊天列表页,每个cell的头像都得切圆角,如果使用以下方式:

1 imageView.backgroundColor = [UIColor whiteColor];
2 imageView.layer.cornerRadius = 25;
3 imageView.layer.masksToBounds = YES;

当数据量较大的时候,快速滑动必然会触发卡顿,我们可以在xcode中打开模拟器的 debug -> 选取 color Offscreen-Rendered,进行离屏渲染检测:

这里我们通过一个demo来进行测试:

1 UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
2 [self.view addSubview:view1];
3 // 背景色
4 view1.backgroundColor = UIColor.redColor;
5 // 边框
6 view1.layer.borderWidth = 2;
7 view1.layer.borderColor = UIColor.blackColor.CGColor;
8 // 圆角
9 view1.layer.cornerRadius = 50;

 

 

 我们可以看到,这里虽然设置了cornerRadius,但是并没有触发离屏渲染。当我们开启layer.masksToBounds或者clipsToBounds时,依然不会触发。

但是如果我们在这个红色的view1上,再添加一个view2,并且将view1开启maskToBounds:

 1    UIView *view1 = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100.0, 100.0)];
 2     [self.view addSubview:view1];
 3     // 背景色
 4     view1.backgroundColor = UIColor.redColor;
 5     // 边框
 6     view1.layer.borderWidth = 2;
 7     view1.layer.borderColor = UIColor.blackColor.CGColor;
 8     // 圆角
 9     view1.layer.cornerRadius = 50;
10     view1.layer.masksToBounds = YES;
11     
12     UIView *view2 = [[UIView alloc] initWithFrame:CGRectMake(20, 20, 20, 20)];
13     [view1 addSubview:view2];
14     // 背景色
15     view2.backgroundColor = UIColor.greenColor;

离屏渲染出现了:

 

 

 3.离屏渲染出现的原因

  • CALayer产生GPU离屏渲染

首先来看一下CALayer的层次结构,CALayer由背景色backgroundColor、内容contents、边缘borderWidth&borderColor构成,如下图所示:

计算机图形学里讲过一个词“油画家算法”,即先绘制场景中的离观察者较远的物体,再绘制较近的物体。而图层的绘制基本遵循这一原则。

当我们设置了cornerRadius以及masksToBounds进行圆角+裁剪时,masksToBounds裁剪属性会应用到所有的图层,本来我们的绘制过程,应该是绘制一个图层丢弃一个图层,但现在contents中有了内容,那就需要在离屏缓冲区中保存,等待圆角+裁剪处理,即引发了 离屏渲染 。

除了cornerRadius+masksToBounds以外,还有以下几种情况也会触发离屏渲染:

  • 开启shadows(阴影)
  • 毛玻璃效果
  • mask(遮罩)
  • allowsGroupOpacity(组不透明)

 

当 CALayer 使用圆角,阴影,遮罩等属性的的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中渲染,则过程中需要进行离屏渲染。
实际项目中主要由Core Graphics API(核心绘图)的使用导致。
 
 4.离屏渲染的优化方案
实际开发中,当视图内容是静态不变时,可以通过设置 shouldRasterize(光栅化)为YES,当我们开启光栅化后,会在首次产生一个位图缓存,当再次使用时候就会复用这个缓存。但是如果图层发生改变的时候就会重新产生位图缓存,所以这个功能一般不能用于UITableViewCell中。
最彻底的解决办法,就是把需要显示的图形在后台线程绘制为图片,避免使用圆角、阴影、遮罩等属性。
  • 例如使用CAShapeLayer和UIBezierPath设置圆角
 1 UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
 2 
 3 imageView.image = [UIImage imageNamed:@"myImg"];
 4 
 5 UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
 6 
 7 CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
 8 
 9 //设置大小
10 
11 maskLayer.frame = imageView.bounds;
12 
13 //设置图形样子
14 
15 maskLayer.path = maskPath.CGPath;
16 
17 imageView.layer.mask = maskLayer;
18 
19 [self.view addSubview:imageView];

感兴趣的同学可以阅读一下Google开源的AsyncDisplayKit(现在迁移到Texture),一个用于保持界面流畅的库,以后有时间我再详细的介绍和分析一下这个库。

 

posted @ 2019-07-08 14:37  黑暗的咏叹  阅读(639)  评论(0编辑  收藏  举报