视图显示到屏幕的过程:
CPU将显示的视图数据计算好,然后传给GPU渲染,渲染结束后,将数据传递给帧缓冲区,等待显示器绘制在屏幕上。
CPU计算:比如设置frame等,绘制一个layer
GPU渲染:将view的多个layer合成,再合成成像素的数据。GPU 能干的事情比较单一:接收提交的纹理(Texture)和顶点描述(三角形),应用变换(transform)、混合并渲染,生成bitmap.
iOS设置View到其显示在屏幕的过程,可以分成两个阶段:
App进程内:
- 布局:设置整个view tree的hierarachy,和view frame参数
- 生成backing image:比如有些view设置了image background或者是imageview 需要加载图片资源等操作。以及重写了view drawRect方法,使用core graphics绘制生成image等。
- 准备:设置动画的layer参数。
- 提交:将以上所生成的数据,打包序列化传递给render 进程。
渲染进程
- 反序列传递需要绘制的数据,生成view tree
- 将数据合成bitmap,放入帧缓冲区。
影响绘制性能的关键问题
- view tree 中layer越多,视图的shape复杂
视图的frame计算都是需要cpu上计算的,所以视图层次越多越复杂多将消耗更多的cpu资源
GPU渲染两种方式
当前屏幕渲染:GPU的渲染操作用于当前的屏幕缓冲区
离屏渲染:指在GPU渲染之外开辟一个缓冲区,进行渲染操作。
只不过,将不在GPU缓冲渲染的操作都统称为离屏渲染:CPU渲染
比如常见重写drawRect操作就是离屏渲染方式。生成的bitmap传递给GPU用于显示。
设置以下属性的时候,都会触发离屏渲染
设置shouldRasterize(光栅化):每一个元素对应帧缓冲区中的一像素,用于缓冲相同的帧; masks,shadows;设置圆角,渐变等
- 像素过渡绘制
因为一个像素被多个view覆盖,view可能设置了透明度。
像素的计算方式 new =old alpha+ v (1- alpha)
old表示现有的纹理(rgb)的值,v表示添加的view的color,透明度为alpha,new表示叠加v之后的纹理值。
比如:old = (100,100,100),v = (50,50,50),alpha = 0.5,那么new = (75,75,75)
如果太多计算的话,可能在1/60s内无法完成计算,就有可能出现掉帧的现象,掉帧到一定程度就可能出现屏幕卡顿的情况。
- 生成backing image
使用 image named:path方法,加载bundle下面的资源,并解压图片文件,这个过程也需要消耗一定的CPU资源。其他的比如从网上下载的资源,需要显示的时候才进行解压。
Demo
使用view来显示一个矩形,通过以下三种不同的方式来测试每种方法的代价,主要是内存消耗上考虑。
- 重写UIView的drawRect方法
- 调用core graphics 生成bitmap image,通过作为UIImageView的显示内容,然后添加到view的subview
- 通过新建CAShapeLayer添加,将绘制的矩形在CAShapeLayer上面,然后添加到View.layer中
因为这三种结果之间的差异变得更明显,所以我种方法都做了100次
最后,结果如下: - drawRect的方法,内存一个view大概12M,100个1200M
- 和第一种结果一样的
- 使用CAShapeLayer添加的,最后内存也基本上没有变化。
那么,得出结论,第一种和第二种最耗内存,最后那种基本不怎么耗内存。
结果分析
每一个view实例,都默认管理着一个layer。UIView主要负责用户的交互,CALayer就是绘制图层属性和图形数据,用于视图的渲染。CALayer也具有层级关系,和UIView不同的是,它不知道响应链的存在。UIView其实是CALayer一层封装,让用户更加关注视图的处理逻辑,而不是视图的绘制逻辑,但是当你遇到性能问题的时候,你也不得不去了解更加深层的结构。
UIView实现了CALayer的CALayerDelegate,view视图的-drawRect方法背后其实调用了CALayer进行重绘和保存中间图片,当调用以下方法
- (void)displayLayer:(CALayer *)layer;
- (void)drawLayer:(CALayer )layer inContext:(CGContextRef)ctx;
先调第一个方法,如果UIView中,重写了该方法,则直接设置给寄宿图contents,如果没有实现那个,尝试调第二个方法,绘制图image。那么
在后面这个过程中,layer会创建一个合适尺寸的bitimage, 传入上下文的大小size * scale。如果是retina屏的话,1028768*4 = 12M左右,所以内存开销非常大。其中,这个上下文ctx,我们并没显示传递过来,因为UIKit会维护隐式的上下文ctx栈。
回到实验部分,第一种方法,重写了drawRect,在数据准备阶段,生成该view的layer的contents的时候,需要在内存中view一样大的bitmap,并且这部分内存,只要view需要被显示在屏幕上,就一直不被释放。这就是实验中第一种方法,内存一直居高不下的原因。
第二种方法,内存先增加,是因为通过到创建core graphics 生成image,同样需要另外创建的内存,最后保留生成的bitmap在内存中.
最后一种方法,通过添加CAShapeLayer到view.layer的方法,其绘制图形的工作完全交由GPU来完成,所以不需要额外的内存。
参考:
https://objccn.io/issue-3-1/
http://www.jianshu.com/p/a1f575709e7c