绘图
iOS绘图的几种方式
先看一张图:
图中表明了iOS中常见的三种绘图方式:
- OpenGL ES / Metal
- Core Graphics
- Core Animation
其中OpenGL ES和Core Graphics都是偏底层的框架,关于OpenGL的使用可参考我的一个例子:
https://github.com/yunyyyun/Magic
下面简单记录下Core Graphics 和 Core Animation
Core Animation
Core Animation是一个图形渲染和动画的基础库,是一个复合引擎,职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的图层,存储在一个叫图层树的体系之中。关于Core Animation有以下几个特点
Core Animation可以直接用在Max OS X和IOS平台上。 Core Animation的动画执行过程都是在后台操作的,不会阻塞主线程(异步绘制)。 Core Animation是直接作用于CALayer,并非直接作用于UIView。
另外,Core Animation是基于QuartzCore的,你可以在CoreAnimation.h中看到对QuartzCore的import
CoreAnimation使用:
//初始化一个线的图层
CAShapeLayer *lineLayer = [CAShapeLayer layer];
[self.layer addSublayer: lineLayer];
//初始化一个描述的路径
UIBezierPath *linePath = [UIBezierPath bezierPath];
//设置线段开始的点
[linePath moveToPoint:beginPoint];
//设置线段结束的点
//这里也可以添加多个点
[linePath addLineToPoint:endPoint];
//设置图层路径
lineLayer.path = linePath.CGPath;
//设置图层的其他属性
lineLayer.lineWidth = lineWidth;
lineLayer.strokeColor = lineColor.CGColor;
lineLayer.fillColor = [UIColor clearColor].CGColor;
Core Graphics的使用
Core Graphics所有的绘制都是在context上下文中进行的,一个context可以理解成一个画板,一个绘图的步骤就是对画板的操作,例如下面为画线的过程:
CGContextRef ctx = UIGraphicsGetCurrentContext();
[_lineColor setStroke];
CGContextSetLineWidth(ctx, _lineWidth);
CGContextMoveToPoint(ctx, start.x, start.y);
CGContextAddLineToPoint(ctx, end.x, end.y);
CGContextStrokePath(ctx);
整体来说包括这几个步骤:
- 获取图文上下文, 即context
- 设置绘图属性,这些属性包括颜色、size、虚线、渐变等
- 绘制图元,图元包括点、线、矩形、椭圆等
- 设置填充,填充包括2种CGContextStrokePath 表示只绘制线条,CGContextFillPath则是填充闭合的path(圆、矩形)
当然还有其它一些比较好用的api,例如
CGContextSaveGState(ctx); 暂存当前context的配置
CGContextRestoreGState(ctx); 恢复上一次保存的配置
个人总结
一般来说,使用Core Animation的意思是指使用CAShapeLayer结合UIBezierPath, 即直接在layer上绘图,其好处有:
-
CAShapeLayer渲染更快速。因为它使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
-
CAShapeLayer更高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
-
CAShapeLayer不会被图层边界剪裁掉,一个 CAShapeLayer 可以在边界之外绘制。你的图层路径不会像在使用 Core Graphics 的普通 CALayer 一样被剪裁掉。
-
CAShapeLayer不会出现像素化。当你给 CAShapeLayer 做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。
而具体绘制图形,一般有两种方式:UIBezierPath 和使用 CGContextRef。CGContextRef是基于UIView(但是实际上也是画在CALayer上的),大约有如下需要注意的:
- 渲染速度,正如前文所述,CAShapeLayer渲染更快速。因为它使用了硬件加速,CALayer则没有
- 像素化等问题
我们一般在UIView上绘图的时候可以继承UIView并重写draw(_ rect: CGRect)方法,在这个方法里边取的上下文进行自定义的绘制。如果重写了这个方法,UIView 就会创建一个寄宿图,大小是 UIView的 width * height * 屏幕scale,如果是在比较大的区域绘制,不建议用这种方式,这种方式会造成内存暴涨。可以通过专用图层来解决这个问题,后边会说到这个问题。
关于K线绘制
实际上对于K线使用Core Graphics或Core Animation,很多公司实用的也是Core Graphics,只要绘图size不要太大也不会出现内存暴涨问题(不然这些大厂早就改了)。当然最佳选择应该还是用Core Animation,即CAShapeLayer+UIBezierPath的方式。至于两种方式差别有多大,还需要实际的验证。
模型树–>呈现树–>渲染树
CoreAnimation 将不同的视图层组合绘制的过程经历了3个阶段: 模型树–>呈现树–>渲染树,其中模型树和呈现树分别对应CALayer的2个属性
- (nullable instancetype)presentationLayer;
- (instancetype)modelLayer;
关于 模型树和呈现树 的工作方式, 这里有个比较形象的比喻:
在CALayer内部,它控制着两个属性:presentationLayer(以下称为P)和modelLayer(以下称为M)。P只负责显示,M只负责数据的存储和获取。我们对layer的各种属性赋值比如frame,实际上是直接对M的属性赋值,而P将在每一次屏幕刷新的时候回到M的状态。比如此时M的状态是1,P的状态也是1,然后我们把M的状态改为2,那么此时P还没有过去,也就是我们看到的状态P还是1,在下一次屏幕刷新的时候P才变为2。而我们几乎感知不到两次屏幕刷新之间的间隙,所以感觉就是我们一对M赋值,P就过去了。P就像是瞎子,M就像是瘸子,瞎子背着瘸子,瞎子每走一步(也就是每次屏幕刷新的时候)都要去问瘸子应该怎样走(这里的走路就是绘制内容到屏幕上),瘸子没法走,只能指挥瞎子背着自己走。可以简单的理解为:一般情况下,任意时刻P都会回到M的状态。而当一个CAAnimation(以下称为A)加到了layer上面后,A就把M从P身上挤下去了。现在P背着的是A,P同样在每次屏幕刷新的时候去问他背着的那个家伙,A就指挥它从fromValue到toValue来改变值。而动画结束后,A会自动被移除,这时P没有了指挥,就只能大喊“M你在哪”,M说我还在原地没动呢,于是P就顺声回到M的位置了。这就是为什么动画结束后我们看到这个视图又回到了原来的位置,是因为我们看到在移动的是P,而指挥它移动的是A,M永远停在原来的位置没有动,动画结束后A被移除,P就回到了M的怀里。 动画结束后,P会回到M的状态(当然这是有前提的,因为动画已经被移除了,我们可以设置fillMode来继续影响P),但是这通常都不是我们动画想要的效果。我们通常想要的是,动画结束后,视图就停在结束的地方,并且此时我去访问该视图的属性(也就是M的属性),也应该就是当前看到的那个样子。按照官方文档的描述,我们的CAAnimation动画都可以通过设置modelLayer到动画结束的状态来实现P和M的同步。
实现动画的方式主要分两大类:CoreAnimation动画和非CoreAnimation动画,其中CoreAnimation动画是充分优化过的,其操作的是呈现树。而非CoreAnimation动画操作的是模型树,常见的有定时器动画和手势动画,相对要慢,但是实现简单。
CALayer 和 UIView 的区别和联系
每个 UIView 内部都有一个 CALayer 在背后提供内容的绘制和显示,并且 UIView 的尺寸样式都由内部的 Layer 所提供。两者都有树状层级结构,layer 内部有 SubLayers,View 内部有 SubViews.但是 Layer 比 View 多了个AnchorPoint
在 View显示的时候,UIView 做为 Layer 的 CALayerDelegate,View 的显示内容由内部的 CALayer 的 display
CALayer 是默认修改属性支持隐式动画的,在给 UIView 的 Layer 做动画的时候,View 作为 Layer 的代理,Layer 通过 actionForLayer:forKey:向 View请求相应的 action(动画行为)
layer 内部维护着三分 layer tree,分别是 presentLayer Tree(动画树),modeLayer Tree(模型树), Render Tree (渲染树),在做 iOS动画的时候,我们修改动画的属性,在动画的其实是 Layer 的 presentLayer的属性值,而最终展示在界面上的其实是提供 View的modelLayer
两者最明显的区别是 View可以接受并处理事件,而 Layer 不可以
既已览卷至此,何不品评一二: