1. 渲染框架
iOS
下的渲染框架:
对于一个App
来说,它会使用CoreGraphics
、CoreAnimation
、CoreImage
进行绘制可视化内容。而这些框架都需要通过OpenGL ES / Metal
来调用GPU
进行绘制,才能最终将内容显示到屏幕上
iOS
下的UIKit
框架,就是继承自CoreGraphics
和CoreAnimation
框架,方便开发者的应用。我们可以设置UIKit
的布局和属性,对界面进行绘制。但是显示和动画,就会用到CoreAnimation
框架
CoreAnimation
框架也被称为核心动画编程框架,依赖于OpenGL ES / Metal
进行GPU
的渲染,例如抖音上的一些酷炫特性,都是基于OpenGL ES / Metal
开发出来的
CoreGraphics
是一个高级的绘图引擎,在CPU
上执行。它的作用是在运行时绘制图像,开发者可以使用这个框架完成绘制路径、阴影、图像创建,图像遮罩等功能
CoreImage
框架用来处理运行前创建的图像,例如工程里的图片。该框架拥有一系列的线程的图片处理器,能够对已经存在的图片进行高效的处理。既能在CPU
上执行,也能在GPU
上执行
2. 渲染流程
图像/图像的渲染流程:
首先,在Application
阶段是由CPU
进行处理,CPU
会负责创建视图、计算视图的Frame
、图片解码、绘制纹理等操作,完成后交给GPU
处理
GPU
在第一个阶段,通过顶点着色器确定图形在硬件上的显示位置,通过片元着色器计算每一个像素点的颜色值
之后在第二个阶段进行光栅化,找到图形像素点的范围,然后把像素点的颜色显示上去
显示后来到第三阶段,将数据放入帧缓冲区,由显示系统将帧缓冲区里的数据显示到屏幕上
整体流程如下:
- 经过
CPU
和GPU
把数据处理好,将其放入FrameBuffer
帧缓冲区中,由视频控制度读取帧缓冲区里的数据,通过显示器显示出来
3. 屏幕扫描
视频控制度读取帧缓冲区里的数据,就是通过屏幕扫描的方式完成的。如图:
在进行图像显示的时候,CRK
电子枪会按照图中的方式,从上到下进行逐行的扫描,扫描的过程就是在读取帧缓冲里面的数据。当扫描完成后,显示器就会显示出一帧的画面。随后电子枪又会回到初始位置,准备下一帧的扫描
当电子枪扫描完第一行,换到下一行的时候,显示器就会发出一个水平同步信号(HSync
)。当一帧画面全部显示完成后,电子枪回到初始位置准备下一帧的扫描之前,显示器又会发出一个垂直同步信号(VSync
)
显示器通常都是固定频率来进行刷新的,这个频率就是垂直同步信号产生的频率。在苹果手机上,刷新频率为每秒60
次(60fps
),所以在进行屏幕卡顿优化时,我们通常会以fps
作为衡量指标,它的值越接近60
,说明屏幕的流畅度越高
4. 掉帧
屏幕卡顿的根本原因:掉帧
当收到一个垂直同步信号时,如果CPU
和GPU
没有完成内容的提交,也就是没有把渲染结果放入帧缓冲区,此时当前这一帧的画面就会被丢弃
显示器会等收到下一次垂直同步信号时,再进行显示。但这时屏幕上显示的还是上一帧的画面,这个过程就是我们所说的掉帧
当出现掉帧的情况,fps
就会出现相对的减少,这也是屏幕卡顿的一个根本原因
5. 离屏渲染的定义
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的FrameBuffer
(帧缓冲区),作为像素数据存储区域,然后由显示控制器把帧缓冲区的数据显示到屏幕上。如果有时因为面临一些限制,一些原因,例如:阴影、遮罩Mask
等,GPU
无法把渲染结果直接写入FrameBuffer
,而是先暂时把中间的一个临时状态存储在另外的内存区域,之后再写入FrameBuffer
,那么这个过程被称之为离屏渲染
详细说明:
正常情况,GPU
都是直接将数据写入到帧缓冲区
但有时因为面临一些限制,GPU
无法一次性的将最终显示效果完成,而是需要将中间状态存储到另外开辟的一块内存中
在内存中会对中间状态进行合成,直到合成为最终显示效果后才会存储到帧缓冲区
此时相比正常情况,多出开辟内存空间存储中间状态的步骤,这个过程我们称之为离屏渲染
6. 图层合并
在日常开发中,当需要做图层合并时,就会触发离屏渲染
开发中我们使用的UIView
,它是基于CALayer
的一个封装。UIView
中每一个属性,都会对应CALayer
中的某个属性。CALayer
负责内容显示,而UIView
负责事件响应。我们在设备上看到的内容,其实都是由CALayer
呈现的
CALayer
分为以下三个部分:
backgroundColor
contents
borderWidth / borderColor
当GPU
进行渲染时,它会遵循画家算法:
例如:当屏幕上要显示最后一张图片的内容时,GPU
作为一个画家,它会先画完一座山。但它不能像我们一样,在画完山之后,翻过头来再去画草和树。它只能开辟一块内存空间,将画好山的画布存储到内存中。之后创建新的画布,再去画草。当画完草之后,再创建画布开始画树。当山、草、数都画完,会在内存空间中进行合并,形成最终的图片内容。等合成后的图片内容需要显示时,GPU
才会将其存储在帧缓冲区
上述过程,就是所谓的离屏渲染。这种复杂操作,很有可能因此出现掉帧,导致屏幕卡顿
7. 代码中的离屏渲染
检测离屏渲染的方式:
模拟器:
Debug
→Color Off-screen Renderd
,当离屏渲染的图层高亮为黄色,可能存在性能问题真机:
Debug
→View Debugging
→Rendering
→Color Offscreen-Renderd Yellow
7.1 光栅化
- (void)lgShouldRasterize {
self.lgImageView.layer.shouldRasterize = YES;
}
- 光栅化会触发离屏渲染
光栅化的作用:将图片保存成bitmap
位图形式,进行缓存。再次显示这张图片时,CPU
会直接从缓存中读取位图,然后传递给GPU
。此时GPU
不会再次渲染这部分的图层,减少GPU
的计算量,从而提高性能
但是,光栅化的运用场景很少。因为光栅化要求是一张静态图片,而且它会触发离屏渲染,它的缓存只能保存100毫秒
,慎用
7.2 遮罩Mask
- (void)lgMask {
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(30, 30, self.lgImageView.bounds.size.width, self.lgImageView.bounds.size.height);
layer.backgroundColor = [UIColor redColor].CGColor;
self.lgImageView.layer.mask = layer;
}
- 遮罩
Mask
会触发离屏渲染
遮罩Mask
属于CALayer
类型,是layer
的一个属性。它是一个遮罩层,覆盖在视图layer
的上层,默认是不存在的
在屏幕上的每一个像素点,它都是由当前像素点上多层layer
通过GPU
混合颜色而来的。当我们设置了遮罩layer
,就需要进行图层合并。因为它会增加GPU
的计算复杂度,GPU
无法一次性渲染到位,这时会通过离屏渲染进行处理
7.3 阴影
- (void)lgShadows {
self.lgImageView.layer.shadowColor = [UIColor redColor].CGColor;
self.lgImageView.layer.shadowOffset = CGSizeMake(20, 20);
self.lgImageView.layer.shadowOpacity = 0.2;
self.lgImageView.layer.shadowRadius = 5;
self.lgImageView.layer.masksToBounds = NO;
}
- 阴影会触发离屏渲染
设置阴影后,它会处于视图layer
的下层。GPU
先进行阴影的绘制,然后开辟内存空间存储阴影。再进行本体的绘制,最后进行阴影和本体的合成,从而达到最终的显示效果
阴影的优化:
- (void)lgShadows2 {
self.lgImageView.layer.shadowColor = [UIColor redColor].CGColor;
self.lgImageView.layer.shadowOpacity = 0.2;
self.lgImageView.layer.shadowRadius = 5;
self.lgImageView.layer.masksToBounds = NO;
[self.lgImageView.layer setShadowPath:[UIBezierPath bezierPathWithRect:CGRectMake(0, 0, self.lgImageView.bounds.size.width + 20, self.lgImageView.bounds.size.height + 20)].CGPath];
}
- 使用
ShadowPath
指定layer
阴影效果路径,不会触发离屏渲染
通过ShadowPath
属性,可以预先告知CoreAnimation
阴影的几何形状。它会根据指定路径构建图形阴影,使得阴影可以独立渲染,不需要依赖于layer
的本体,所以不会触发离屏渲染
7.4 抗锯齿
- (void) lgEdgeAnntialiasing {
CGFloat angle = M_PI / 60.0;
[self.lgImageView.layer setTransform:CATransform3DRotate(self.lgImageView.layer.transform, angle, 0.0, 0.0, 1.0)];
self.lgImageView.layer.allowsEdgeAntialiasing = YES;
}
图片开启抗锯齿,不一定会触发离屏渲染,这个和图片的填充模式(contentMode
)有关:
ScaleToFill
:缩放图片,使图片充满容器,会导致图片变形,但不会触发离屏渲染ScaleAspectFit / ScaleAspectFill
:二者都会保证图片比例不变。Fit
使图片全部显示在容器中,比例不符留白。而Fill
会填充整个容器,可能只有部分图片能显示出来。但它们都会触发离屏渲染
在开发中,如果能保证图片和控件大小成比例,将填充模式设置为ScaleToFill
,在性能方面有一定程度的帮助
7.5 不透明
- (void)lgAllowsGroupOpacity {
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 20, 20)];
view.backgroundColor = [UIColor greenColor];
[self.lgImageView addSubview:view];
self.lgImageView.alpha = 0.8;
self.lgImageView.layer.allowsGroupOpacity = YES;
}
allowsGroupOpacity
:允许组不透明,默认为Yes
设置allowsGroupOpacity
属性,不一定会触发离屏渲染:
如果
allowsGroupOpacity
属性为NO
,此时设置父视图透明度,子视图也会跟随透明,这种情况不会触发离屏渲染如果
allowsGroupOpacity
属性为Yes
,但不设置视图透明度(alpha = 1.0
),也不会触发离屏渲染如果
allowsGroupOpacity
属性为Yes
,此时设置父视图透明度,子视图不会跟随透明,这种情况会触发离屏渲染如果父视图中没有子视图,即使允许组不透明被启用,并设置父视图透明度,也不会触发离屏渲染
当允许组不透明被启用,并设置父视图透明度时,GPU
除了渲染子视图之外,还要渲染子视图下面的父视图部分,然后将二者进行合成,所以会触发离屏渲染
7.6 圆角
7.6.1 UIImageView
仅设置圆角:
- (void)lgRadius {
self.lgImageView.layer.cornerRadius = 40;
}
- 仅设置圆角,不会触发离屏渲染
layer
包含backgroundColor
、contents
、border
三个部分。当仅设置圆角时,设置的是contents
层的圆角。系统在iOS9
之后进行了优化,不会触发离屏渲染
设置圆角和边框:
- (void)lgRadius {
self.lgImageView.layer.borderWidth = 5;
self.lgImageView.layer.cornerRadius = 40;
}
- 设置圆角和边框,会触发离屏渲染
同时设置圆角和边框,圆角是对contents
层进行设置,而边框是对border
层进行设置,最终要进行图层合并,所以会触发离屏渲染
设置圆角和背景色:
- (void)lgRadius {
// self.lgImageView.backgroundColor = [UIColor redColor];
self.lgImageView.layer.backgroundColor = [UIColor greenColor].CGColor;
self.lgImageView.layer.cornerRadius = 40;
}
- 设置圆角和背景色,会触发离屏渲染
同时设置圆角和背景色,圆角是对contents
层进行设置,而背景色是对backgroundColor
层进行设置,最终要进行图层合并,所以会触发离屏渲染
在UIImageView
中,使用backgroundColor
或layer.backgroundColor
,都是对backgroundColor
层设置背景色
7.6.2 UILabel
设置圆角,同时使用backgroundColor
设置背景色:
- (void)lgRadius {
self.lgLabel.backgroundColor = [UIColor redColor];
self.lgLabel.layer.cornerRadius = 10;
}
- 设置圆角,同时使用
backgroundColor
设置背景色,不会触发离屏渲染
设置圆角,同时使用layer.backgroundColor
设置背景色:
- (void)lgRadius {
self.lgLabel.layer.backgroundColor = [UIColor greenColor].CGColor;
self.lgLabel.layer.cornerRadius = 10;
}
- 设置圆角,同时使用
layer.backgroundColor
设置背景色,会触发离屏渲染
这里就体现了UILabel
控件的特殊性:
UILabel
的圆角,同样是对contents
层进行设置使用
backgroundColor
设置背景色,也是对contents
层进行设置但使用
layer.backgroundColor
,是对backgroundColor
层进行设置因此后者要进行图层合并,所以会触发离屏渲染
UIImageView
和UILabel
设置背景色的区别:
- (void)lgRadius {
self.lgImageView.backgroundColor = [UIColor redColor];
self.lgImageView.layer.backgroundColor = [UIColor greenColor].CGColor;
self.lgImageView.layer.cornerRadius = 40;
self.lgLabel.backgroundColor = [UIColor redColor];
self.lgLabel.layer.backgroundColor = [UIColor greenColor].CGColor;
self.lgLabel.layer.cornerRadius = 10;
}
- 运行结果,
UIImageView
为绿色,UILabel
为红色
因为UIImageView
都是对backgroundColor
层进行设置,绿色是最后设置的,所以显示为绿色
而UILabel
先对contents
层设置为红色,然后对backgroundColor
层设置为绿色,由于contents
层在backgroundColor
层之上,所以显示为红色
7.6.3 贝塞尔曲线
通过贝塞尔曲线画圆:
- (void)lgBezier {
//开始上下文
UIGraphicsBeginImageContextWithOptions(self.lgImageView.bounds.size, NO, 0.0);
[[UIBezierPath bezierPathWithRoundedRect:self.lgImageView.bounds cornerRadius:100] addClip];
[self.lgImageView drawRect:self.lgImageView.bounds];
//当前上下文
self.lgImageView.image = UIGraphicsGetImageFromCurrentImageContext();
// 结束上下文
UIGraphicsEndImageContext();
}
- 通过贝塞尔曲线画圆,不会触发离屏渲染
使用贝塞尔曲线也是消耗性能的,它虽然不会触发离屏渲染,但使用过程中会进行上下文切换,这样的操作同样是耗时的
7.6.4 优化方案
圆角需求的优化方案,如果列表中头像区域周围背景没有图案,可以采用一张静态图片。中间圆形区域透明,四周预留和背景颜色一致的遮罩
- 例如:列表的背景是白色,图片四周同样留白,中间透明,然后将其覆盖在头像图片至上
7.7 特殊的离屏渲染
根据离屏渲染的定义,如果将GPU
没有把渲染结果直接存储到帧缓冲区的渲染方式都称为离屏渲染,那么还有另一种特殊的离屏渲染方式
-(void)drawRect:(CGRect)rect {
...
}
如果我们重写drawRect
方法,系统就会对当前View
生成一块内存区域,用来等待绘制。这种情况下会开辟一块CGContext
画布,渲染数据会临时存储在CGContext
画布中,也没有直接存储到帧缓冲区
使用任何Core Graphics
技术进行绘制操作,例如图片解码,都会涉及到CPU
渲染。所有CPU
渲染都无法直接绘制到GPU
的帧缓冲区中,需要用到中转的内存区域,通过CPU
渲染得到的bitmap
最后再交由GPU
用于显示
但根据苹果官方的说法,重写drawRect
方法并不是真正意义上的离屏渲染,这种方式使用Xcode
也检测不到离屏渲染。但根据离屏渲染的定义,它属于一种特殊的离屏渲染
8. 离屏渲染的影响与优化
影响:
离屏渲染会开辟内存用来存储中间状态,它会增加内存的消耗
GPU
需要在离屏和当前屏进行反复的上下文切换,这种耗时操作可能会引起掉帧,导致屏幕卡顿
优化:
阴影使用
ShadowPath
指定layer
阴影效果路径圆角使用贝塞尔曲线。如果可以,使用图片遮罩的方案性能最佳
总结
渲染框架:
App
使用CoreGraphics
、CoreAnimation
、CoreImage
进行绘制可视化内容CoreGraphics
是一个高级的绘图引擎,在CPU
上执行。它的作用是在运行时绘制图像,开发者可以使用这个框架完成绘制路径、阴影、图像创建,图像遮罩等功能CoreAnimation
框架也被称为核心动画编程框架,依赖于OpenGL ES / Metal
进行GPU
的渲染CoreImage
框架用来处理运行前创建的图像,例如工程里的图片。该框架拥有一系列的线程的图片处理器,能够对已经存在的图片进行高效的处理。既能在CPU
上执行,也能在GPU
上执行
渲染流程:
在
Application
阶段是由CPU
进行处理,CPU
会负责创建视图、计算视图的Frame
、图片解码、绘制纹理等操作,完成后交给GPU
处理GPU
在第一个阶段,通过顶点着色器确定图形在硬件上的显示位置,通过片元着色器计算每一个像素点的颜色值之后在第二个阶段进行光栅化,找到图形像素点的范围,然后把像素点的颜色显示上去
显示后来到第三阶段,将数据放入帧缓冲区,由显示系统将帧缓冲区里的数据显示到屏幕上
屏幕扫描:
在进行图像显示的时候,
CRK
电子枪会按照图中的方式,从上到下进行逐行的扫描当电子枪扫描完第一行,换到下一行的时候,显示器就会发出一个水平同步信号(
HSync
)当一帧画面全部显示完成后,电子枪回到初始位置准备下一帧的扫描之前,显示器又会发出一个垂直同步信号(
VSync
)
显示器通常都是固定频率来进行刷新的,这个频率就是垂直同步信号产生的频率
在苹果手机上,刷新频率为每秒
60
次(60fps
),所以在进行屏幕卡顿优化时,我们通常会以fps
作为衡量指标,它的值越接近60
,说明屏幕的流畅度越高
掉帧:
当收到一个垂直同步信号时,如果
CPU
和GPU
没有完成内容的提交,也就是没有把渲染结果放入帧缓冲区,此时当前这一帧的画面就会被丢弃显示器会等收到下一次垂直同步信号时,再进行显示。但这时屏幕上显示的还是上一帧的画面,这个过程就是我们所说的掉帧
当出现掉帧的情况,
fps
就会出现相对的减少,这也是屏幕卡顿的一个根本原因
离屏渲染的定义:
正常情况,
GPU
都是直接将数据写入到帧缓冲区但有时因为面临一些限制,
GPU
无法一次性的将最终显示效果完成,而是需要将中间状态存储到另外开辟的一块内存中在内存中会对中间状态进行合成,直到合成为最终显示效果后才会存储到帧缓冲区
此时相比正常情况,多出开辟内存空间存储中间状态的步骤,这个过程我们称之为离屏渲染
图层合并:
开发中我们使用的
UIView
,它是基于CALayer
的一个封装。UIView
中每一个属性,都会对应CALayer
中的某个属性CALayer
负责内容显示UIView
负责事件响应
CALayer
分为以下三个部分:backgroundColor
contents
borderWidth / borderColor
当
GPU
进行渲染时,它会遵循画家算法。将临时状态存储在开辟的内存区域,在内存空间中进行合并,形成最终的图片内容。等合成后的图片内容需要显示时,GPU
才会将其存储在帧缓冲区
代码中的离屏渲染:
光栅化
将图片保存成
bitmap
位图形式,进行缓存缺点:会触发离屏渲染,它的缓存只能保存
100毫秒
遮罩
Mask
遮罩
Mask
会触发离屏渲染遮罩
Mask
属于CALayer
类型,是layer
的一个属性。它是一个遮罩层,覆盖在视图layer
的上层,默认是不存在的
阴影
直接设置
shadow
等属性,会触发离屏渲染使用
ShadowPath
指定layer
阴影效果路径,不会触发离屏渲染
抗锯齿
和图片的填充模式(
contentMode
)有关:ScaleToFill
:缩放图片,使图片充满容器,会导致图片变形,但不会触发离屏渲染ScaleAspectFit / ScaleAspectFill
:二者都会保证图片比例不变,但它们都会触发离屏渲染
不透明
- 如果
allowsGroupOpacity
属性为Yes
,此时设置父视图透明度,子视图不会跟随透明,这种情况会触发离屏渲染
- 如果
圆角
仅设置圆角,只对
contents
层进行设置,不会触发离屏渲染设置圆角的同时,设置边框或背景色,会触发离屏渲染
设置圆角的同时,使用
backgroundColor
设置背景色:在
UIImageView
中,是对backgroundColor
层设置,会触发离屏渲染在
UILabel
中,是对contents
层进行设置,不会触发离屏渲染
贝塞尔曲线
通过贝塞尔曲线画圆,不会触发离屏渲染
但它同样消耗性能,使用过程中会进行上下文切换,这样的操作同样是耗时的
圆角的优化方案
- 采用一张静态图片作为遮罩
特殊的离屏渲染
重写
drawRect
方法,会开辟一块CGContext
画布,渲染数据会临时存储在CGContext
画布中,也没有直接存储到帧缓冲区根据苹果官方的说法,重写
drawRect
方法并不是真正意义上的离屏渲染,这种方式使用Xcode
也检测不到离屏渲染。但根据离屏渲染的定义,它属于一种特殊的离屏渲染
离屏渲染的影响与优化:
影响:
离屏渲染会开辟内存用来存储中间状态,它会增加内存的消耗
GPU
需要在离屏和当前屏进行反复的上下文切换,这种耗时操作可能会引起掉帧,导致屏幕卡顿
优化:
阴影使用
ShadowPath
指定layer
阴影效果路径圆角使用贝塞尔曲线。如果可以,使用图片遮罩的方案性能最佳