开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第15天,点击查看活动详情
如何触发离屏渲染
首先,我们在demo中创建了三个view,颜色分别为:红色、蓝色、绿色,红色在最下面,再依次是蓝色、绿色。如下图所示:
当我们把中间的蓝色,改成50%透明度后,展示效果如下所示:
我们发现最下面的红色展示效果没有改变,中间原来蓝色和最上面的绿色展示的效果都发生了变化。
我们开启离屏渲染debug开关后发现(Debug->Color Off-screen Rendered),中间原来蓝色和最上面的绿色视图发生了离屏渲染。
离屏渲染原理
为什么只是修改了中间视图的透明度就触发了离屏渲染呢? 离屏渲染,顾名思义为屏幕外的渲染,就是说渲染的结果不会直接呈现到当前屏幕上,而是等待合适的时机才会被展示。 iOS设备并不像现实当中拿出三个不同视图然后叠加起来呈现出一个视图。iOS屏幕其实是由一个个像素点组成,iOS在展示时是没有层级的概念的,通过计算后一帧一帧的渲染到屏幕上的。
整体的渲染流程可以分为三个阶段:
1,CPU阶段:CPU的计算主要是通过CoreAnimation来处理,通过OpenGL ES/Metal将数据传递给GPU。
2,GPU阶段:GPU渲染主要是将接收到的渲染数据进行一系列渲染之后将帧数据存储在帧缓存(Frame Buffer)里面,供视频控制器调用。
3,屏幕显示:视频控制器从帧缓存中获取到帧数据显示在屏幕上。
当GPU无法直接把渲染结果存放到帧缓存中,而是先暂时把中间的一个临时状态存放在另外一个区域。之后再存放到帧缓存,也就是说GPU需要再当前屏幕缓存区以外开辟一个新的缓冲区进行操作。
GPU渲染采用的是“画家算法”,只能一层一层的输出,当一层不能直接生成图片的话就需要额外开辟新的缓冲区来存放这些临时图层直到最后生成了一张完整的图片之后再写入帧缓存里面。
现在回头来看一下上面的demo,当画完中间那层蓝色的view之后却不能直接放到帧缓存里面,因为有透明度,透明度和红色会产生其他颜色;并且蓝色view上面一层绿色的view都是需要经过计算的。最终将计算完成的内容再放到帧缓存里面。也就是因为此才产生了离屏渲染。
离屏渲染的性能问题
离屏渲染需要在屏幕外开辟内存空间,提前使用CPU渲染复杂的视图,保证视频控制器能够及时地从缓存区读到新的渲染结果。它在GPU面临性能瓶颈时,将压力转移一部分给比较空闲的CPU,然而CPU的渲染能力远没有GPU高效,同时也是一种以空间换取时间的策略。
视频控制器要读取离屏渲染的结果,需要把渲染上下文从当前屏幕缓存区切到屏幕外缓存区,当要显示非离屏渲染视图的时候又切换回来,,然而不可能在一屏幕上所有的元素都是离屏渲染的,所以视频控制器上下文需要不停的来回切换。而这种上下文切换的代价非常昂贵。 所以离屏渲染会带来各方面的开销,要尽可能的避免。
离屏渲染案例
GroupOpactity
上面的demo就是属于GroupOpactity,当然还有layer。
UIView *testView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 200, 200, 200)];
testView.backgroundColor = [UIColor redColor];
testView.layer.allowsGroupOpacity = YES;
testView.layer.opacity = 0.5;
// 设置子Layer
CATextLayer *subLayer = CATextLayer.layer;
subLayer.frame = CGRectMake(50, 50, 100, 100);
subLayer.backgroundColor = [UIColor greenColor].CGColor;
subLayer.foregroundColor = [UIColor blackColor].CGColor;
subLayer.string = @"subLayer";
subLayer.opacity = 0.5;
[testView.layer addSublayer:subLayer];
[self.view addSubview:testView];
圆角
UIView * roundedView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 200, 200, 200)];
// 设置圆角
roundedView.layer.cornerRadius = 100;
// 设置边框 - 没有影响
// roundedView.layer.borderWidth = 2.0;
// roundedView.layer.borderColor = UIColor.redColor.CGColor;
// 设置裁剪
roundedView.clipsToBounds = YES;
[self.view addSubview:roundedView];
// 设置内容
roundedView.backgroundColor = UIColor.greenColor;
roundedView.layer.contents = (__bridge id)[UIImage imageNamed:@"icon.png"].CGImage;
阴影
UIView * testView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 200, 200, 200)];
// 设置内容
testView.backgroundColor = UIColor.greenColor;
// 设置阴影
testView.layer.shadowColor = [UIColor grayColor].CGColor;
testView.layer.shadowOpacity = 0.8f;
[self.view addSubview:testView];
Mask
UIView * testView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 200, 200, 200)];
// 设置内容
// testView.backgroundColor = UIColor.redColor;
testView.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"icon.png"].CGImage);
// 设置Mask
CALayer * maskLayer = CALayer.layer;
maskLayer.frame = CGRectMake(50, 50, 100, 100);
maskLayer.backgroundColor = [UIColor grayColor].CGColor;
testView.layer.mask = maskLayer;
[self.view addSubview:testView];
毛玻璃
UIView * testView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 200, 200, 200)];
// 设置内容
testView.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"icon.png"].CGImage);
// 设置毛玻璃效果
UIBlurEffect * blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
UIVisualEffectView * visualView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
visualView.frame = CGRectMake(50, 50, 100, 100);
[testView addSubview:visualView];
[self.view addSubview:testView];
shouldRasterize 光栅化
UIView * testView = [[UIView alloc] initWithFrame:CGRectMake([UIScreen mainScreen].bounds.size.width/2-100, 200, 200, 200)];
// 设置内容
// testView.backgroundColor = UIColor.redColor;
testView.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"icon.png"].CGImage);
testView.layer.shouldRasterize = YES;
[self.view addSubview:testView];
优化措施
- AsyncDisplayKit框架是专门针对文字和图片的异步渲染的框架,可以了解一下。
- 对于圆角图片或特殊样式图片可以直接使用切图。如果一定要使用代码实现可以预先使用CoreGraphics为图片裁剪圆角。
- 对于view的圆形边框,如果没有背景色,可以使用cornerRadius。
- 对于阴影,可以使用shadowPath来规避。
- 对于毛玻璃效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果(CIGaussianBlur),并手动管理渲染结果。