@qidiandasheng
2016-09-23T20:48:28.000000Z
字数 8666
阅读 3012
iOS界面开发
Core Graphics Framework
是一套基于C的API框架,使用了Quartz
作为绘图引擎。它提供了低级别、轻量级、高保真度的2D渲染。该框架可以用于基于路径的绘图、变换、颜色管理、脱屏渲染,模板、渐变、遮蔽、图像数据管理、图像的创建、遮罩以及PDF文档的创建、显示和分析。
下图是Core Graphics Framework
里的一些头文件:
2D绘图一般可以拆分成以下几个操作:
由于像素是依赖于目标的,所以2D绘图并不能操作单独的像素,我们可以从上下文(Context)读取它。
绘图就好比在画布上拿着画笔机械的进行画画,通过制定不同的参数来进行不同的绘制。
这里为什么写这两个呢!
因为我傻啊,刚开始看CALayer的时候,发现有个Contents(寄宿图)
属性,文章中写到当你重写-drawRect:
的时候会产生一张寄宿图,如果UIView检测到-drawRect:
方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale
的值。
但是对UIView来说,寄宿图并不是必须的,如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:
方法。
以下就是我当初愚蠢的想法:
而我就是把
Contents
看成了曾经看到过的Context(图形上下文)
。所以我就在疑惑了,绘图不是都需要图形上下文的吗?那不是都有寄宿图了,那为什么还说重写-drawRect:
会产生寄宿图,发生性能问题呢?
所以我们现在就来好好说说Context
和Contents
。
Context(图形上下文)
Core Graphics
里有个CGContext.h
文件,我们看到图形上下文(Graphics context)是一个CGContextRef
数据,其作用是:
1:保存绘图信息、绘图属性
2:绘制目标图案
3:输出绘制好的图案到输出目标去,即渲染到什么地方去(可以是PDF文件、bitmap或者显示器的窗口上)
在同一个绘图序列里面, 指定不同的图形上下文, 可以将所绘制的图像绘制到不同的目标上面,在 Quartez2D 里, 提供了几种 Graphics Context:
- Bitmap Graphics Context
- PDF Graphics Context
- Window Graphics Context
- Layer Graphics Context
- Printer Graphics Context
Core Graphics API所有的操作都在一个上下文中进行。所以在绘图之前需要获取该上下文并传入执行渲染的函数中。如果你正在渲染一副在内存中的图片,此时就需要传入图片所属的上下文。获得一个图形上下文是我们完成绘图任务的第一步,你可以将图形上下文理解为一块画布。如果你没有得到这块画布,那么你就无法完成任何绘图操作。
获取图形上下文的几种方式:
1.drawRect:
2.inContext:
- (void)drawInContext:(CGContextRef)ctx
- (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx)
3.UIGraphicsBeginImageContextWithOptions
获取到图形上下文之后我们就要开始绘图了。这里有两大绘图框架:
UIKit
像UIImage、NSString(绘制文本)、UIBezierPath(绘制形状)、UIColor都知道如何绘制自己。
这些类提供了功能有限但使用方便的方法来让我们完成绘图任务。一般情况下,UIKit就是我们所需要的。Core Graphics
这是一个绘图专用的API族,它经常被称为QuartZ或QuartZ 2D。Core Graphics是iOS上所有绘图
功能的基石,包括UIKit。说明
其实UIKit
就是对Core Graphics
的封装,比如在绘制形状的时候就可以在图形上下文中用UIBezierPath
或Core Graphics
直接绘制需要的形状。
绘图的例子:
这里有四种获取图形上下文的方式,还有两个绘图框架,那么我们就有8种方式绘制我们需要的图形。我这里写了个demo可以看一下:DSCoreGraphics
Contents(寄宿图)
Contents
其实是属于CALayer
的属性,CALayer
是属于QuartzCore Framework(CoreAnimation)
框架的。
CALayer
呢简单点说就是对Core Graphics
绘制的管理类。CALayer
类能够包含一张你喜欢的图片,也就是Contents(寄宿图)
(即图层中包含的图)。
我们上文中说道当你重写-drawRect:
的时候会产生一张寄宿图,如果UIView检测到-drawRect:
方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale
的值。而这个寄宿图会消耗CPU和内存,常常这个寄宿图又不是必须的。(-drawRect:
方法没有默认的实现)
注: 上面获取图形上下文中drawRect:
、inContext:
这两种方式系统都会为视图分配一个寄宿图。
测试:你可以用DSCoreGraphics的例子测试一下,使用
drawRect:
获取上下文的例子中把UIView
的frame.size
设的很大,比如2000x2000,你能看到内存急剧的上升,我看到的是159M左右。说明这里就产生了一个很大的寄宿图。而我们使用
inContext:
获取上下文的例子中把layer
的frame.size
设的很大,也是2000x2000,但是我们发现内存并没有急剧的上升。文章内存恶鬼drawRect中说使用
drawRect:
会产生寄宿图,所以会产生大量内存。而CAShapeLayer
不需要创建寄宿图,所以不会产生大量内存。
其实我觉得是错误的,既然CAShapeLayer
是继承自CALayer
,那为什么CALayer
会产生寄宿图而CAShapeLayer
不会呢。
你可要用我的DSCoreGraphics中CGOneViewController.h(drawRect:)
的例子测试一下,只有给View
设置了背景颜色的时候(view.backgroundColor = [UIColor blackColor];
)才会有内存激增,如果不设置颜色,其实内存是跟使用CALayer
差不多的。所以我的结论就是
drawRect:
、inContext:
这两种方式都会产生寄宿图。只是drawRect:
的时候产生的寄宿图其实是包含的view
的颜色的,当view
有颜色的时候这张寄宿图也会有相应的颜色所以就会特别大。
而CALayer
使用inContext:
这种方式时产生的寄宿图是不包含CALayer
的颜色的,所以产生的寄宿图就不会很大。
具体介绍可以参考寄宿图这篇文章。
我这里使用
UIGraphicsBeginImageContextWithOptions
这种获取上下文的方式进行绘制。这部分的代码可以在DSCoreGraphics中看到。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
UIFont *font = [UIFont systemFontOfSize:30];
NSString *string = @"Core Graphics";
[string drawAtPoint:CGPointMake(0, 0) withAttributes:@{NSFontAttributeName:font,NSForegroundColorAttributeName:[UIColor redColor]}];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/*---------------使用绘制完成的那张图片------------------*/
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
[imageView setImage:im];
[self.view addSubview:imageView];
- (void)drawAtPoint:(CGPoint)point;
- (void)drawAtPoint:(CGPoint)point blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
- (void)drawInRect:(CGRect)rect;
- (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
使用该方法可以构建更复杂的功能,如混合图片,类似PS中的功能,如颜色加深和强光以及让图片部分透明,这些方法都可以实现。相比于之前创建多个UIImageView对象,使用这种方式会更加的方便和容易。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(300,300), NO, 0);
UIImage *img1 = [UIImage imageNamed:@"one"];
UIImage *img2 = [UIImage imageNamed:@"two"];
UIImage *img3 = [UIImage imageNamed:@"three"];
//这里使用了混合模式,正常和透明度为50%
[img1 drawInRect:CGRectMake(0, 0, 150, 150) blendMode:kCGBlendModeNormal alpha:.5];
[img2 drawInRect:CGRectMake(50, 50, 150, 150)];
[img3 drawInRect:CGRectMake(150, 150, 150, 150)];
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/*---------------------------------*/
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 300, 300)];
[imageView setImage:im];
[self.view addSubview:imageView];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
//DrawingLine
[[UIColor brownColor] set]; //设置上下文使用的颜色
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 2.0f);
CGContextMoveToPoint(context, 0.0f, 150.0f); // 画笔移动到某点
CGContextAddLineToPoint(context, 200.0f, 150.0f);
CGContextStrokePath(context); //执行绘制
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/*---------------------------------*/
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[imageView setImage:im];
[self.view addSubview:imageView];
上下文在绘制一条直线后会定格在刚才绘制的线的末端,这样我们可以再通过CGContextAddLineToPoint方法来设置立即进行下一条线的绘制。在线的交界处,我们可以设置交界的样式(CGLineJoin),这个枚举中有三种样式:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
//DrawingLinesContinuously
CGContextSetLineWidth(context, 6.0f);
CGContextSetLineJoin(context, kCGLineJoinRound); //线条交汇处样式:圆角
CGContextMoveToPoint(context, 20.0f, 150.0f);
CGContextAddLineToPoint(context, 20.0f, 80.0f);
CGContextAddLineToPoint(context, 200.0f, 80.0f);
CGContextStrokePath(context);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/*---------------------------------*/
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[imageView setImage:im];
[self.view addSubview:imageView];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
//DrawingRect
CGRect strokeRect = CGRectMake(0, 85, 200, 60);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2.0f);
CGContextStrokeRect(context, strokeRect);
UIColor *clearRed = [UIColor colorWithRed:0.5 green:0.0 blue:0.0 alpha:0.2];
CGContextSetFillColorWithColor(context, clearRed.CGColor);
CGContextFillRect(context, strokeRect);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/*---------------------------------*/
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 250, 200)];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[imageView setImage:im];
[self.view addSubview:imageView];
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect strokeRect = CGRectMake(0, 85, 200, 60);
//DrawingRect
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
NSArray *colors = @[(__bridge id)[UIColor colorWithRed:0.3 green:0.0 blue:0.0 alpha:0.2].CGColor,
(__bridge id)[UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:0.8].CGColor];
const CGFloat locations[] = {0.0, 1.0};
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
CGPoint startPoint = CGPointMake(CGRectGetMinX(strokeRect), CGRectGetMinY(strokeRect)); //矩形最小x,y
CGPoint endPoint = CGPointMake(CGRectGetMaxX(strokeRect), CGRectGetMaxY(strokeRect)); //矩形最大x,y
CGContextSaveGState(context);
CGContextAddRect(context, strokeRect);
CGContextClip(context);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0); //开始绘制
CGContextRestoreGState(context);
//释放资源
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
/*---------------------------------*/
UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 250, 200)];
imageView.contentMode = UIViewContentModeScaleAspectFit;
[imageView setImage:im];
[self.view addSubview:imageView];
离屏渲染有两种方式:
GPU渲染:GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。一般像设置圆角的时候产生的GPU离屏渲染会产生卡顿的原因为屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。
CPU渲染:如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地完成,渲染得到的bitmap最后再交由GPU用于显示。
所以说有时候CPU渲染其实是把一部分GPU的操作转移到了CPU上,节省了GPU的时间,在因为GPU渲染时间过长造成的卡顿中我们就可以使用这种方式防止卡顿。为什么会这样呢?原因如下:
在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。
从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。