实际上iOS的UIKit框架就是基于Core Graphics(Quartz2D)和Core Animation(Quartz Core)实现的,其中Core Graphics负责把内容绘制出来,Core Animation负责用图层把绘制的内容呈现出来和并且提供动画能力。
1.png目录
一、什么是Quartz2D?
二、Quartz2D的几个关键词
1、图形上下文
2、图形上下文栈
3、Quartz2D的内存管理
三、Quartz2D绘制路径的能力?
一、什么是Quartz2D?
-
Core Graphics框架是一个C语言写的、支持iOS和macOS的二维绘图框架,专门用来做绘图操作的。而Quartz2D就是Core Graphics框架的绘图引擎,所谓引擎就是指经过封装的一个函数库,但其实Quartz2D并不是一个真实存在的库,它只是包含了Core Graphics框架里面的部分函数,是一个抽象的引擎。那么,此处我们暂时把Core Graphics和Quartz2D等同,但其实明显Quartz2D要比Core Graphics小,Quartz2D的数据类型和函数基本上都是以CG为前缀的。
-
Quartz2D具备很多的能力,如:
在Quartz2D众多的能力中,我们使用最多的能力还是它绘制路径的能力,常用它来自定义一些UI控件。
- 绘制路径
- 不同输出设备(如PDF文件、bitmap图像、显示器窗口或者打印机等)支持不同的颜色范围,Quartz2D有专门处理颜色的库;
- 不同输出设备的坐标系统也是不一样的,Quartz2D有专门处理仿射变换的库;
- 还有阴影、透明层、反锯齿、PDF文档生成与访问等能力。
二、Quartz2D的几个关键词
1、图形上下文
- 绘图需要画布,而图形上下文就是Quartz2D的画布;
- 图形上下文中存储着我们要把“一幅画”画成什么样子的信息,也决定了我们要把画渲染到什么地方去显示(如输出设备可以是view所关联的layer、PDF文件、bitmap图像、显示器窗口或者打印机);我们可以把图形上下文理解为具有路径信息区域、路径参数区域和路径绘制区域三个区域,其中路径信息区域存储着路径的起始点、结束点等信息,路径参数区域存储着路径颜色、路径宽度、路径开头和结尾样式、路径转角样式等参数,路径绘制区域用来根据前两个区域的信息和参数来绘制路径并保存,然后在对图形上下文渲染的时候再把绘制好的路径渲染到相应的输出设备显示。如下图:
- 一般情况下我们使用UIView给我们提供的图形上下文就可以了,重写view的
-drawRect:
方法在其内部通过UIGraphicsGetCurrentContext()
来获取;自定义上下文会降低内存的使用效率,导致性能下降。
2、图形上下文栈
我们先通过一个问题来引出图形上下文栈这个概念,不一定非要看懂这段代码,看了后面的小节回过头来看就能看懂了,先看着就可以了:
(清单2.1)
//
// CustomView.m
// Quartz2D
//
// Created by 意一yiyi on 2018/2/5.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 画第一条直线
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextMoveToPoint(context, 100, 100);
CGContextAddLineToPoint(context, 200, 100);
CGContextStrokePath(context);
// 画第二条直线
CGContextMoveToPoint(context, 100, 200);
CGContextAddLineToPoint(context, 200, 200);
CGContextStrokePath(context);
}
@end
1.png
运行,我们会发现,这两条直线被渲染成了同样的参数(都是红色、宽度11、都是圆角)。那问题就来了,对于第二条线我们并没有对它做参数方面的设置,它为什么应用了第一条线的参数设置而不是默认的参数设置呢?如果我们有需求说是第二条直线要采用默认的参数设置怎么办?在回答这个问题之前,我们先看一下
(1)Quartz2D是如何完成一次完整的绘图操作的:
-
第一步,调用
-drawRect:
方法:当程序第一次要显示我们自定义的view时,会首先调用该view的-drawRect:
方法; -
第二步,获取该view所关联的图形上下文(是一个Layer Graphics Context):我们可以在view的
-drawRect:
方法里获取到该view所关联的图形上下文(这也是唯一一个我们能获取到一个view所关联的图形上下文的方法,它是一个Layer Graphics Context,所以绘制出来的内容其实是渲染在view所关联的layer上的)。 -
第三步,图形上下文的三个区域的协同工作绘制出路径:当我们设置好路径的信息和参数之后,Quartz2D并不会直接把路径绘制到view上,而是会去路径信息区域和路径参数区域查找路径的信息和参数,先在路径绘制区域把路径绘制出来并保存在这里;
-
第四步,路径渲染到view关联的layer上呈现出来:当路径在路径绘制区域绘制并保存完成后,我们调用渲染路径的相关方法时,路径才会被渲染显示到view所关联的layer上完成这一次绘图操作。
(2)这样我们就可以回答(清单2.1)的问题了
- 绘制第一条线的时候,图形上下文如下:
- 绘制第二条线的时候,图形上下文如下:
- 可以看出,我们在绘制第二条线的时候只是改变了图形上下文的路径信息区域的信息,而路径参数区域还是保存着第一条线的参数没有变化,所以就导致了路径在绘制的时候第二条线和第一条线采取了同样的路径参数。所以,我们只要把第二条线的参数设置设置为默认参数设置就可以达到效果了,如下:
(清单2.2)
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 画第一条直线
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextMoveToPoint(context, 100, 100);
CGContextAddLineToPoint(context, 200, 100);
CGContextStrokePath(context);
// 画第二条直线
CGContextSetRGBStrokeColor(context, 0, 0, 0, 1);
CGContextSetLineWidth(context, 1);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextMoveToPoint(context, 100, 200);
CGContextAddLineToPoint(context, 200, 200);
CGContextStrokePath(context);
}
更改后,绘制第二条线的时候,图形上下文
但是如果有很多条路径的话,我们这样一条一条修改为默认状态也是很费劲的,所以接下来我们就引入了图形上下文栈这个概念。
(3)图形上下文栈
- 其实我们在获取到view所关联的图形上下文之后,它是会被保存在一个图形上下文栈里面的;
- 在获取完图形上下文后的第一时间,我们可以通过
CGContextSaveGState(context)
函数来复制一份最原始的纯洁的图形上下文,并把它压入图形上下文栈中; - 然后在需要使用图形上下文的时候,使用
CGContextRestoreGState(context)
从图形上下文栈中拿出复制的那份纯洁的图形上下文在上面绘图; - 需要注意的是,我们复制(入栈)了几份图形上下文,就可以拿出(出栈)几份图形上下文来使用,数量不匹配的话程序会崩掉。
例如下图:
1.png
- 因此,我们就可以通过如下代码来实现上面想要达到的需求。
(清单2.3)
//
// CustomView.m
// Quartz2D
//
// Created by 意一yiyi on 2018/2/5.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);// 复制一份最原始的纯洁的图形上下文入栈
// 画第一条直线
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextMoveToPoint(context, 100, 100);
CGContextAddLineToPoint(context, 200, 100);
CGContextStrokePath(context);
// 画第二条直线
CGContextRestoreGState(context);// 出栈一份最原始的纯洁的图形上下文用来绘图
CGContextMoveToPoint(context, 100, 200);
CGContextAddLineToPoint(context, 200, 200);
CGContextStrokePath(context);
}
@end
1.png
运行,可见达到了效果:绘制第一条线的时候,用的是图形上下文B,B里面的路径信息区域、路径参数区域、路径绘制区域都是空的,然后被设置为了红色、宽度11、圆角的路径参数;绘制第二条线的时候,栈顶元素已经是图形上下文A了,而A里面的路径信息区域、路径参数区域、路径绘制区域也都是空的,是默认状态,并不受图形上下文B的影响,所以能达到效果。
3、Quartz2D的内存管理
Quartz2D不支持ARC,所以我们需要对其内存进行手动管理,有下面一些规则:
- 如果你创建或者拷贝了一个对象,那么你将持有这个对象,因此你必须在不需要使用它的时候手动释放它。通常,如果使用含有Create或者Copy单词的函数获取一个对象,当使用完后就必须手动释放它,否则将导致内存泄露;如果使用不含有Create或者Copy单词的函数获取一个对象,你将不会拥有对象的引用,不需要释放它。
- 如果你想增加一个对象的引用计数,那么也必须retain它并且在不需要时release掉,可以使用Quartz2D的函数来指定retain和release一个对象。例如,如果创建了一个CGColorspace对象,则使用函数CGColorSpaceRetain和CGColorSpaceRelease来retain和release对象。
三、Quartz2D绘制路径的能力?
其实无论使用Quartz2D绘制什么东西,核心的绘图步骤都是四步:
- (1)获取图形上下文
- (2)设置路径参数
- (3)设置路径信息
- (4)渲染路径
那么接下来,我们用Quartz2D绘制绘制第一篇中UIBezierPath所绘制过的例子:来看下如何使用Quartz2D绘制路径,并且和UIBezierPath绘制路径进行对比。
1、画一条直线
(清单2.4)
//
// CustomView.m
// Quartz2D
//
// Created by 意一yiyi on 2018/2/5.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
// (1)获取图形上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 入栈
CGContextSaveGState(context);
CGContextSaveGState(context);
// 画第一条直线
// (2)设置路径参数
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextSetLineJoin(context, kCGLineJoinRound);
// (3)设置路径信息
CGContextMoveToPoint(context, 100, 100);
CGContextAddLineToPoint(context, 200, 100);
// (4)渲染路径
CGContextStrokePath(context);
// 画第二条直线
CGContextRestoreGState(context);// 出栈
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextMoveToPoint(context, 100, 200);
CGContextAddLineToPoint(context, 200, 200);
CGContextStrokePath(context);
// 画第三条直线
CGContextRestoreGState(context);// 出栈
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapSquare);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextMoveToPoint(context, 100, 300);
CGContextAddLineToPoint(context, 200, 300);
CGContextStrokePath(context);
}
@end
1.png
2、画圆弧、 圆、椭圆
(清单2.5)
//
// CustomView.m
// Quartz2D
//
// Created by 意一yiyi on 2018/2/5.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 画一段圆弧
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextMoveToPoint(context, 150, 100);
// x,y:圆心
// radius:半径
// startAngle:开始的弧度
// endAngle:结束的弧度
// clockwise:是否顺时针画圆弧(0-顺时针,1-逆时针)
CGContextAddArc(context, 100, 100, 50, 0, M_PI_2, NO);
CGContextStrokePath(context);
// 画圆
CGContextSetRGBFillColor(context, 1, 0, 0, 1);
CGContextMoveToPoint(context, 150, 250);
CGContextAddArc(context, 100, 250, 50, 0, M_PI * 2, NO);
CGContextFillPath(context);
// 画椭圆
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextMoveToPoint(context, 150, 400);
CGContextAddEllipseInRect(context, CGRectMake(50, 350, 100, 50));
CGContextStrokePath(context);
}
@end
1.png
3、画矩形
(清单2.6)
//
// CustomView.m
// Quartz2D
//
// Created by 意一yiyi on 2018/2/5.
// Copyright © 2018年 意一yiyi. All rights reserved.
//
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 画矩形
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetLineWidth(context, 11);
CGContextSetLineCap(context, kCGLineCapButt);
CGContextAddRect(context, CGRectMake(100, 100, 100, 100));
CGContextStrokePath(context);
}
@end
1.png
其它绘制的例子就不一一列举了,使用Quartz2D都能实现。通过和UIBezierPath绘图的对比,我们可以发现使用UIBezierPath来绘图会更加方便。
结论:
- 其实UIBezierPath存在于UIKit中,它就是对Quartz2D绘制路径能力的OC封装;
- 所以如果遇到需要绘制路径的场景,我们可以优先使用UIBezierPath,它使用起来比较方便也基本能满足需求,而且需要我们考虑的东西也比较少,只有当遇到需要使用Quartz2D的其它能力时再考虑直接使用Quartz2D。