再了解 iOS 离屏渲染之前,我们先看一下 iOS 渲染机制
CPU将计算好的需要显示的内容提交给GPU,GPU渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照Vsync(垂直脉冲)信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器进行显示。
为什么产生离屏渲染?
产生的原因主要有两方面:
1、在VSync(垂直脉冲)信号作用下,视频控制器每隔16.67ms就会去帧缓冲区(当前屏幕缓冲区)读取渲染后的数据;但是有些效果被认为不能直接呈现于屏幕前,而需要在别的地方做额外的处理,进行预合成。比如图层属性的混合体再没有预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前必须在一个屏幕外上下文中被渲染(不论CPU还是GPU)。
2、有些视图渲染后的纹理需要被多次复用,但屏幕内的渲染缓冲区是实时更新的,所以需要通过开辟屏幕外的渲染缓冲区,将视图的内容渲染成纹理并缓存,然后再需要的时候在调入屏幕缓冲区,可以避免多次渲染的开销。
相比于当前屏幕渲染,离屏渲染的代价是很高的,主要体现在两个方面:
1、创建新缓冲区:要想进行离屏渲染,首先要创建一个新的缓冲区。
2、上下文切换:离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
特殊的离屏渲染:CPU渲染
如果重写了drawRect方法,并且使用任何Core Graphics 的技术进行了绘制操作,就涉及到CPU渲染。整个渲染过程由CPU在App内同步完成,渲染得到的bitmap(位图)最后再交由GPU用于显示。
CoreGraphic通常是线程安全的,所以可以进行一步绘制,显示的时候再回主线程,一个简单异步绘制内容如下:
- (void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
如何检测项目里面的离屏渲染
模拟器:在工作栏上面的Debug -> Color Off-Screen Rendered
真机在:工作栏上面的Debug -> View Debugging -> Rendering -> Color Off-Screen Rendered Yellow
开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
离屏渲染触发方式
设置了以下属性时,都会触发离屏绘制:
shouldRasterize(光栅化)
masks(遮罩)
shadows(阴影)
edge antialiasing(抗锯齿)
group opacity(不透明)
cornerRadius+maskToBounds(圆角)
需要注意的是,如果 shouldRasterize 被设置成YES,在触发离屏绘制的同时,会将光栅化后的内容缓存起来,如果对应的 layer 及其 sublayers 没有发生改变,在下一帧的时候可以直接复用。这将在很大程度上提升渲染性能。
而其它属性如果是开启的,就不会有缓存,离屏绘制会在每一帧都发生。
Instruments
Instruments 的 Core Animation 工具中有几个和离屏渲染相关的检查选项:
Color Offscreen-Rendered Yellow:开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。
Color Hits Green and Misses Red:如果shouldRasterize被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了
iOS 离屏渲染解决参考方案:
技巧:
设置圆角:
方法1、如果只是单纯给图片设置圆角时,把背景设置成透明,可以避免离屏渲染。
方法2、用一个背景透明的圆形图片改在图片上
设置阴影
给阴影设置路径:指定一个与边界相同的简单路径后离屏渲染特征消失,帧率恢复正常。
imageViewLayer.layer.shadowColor = UIColor.blackColor().CGColor
imageViewLayer.layer.shadowOpacity = 1.0 //此参数默认为0,即阴影不显示
imageViewLayer.layer.shadowRadius = 2.0 //给阴影加上圆角,对性能无明显影响
imageViewLayer.layer.shadowOffset = CGSize(width: 5, height: 5)
//设定路径:与视图的边界相同
let path = UIBezierPath(rect: cell.imageView.bounds)
imageViewLayer.layer.shadowPath = path.CGPath//路径默认为 nil
参考文章:
https://www.jianshu.com/p/aa8dc1a61c91