关于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(组不透明)
实际项目中主要由Core Graphics API(核心绘图)的使用导致。
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),一个用于保持界面流畅的库,以后有时间我再详细的介绍和分析一下这个库。