引子
PD:我们需要的界面大概是这样子的,可以实现吗?
技术:ok,这个界面很简单,我们用基本的view就可以实现。
数日后
UED:我们的设计是这样子的
技术:呃,晴天霹雳啊,为什么搞的这么复杂,又要弧度,又要曲线?为什么不能用标准的框架来
UED:这就是艺术,激烈而又平滑的过渡才能彰显她旺盛的生命力,而明暗错落的颜色赋予了她燃烧不尽的激情,仿佛一个少女,身材高挑、烈焰红唇......
技术:就不能少搞些奇技淫巧吗,我已经连吐槽的力气都没有了...
相信每个前端开发都有过这样的经历,我们认为可以用通用视图解决的需求,视觉非要加上点特殊的元素,不规则图形、不均匀颜色。做为一个有追求的Coder虽然嘴上说着不要,但身体却很诚实的去查资料。google一下iOS 绘图,各种绘图的技术简介和demo就搜出来了,随之而来的是各种意思好像都差不多,好像不是一回事的名词:UIBezierPath 、Quartz、Quartz 2D 、QuartzCore、Core Graphic、OpenGL 、OpenGL ES......
此时的我是这样子的
完全搞不懂到底在哪种场景下应该使用哪种技术,有木有。本文的目的就是梳理在iOS平台中供开发人员使用的绘图框架以及他们之间的恩怨纠葛,以便选择最合适的技术框架完成需求。
iOS、MacOS系统图形架构
由此图可见:
iOS提供了两套绘图框架,分别是UIBezierPath和Core Graphics。UIBezierPath属于UIKit。UIBezierPath是对Core Graphics框架的进一步封装。
OpenGL和Core Graphics都是绘图专用的API类族,调用图形处理器(GPU)进行图形的绘制和渲染。在架构上是平级的,相比UIkit更接近底层。
UIBezierPath
用于创建基于矢量的路径,如圆形、椭圆形和矩形,或者由多个直线和曲线组成的形状。绘图步骤也非常的简单:
重写drawRect方法
创建UIBezierPath对象
设置绘图属性,lineWidth
渲染
UIBezierPath绘制图形代码
//绘制圆形
- (void)drawRect:(CGRect)rect{
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:rect];
//设置绘图属性
path.lineWidth = 1;
UIColor *color = [UIColor readColor];
[color set];
//按照路径绘制图形
[path stroke];
}
Quartz
在介绍 Core Graphics 之前,我们先把 Quartz 的概念理顺清楚。嫌啰嗦可以直接拉到下面看结论
简单来说:
Quartz由Quartz Compositor和Quartz 2D两部分组成
Core Graphics是基于Quartz框架的2D绘图引擎。所以很多资料将Core Graphics称为Quartz是不准确的。
Core Graphics同Quartz 2D是等价的。
那么Quartz同OpenGL是什么关系呢?他的底层是否通过OpenGL调用GPU?
结论
CoreGraphics是基于Quartz框架的绘图引擎,同Quartz 2D是等价的。
Quartz Extreme是针对Quartz底层的GPU加速。
Quartz仅使用OpenGL的命令集,直接连接AGP(图形加速接口)
Quartz在CPU上执行绘图命令,在GPU上最终合成成图形
QuartzGL是Quartz 2D API的GPU加速。启用QuartzGL后,所有Quartz绘图命令都将转换为OpenGL命令并在GPU上执行。这个模式默认是关闭的
Quartz在进行3D图形渲染时是基于OpenGL的,通过OpenGL连接AGP
Core Graphics
Core Graphics是基于Quartz框架的高保真输出2D图形的渲染引擎。可处理基于路径的绘图、抗锯齿渲染、渐变、图像、颜色管理、PDF文档等。 Core Graphics提供了一套2D绘图功能的C语言API,使用C结构体和C的函数模拟了一套面向对象的编程机制。Core Graphics中没有OC的对象和方法。
无论图片、PDF还是视图的图层,都是由CoreGraphics框架完成绘制的。UIImage、UIBezierPath和NSString都提供了至少一种用于在drawRect:中绘图的方法,实现原理是将Core Graphics代码封装在其中,降低绘图难度。
Context
CoreGraphics中最重要的对象是graphics context,既图形上下文。context是CGContextRef的对象,负责存储绘画状态和绘制内存所处的内存空间。
我以前一直无法很好的理解Context,后来接触Android绘图的时候发现Android没有这个东西。Android的2D图形绘制通过Canvas和Paint实现的。Android的绘图框架理解起来就非常的容易,你想画图首先你要有画的地方Canvas,其次你要有笔Paint。而在iOS中Context就是要画图的画板和画笔。
Core Graphics绘制图形代码
//绘制圆形
- (void)drawInContext:(CGContextRef)ctx
{
//保存当前的绘图Context
CGContextSaveGState(ctx);
CGRect rectAngle = CGRectMake(0, 0, 100, 100);
//添加一个椭圆路径
CGContextAddEllipseInRect(ctx, rectAngle);
//设置边框宽度
CGContextSetLineWidth(ctx, self.ellipseBorderWidth);
//设置边框颜色
CGContextSetStrokeColorWithColor(ctx, self.ellipseBorderColor.CGColor);
CGContextStrokePath(ctx);
//设置填充色
CGContextSetFillColorWithColor(ctx, self.ellipseFillColor.CGColor);
CGContextFillEllipseInRect(ctx, rectAngle);
CGContextRestoreGState(ctx);
}
Core Graphics的Ref后缀类型
带有Ref后缀的类型是CoreGraphics中用来模拟面向对象机制的C结构。CoreGraphics对象是在堆上分配内存,因此创建CoreGraphics对象时,会返回一个指向对象内存地址的指针。
使用这种分配方式的C结构都有一个用来表示结构指针的类型定义(type definition)。例如,CGColor结构,不会被直接使用的类型,有一个表示Color * 的类型定义—CGColorRef,用来被使用的类型。使用这种类型定义是为了区分指针变量,方便开发者判断指针变量是指向C结构还是可以接收消息的Objective-C对象。
CGRect和CGPoint这种比较简单直接在栈上分配的结构体,不需要使用结构指针,因此类型名称后不带Ref后缀。
带有Ref后缀的类型的对象可能是强引用指针,成为指向对象的拥有者。ARC是无法识别Core对象的所有权,必须在使用后手动释放。规则是,如果使用名称中带有create或者copy的函数创建了一个CoreGraphics对象,就必须调用Release函数并传入该对象的指针。
Core Graphics 能完成的工作
绘制图形 : 线条、三角形、矩形、圆、圆弧弧等
绘制文字
绘制生成图片(图像)
读取生成PDF
绘制渐变
Core Graphics相比UIBezierPath在使用上更复杂一些,但是支持的效果也更多,程序运行效率更高。所以现在iOS系统上绘图需求基本上都使用Core Graphis来完成。
OpenGL && OpenGL ES
OpenGL ES 是OpenGL 三维图形API的子集,针对嵌入式操作系统设计。iOS和Android都在系统内集成了OpenGL ES。所以针对OpenGL ES 的代码可以实现跨平台,大多数游戏框架都基于OpenGL。
OpenGL编程流程
1、编写shader脚本
编写vertex shader
编写fragment shader
2、创建shader实例
创建 shader
装载 shader
编译 shader
3、创建program实例
创建program
装配shader
链接program
使用program
装载OpenGL shader 、创建OpenGL program示例代码
- (void)setupProgram
{
NSString *vertexShaderPath = [[NSBundle mainBundle] pathForResource:@"VertexShader" ofType:@"glsl"];
NSString *fragmentShaderPath = [[NSBundle mainBundle] pathForResource:@"FragmentShader" ofType:@"glsl"];
GLuint vertexShader = [GLESUtils loadShader:GL_VERTEX_SHADER withFilepath:vertexShaderPath];
GLuint fragmentShader = [GLESUtils loadShader:GL_FRAGMENT_SHADER withFilepath:fragmentShaderPath];
//Create program,attach shaders
_programHandle = glCreateProgram();
if (!_programHandle)
{
NSLog(@"Failed to create program");
return;
}
glAttachShader(_programHandle, vertexShader);
glAttachShader(_programHandle, fragmentShader);
glLinkProgram(_programHandle);
GLint linked;
glGetProgramiv(_programHandle, GL_LINK_STATUS, &linked);
if (!linked)
{
GLint infoLen = 0;
glGetProgramiv(_programHandle, GL_INFO_LOG_LENGTH, &infoLen);
if (infoLen > 1)
{
char *infoLog = malloc(sizeof(char) *infoLen);
glGetProgramInfoLog(_programHandle, infoLen, NULL, infoLog);
free(infoLog);
}
glDeleteProgram(_programHandle);
_programHandle = 0;
return;
}
glUseProgram(_programHandle);
_positionSlot = glGetAttribLocation(_programHandle, "vPosition");
}
iOS系统使用OpenGL编程流程
1、设置CAEAGLLayer
CAEAGLLayer是iOS中用于呈现OpenGL ES的渲染内容的
2、设置EAGLContext
OpenGL ES 渲染上下文。这个context管理所有使用OpenGL ES 进行描绘的状态,命令以及资源信息。
3、创建Renderbuffer
缓冲区用于存储绘图数据,Render Buffer有三种类型,分别是color、depth、stencil buffer
4、创建Framebuffer object
Framebuffer object是buffer的管理者,color、depth、stencil可以添加到一个Framebuffer object上
5、销毁Renderbuffer和Framebuffer
当UIView变化后,layer的宽高也随之变化,导致原来的renderbuffer 不再相符,需要销毁既有 renderbuffer 和 framebuffer
iOS系统调用OpenGL示例代码
//在iOS设备中通过CAEAGLLayer使用OpenGL接口渲染内容
+ (Class)layerClass
{
return [CAEAGLLayer class];
}
//设置呈现图层
- (void)setupLayer
{
_eaglLayer = (CAEAGLLayer *)self.layer;
_eaglLayer.opaque = YES;
_eaglLayer.drawableProperties = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:NO], kEAGLDrawablePropertyRetainedBacking,
kEAGLColorFormatRGBA8, kEAGLDrawablePropertyColorFormat ,nil];
}
//设置图形上下文
- (void)setupContext
{
EAGLRenderingAPI api = kEAGLRenderingAPIOpenGLES2;
_context = [[EAGLContext alloc] initWithAPI:api];
if (!_context)
{
NSLog(@"Failed to initialize");
exit(1);
}
if (![EAGLContext setCurrentContext:_context])
{
exit(1);
}
}
//设置渲染缓冲区
- (void)setupRenderBuffer
{
glGenRenderbuffers(1, &_colorRenderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderBuffer);
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:_eaglLayer];
}
//设置FrameBuffer
- (void)setupFrameBuffer
{
glGenFramebuffers(1, &_frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderBuffer);
}
- (void)destoryRenderAndFrameBuffer
{
glDeleteFramebuffers(1 , &_frameBuffer);
_frameBuffer = 0;
glDeleteRenderbuffers(1, &_colorRenderBuffer);
_colorRenderBuffer = 0;
}
示例代码地址:
http://gitlab.alibaba-inc.com/xunfeng.zy/opengl/blob/master/OpenGLDemo/
结论
UIBezierPath的优势是方便,能用最少的代码得要想要的图形,并且不需要管理图形上下文、缓冲区等容易出问题的地方,只需要关注图形本身就行了。最主要的缺点是支持的效果有限,当需要实现一些复杂图形、复杂渐变效果的时候就无能为力了。所以如果只是一个简单的图形没有特别的要求,可以用UIBezierPath实现。
Core Graphics的功能就比UIBezierPath强大很多,使用起来也更复杂,而且需要自己管理图形上下文,需要投入更多的开发工作量。在效率和可做更多工作这两个方面上Core Graphics全面压制UIBezierPath,所以如果是复杂的图形、多个图形叠加、多种颜色渐变等需求可以使用Core Graphics实现。
OpenGL ES是直接操作GPU绘图,而Core Graphics框架是先把命令输入到CPU再调用GPU绘图。很明显OpenGL ES绘图的效率更高,使用OpenGL ES绘图也可以实现跨平台,而Core Graphics只能在iOS系统中使用。相比Core Graphics只支持2D绘图,OpenGL ES对2D\3D都有很好的支持。
可见OpenGL ES是全方面的碾压Core Graphics,但是除了特别大的图片运算需求外,我们很少使用OpenGL ES。其实上面的示例代码已经很好的说明了,绘制同一个图形UIBezierPath需要5行代码,Core Graphics需要10行代码,虽然Core Graphics明显比UIBezierPath工作量大,但还是同一个数量级的。
而使用OpenGL ES绘制,算上Vertex Shader和Fragment Shader绘制一个图形要300~400行代码。整整增加了两个数量级。再加上调试、debug的工作量,就算有跨平台的加成也无法抵消增加的时间成本。想使用OpenGL进行绘图,基本上都需要二次封装,这也是为什么我们在绘图的时候一般首选Core Graphics。
作者:darcy87
链接:https://www.jianshu.com/p/0cfd271fc6db
相关推荐: