iOS图形渲染分析
原创文章转载请注明出处,谢谢
这次主要要讲一些关于绘图方面的东西,涉及的方面可能会比较多一点,也是前段时间项目中有不少这方面的知识所以花了点时间研究了一下。文章的内容主要分为两部分,第一部分是关于iOS上一个Chart的第三方库的一些进阶使用;第二部分是在第一部分上研究的iOS上的绘图原理以及性能方面的探索。这篇文章的目的主要是为以后讲关于绘图方面的知识抛砖引玉吧,因为后面的时间会讲一些关于性能监测以及性能优化方面的内容,绘图的性能也占了里面很大的一部分。
第一部分
Charts(MPAndroidChart iOS版)
MPAndroidChart是一个Andriod上被经常使用的绘制曲线的第三方库,danielgindi把这个库使用swift成功移植到了iOS/tvOS/OSX上,它几乎保留的Android的所有功能,同时代码结构上也和Android的非常的相似。个人感觉就是名字取的不好,就只是叫Charts,是在太不直观了。
然后只要讲一些关于这个库的一些进阶使用吧,其实应该算是一些扩展方面的使用。对于那些基础的使用我就不讲了,同学们自行查看Demo应该就可以明白了。好,接下来我们看一张设计图:
pic_1.png其实我们很少情况下可以直接使用Charts的基础功能就完成我们所想要的效果,所以基本上的情况下我们都需要对原始的Charts进行扩展才能满足我们的需求。这里有一点需要注意的是不到万不得已最好永远都不要去改动第三方的库源码,采用继承的方式永远要优于修改,无论是对于扩展性来言还是以后第三方库的更新等等(在你每次想修改源码之前最好问问自己是不是真的看懂了源码的原理,作者完全没有帮你预留修改的扩张?基本上优秀的第三方库的扩展性是很鲁棒)。
言归正传上图中有三个点需要我们在原有的库上进行扩展才可以完成我们的功能。
关于背景色的渲染
关于背景色的渲染,也就是我们标注的Note:One这部分的内容,Charts的基础功能其实只能做到两个点之间画上背景色,并不能做到像上图那样在同一个区间中分开画上两种颜色,所以很明显我们只能自己去实现这个需求,继承一个新的Chart然后重写它的背景绘制方法,我们来看一张演变的过程图。
pic_2.jpg- 默认状态是Charts可以直接支持的能力;
- 转变状态其实就是计算出每两个坐标区间之间需要分开颜色的子区间,以子区间的坐标进行背景绘制;
- 改进状态就是在转变状态下对绘制范围以及背景渐变做了约束;
实际上从转变状态到改进状态虽然看上去比较简单,实际上还是有许多工作需要完成,这里介绍一下我采用的方案。
转变状态相对比较简单,就是计算每两个坐标的子区间然后绘制一个大Rect就可以了。
改进状态相对比较复杂,我们需要计算出个单位区间的坐标,每一个单位区间其实都是一个梯形,也就是说我们只需要通过画一个三角形和一个矩形就可以画出一个单位区间,以此类推我们就可以绘制出整个背景,其实就是基本一个线性公式.值得注意的一点是关于渐变色的渲染我们都应该从顶部至底部的方向保证不会出现颜色上的差异
y = kx + b;
关于第二行的X坐标以及底部的标注数据
关于第二行的X坐标以及底部的标注数据,也就是我们标注的Note:Two和Note:Three这两部分的内容。
当然以来基础额Charts肯定无法完成我们的需求,实现思路其实和绘制背景类似,我们重写绘制X坐标的函数,在原来的基础上利用获得的坐标值在下方再添加一行我们需要的内容。底部的标注数据的绘制也是同一个原理,在特定的位置在添加标注数据的贴图就可以完成我们的需求。
关于Charts这部分的使用其实每个人面对的问题可能都是不一样的,每个设计都是不一样的,所以我们无法涵盖所有问题的解决方案各自是什么,当你发现这些库的基础功能无法满足你的需要时,通常遇到这种问题我的建议是两个:
-
类似于绘图这类的第三方库它都会有很好的扩展性,你要做的就是去读源码,摸清它的原理在这个基础上去继承扩展你需要的功能,最要永远不要去做改动源码这种事。
-
换位思考,想想作者如果是你的话,你会怎么去实现这个功能,把思路理清楚然后在动手,不要边做边想会比较好。
第二部分
这一部分内容我也是网上看了不少别人的博客后一些理解,之前对于这块也不是很了解,这次是理论知识而且比较粗浅,下次会分享一些实际性能优化方案,和更深层次的一些东西。
UIView和CALayer
关于UIView和CALayer的关系这里大致讲主要的几点:
-
每个UIView都包含一个CALayer在背后提供内容的绘制和显示(一个layer可能包含多个子layer),并且UIView的bound,frame都由内部的Layer所提供。两者都有树状层级结构,layer内部有SubLayers,View内部有SubViews.但是Layer比View多了个AnchorPoint,AnchorPoint相比Postion的区别在于position点是相对suerLayer的,anchorPoint点是相对layer的,两者都是中心点的位置,只是相对参照物不同罢了。
-
在View显示的时候,UIView做为Layer的CALayerDelegate,View的显示内容由内部的CALayer的display。
-
View可以接受并处理事件,而Layer不可以,因为UIKit使用UIResponder作为响应对象。
-
layer内部维护着三分layer tree,分别是presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree(渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是Layer的presentLayer的属性值,而最终展示在界面上的其实是提供View的modelLayer。
界面的绘制和渲染
UIView是如何到显示的屏幕上的。
这件事要从RunLoop开始,RunLoop是一个60fps的回调,也就是说每16.7ms绘制一次屏幕,也就是我们需要在这个时间内完成view的缓冲区创建,view内容的绘制这些是CPU的工作;然后把缓冲区交给GPU渲染,这里包括了多个View的拼接(Compositing),纹理的渲染(Texture)等等,最后Display到屏幕上。但是如果你在16.7ms内做的事情太多,导致CPU,GPU无法在指定时间内完成指定的工作,那么就会出现卡顿现象,也就是丢帧。
60fps是Apple给出的最佳帧率,但是实际中我们如果能保证帧率可以稳定到30fps就能保证不会有卡顿的现象,60fps更多用在游戏上。所以如果你的应用能够保证33.4ms绘制一次屏幕,基本上就不会卡了。
总的来说,UIView从Draw到Render的过程有如下几步:
-
每一个UIView都有一个layer,每一个layer都有个content,这个content指向的是一块缓存,叫做backing store。
-
UIView的绘制和渲染是两个过程,当UIView被绘制时,CPU执行drawRect,通过context将数据写入backing store。
-
当backing store写完后,通过render server交给GPU去渲染,将backing store中的bitmap数据显示在屏幕上。
下图就是从CPU到GPU的过程
pic_5.jpeg其实说到底CPU就是做绘制的操作把内容放到缓存里,GPU负责从缓存里读取数据然后渲染到屏幕上。
就如同下图的所示
pic_4.jpeg整个过程也就是一件事:CPU将准备好的bitmap放到RAM里,GPU去搬这快内存到VRAM中处理。
而这个过程GPU所能承受的极限大概在16.7ms完成一帧的处理,所以最开始提到的60fps其实就是GPU能处理的最高频率。
因此,GPU的挑战有两个:
- 将数据从RAM搬到VRAM中
- 将Texture渲染到屏幕上
这两个中瓶颈基本在第二点上。渲染Texture基本要处理这么几个问题:
合成(Compositing):
Compositing是指将多个纹理拼到一起的过程,对应UIKit,是指处理多个view合到一起的情况(drawRect只有当addsubview情况下才会触发)
[self.view addsubview:subview]
如果view之间没有叠加,那么GPU只需要做普通渲染即可。 如果多个view之间有叠加部分,GPU需要做blending。
尺寸(Size):
这个问题,主要是处理image带来的,假如内存里有一张400x400的图片,要放到100x100的imageview里,如果不做任何处理,直接丢进去,问题就大了,这意味着,GPU需要对大图进行缩放到小的区域显示,需要做像素点的sampling,这种smapling的代价很高,又需要兼顾pixel alignment。计算量会飙升。
离屏渲染(Offscreen Rendering And Mask):
我们来看一下关于iOS中图形绘制框架的大致结构
pic_3.jpeg
UIKit是iOS中用来管理用户图形交互的框架,但是UIKit本身构建在CoreAnimation框架之上,CoreAnimation分成了两部分OpenGL ES和Core Graphics,OpenGL ES是直接调用底层的GPU进行渲染;Core Graphics是一个基于CPU的绘制引擎;
我们平时所说的硬件加速其实都是指OpenGL,Core Animation/UIKit基于GPU之上对计算机图形合成以及绘制的实现,由于CPU是渲染能力要低于GPU,所以当采用CPU绘制时动画时会有明显的卡顿。
但是其中的有些绘制会产生离屏渲染,额外增加GPU以及CPU的绘制渲染。
OpenGL中,GPU屏幕渲染有以下两种方式:
-
On-Screen Rendering即当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
-
Off-Screen Rendering即离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。
离屏渲染的代价主要包括两方面内容:
- 创建新的缓冲区
- 上下文的切换,离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上有需要将上下文环境从离屏切换到当前屏幕。而上下文环境的切换是要付出很大代价的。
为什么需要离屏渲染?
目的在于当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,即当主屏的还没有绘制好的时候,所以就需要屏幕外渲染,最后当主屏已经绘制完成的时候,再将离屏的内容转移至主屏上。
离屏渲染的触发方式:
- shouldRasterize(光栅化)
- masks(遮罩)
- shadows(阴影)
- edge antialiasing(抗锯齿)
- group opacity(不透明)
上述的一些属性设置都会产生离屏渲染的问题,大大降低GPU的渲染性能。
CPU渲染:
以上所说的都是离屏渲染发生在OpenGL SE也就是GPU中,但是CPU也会发生特殊的渲染,我们的CPU渲染,也就是我们使用Core Graphics的时候,但是要注意的一点的是只有在我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地 完成,渲染得到的bitmap最后再交由GPU用于显示。
理论上CPU渲染应该不算是标准意义上的离屏渲染,但是由于CPU自身做渲染的性能也不好,所以这种方式也是需要尽量避免的。
分析
所以对于当屏渲染,离屏渲染和CPU渲染的来说,当屏渲染永远是最好的选择,但是考虑到GPU的浮点运算能力要比CPU强,但是由于离屏渲染需要重新开辟缓冲区以及屏幕的上下文切换,所以在离屏渲染和CPU渲染的性能比较上需要根据实际情况作出选择。
总结
其实第一部分的实现当时并没有太多考虑性能上的一些问题,所以具体绘图性能方面的优化,我会在下次的文章中阐述,也是我们App中实际遇到的一些情况以及对应的解决方案。
引用
http://vizlabxt.github.io/blog/2012/10/22/UIView-Rendering/(理解UIView的绘制)