[关闭]
@qidiandasheng 2016-09-23T20:48:28.000000Z 字数 8666 阅读 3012

CoreGraphics图形绘制

iOS界面开发


介绍

Core Graphics Framework是一套基于C的API框架,使用了Quartz作为绘图引擎。它提供了低级别、轻量级、高保真度的2D渲染。该框架可以用于基于路径的绘图、变换、颜色管理、脱屏渲染,模板、渐变、遮蔽、图像数据管理、图像的创建、遮罩以及PDF文档的创建、显示和分析。

下图是Core Graphics Framework里的一些头文件:

2D绘图一般可以拆分成以下几个操作:

由于像素是依赖于目标的,所以2D绘图并不能操作单独的像素,我们可以从上下文(Context)读取它。
绘图就好比在画布上拿着画笔机械的进行画画,通过制定不同的参数来进行不同的绘制。


Context和Contents

这里为什么写这两个呢!

因为我傻啊,刚开始看CALayer的时候,发现有个Contents(寄宿图)属性,文章中写到当你重写-drawRect:的时候会产生一张寄宿图,如果UIView检测到-drawRect: 方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale 的值。

但是对UIView来说,寄宿图并不是必须的,如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。

以下就是我当初愚蠢的想法:

而我就是把Contents看成了曾经看到过的Context(图形上下文)。所以我就在疑惑了,绘图不是都需要图形上下文的吗?那不是都有寄宿图了,那为什么还说重写-drawRect:会产生寄宿图,发生性能问题呢?

所以我们现在就来好好说说ContextContents


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. 1.drawRect:
  2. 2.inContext:
  3. - (void)drawInContext:(CGContextRef)ctx
  4. - (void)drawLayer:(CALayer*)layer inContext:(CGContextRef)ctx)
  5. 3.UIGraphicsBeginImageContextWithOptions

获取到图形上下文之后我们就要开始绘图了。这里有两大绘图框架:

UIKit
像UIImage、NSString(绘制文本)、UIBezierPath(绘制形状)、UIColor都知道如何绘制自己。
这些类提供了功能有限但使用方便的方法来让我们完成绘图任务。一般情况下,UIKit就是我们所需要的。

Core Graphics
这是一个绘图专用的API族,它经常被称为QuartZ或QuartZ 2D。Core Graphics是iOS上所有绘图
功能的基石,包括UIKit。

说明
其实UIKit就是对Core Graphics的封装,比如在绘制形状的时候就可以在图形上下文中用UIBezierPathCore 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:获取上下文的例子中把UIViewframe.size设的很大,比如2000x2000,你能看到内存急剧的上升,我看到的是159M左右。说明这里就产生了一个很大的寄宿图。

而我们使用inContext:获取上下文的例子中把layerframe.size设的很大,也是2000x2000,但是我们发现内存并没有急剧的上升。

文章内存恶鬼drawRect中说使用drawRect:会产生寄宿图,所以会产生大量内存。而CAShapeLayer不需要创建寄宿图,所以不会产生大量内存。
其实我觉得是错误的,既然CAShapeLayer是继承自CALayer,那为什么CALayer会产生寄宿图而CAShapeLayer不会呢。
你可要用我的DSCoreGraphicsCGOneViewController.h(drawRect:)的例子测试一下,只有给View设置了背景颜色的时候(view.backgroundColor = [UIColor blackColor];)才会有内存激增,如果不设置颜色,其实内存是跟使用CALayer差不多的。

所以我的结论就是drawRect:inContext:这两种方式都会产生寄宿图。只是drawRect:的时候产生的寄宿图其实是包含的view的颜色的,当view有颜色的时候这张寄宿图也会有相应的颜色所以就会特别大。
CALayer使用inContext:这种方式时产生的寄宿图是不包含CALayer的颜色的,所以产生的寄宿图就不会很大。

具体介绍可以参考寄宿图这篇文章。


绘图

我这里使用UIGraphicsBeginImageContextWithOptions这种获取上下文的方式进行绘制。这部分的代码可以在DSCoreGraphics中看到。

绘入字符串

  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
  2. UIFont *font = [UIFont systemFontOfSize:30];
  3. NSString *string = @"Core Graphics";
  4. [string drawAtPoint:CGPointMake(0, 0) withAttributes:@{NSFontAttributeName:font,NSForegroundColorAttributeName:[UIColor redColor]}];
  5. UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
  6. UIGraphicsEndImageContext();
  7. /*---------------使用绘制完成的那张图片------------------*/
  8. UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
  9. [imageView setImage:im];
  10. [self.view addSubview:imageView];

绘制图片

  1. - (void)drawAtPoint:(CGPoint)point;
  2. - (void)drawAtPoint:(CGPoint)point blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
  3. - (void)drawInRect:(CGRect)rect;
  4. - (void)drawInRect:(CGRect)rect blendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;

使用该方法可以构建更复杂的功能,如混合图片,类似PS中的功能,如颜色加深和强光以及让图片部分透明,这些方法都可以实现。相比于之前创建多个UIImageView对象,使用这种方式会更加的方便和容易。

  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(300,300), NO, 0);
  2. UIImage *img1 = [UIImage imageNamed:@"one"];
  3. UIImage *img2 = [UIImage imageNamed:@"two"];
  4. UIImage *img3 = [UIImage imageNamed:@"three"];
  5. //这里使用了混合模式,正常和透明度为50%
  6. [img1 drawInRect:CGRectMake(0, 0, 150, 150) blendMode:kCGBlendModeNormal alpha:.5];
  7. [img2 drawInRect:CGRectMake(50, 50, 150, 150)];
  8. [img3 drawInRect:CGRectMake(150, 150, 150, 150)];
  9. UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
  10. UIGraphicsEndImageContext();
  11. /*---------------------------------*/
  12. UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 300, 300)];
  13. [imageView setImage:im];
  14. [self.view addSubview:imageView];

画线

  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
  2. //DrawingLine
  3. [[UIColor brownColor] set]; //设置上下文使用的颜色
  4. CGContextRef context = UIGraphicsGetCurrentContext();
  5. CGContextSetLineWidth(context, 2.0f);
  6. CGContextMoveToPoint(context, 0.0f, 150.0f); // 画笔移动到某点
  7. CGContextAddLineToPoint(context, 200.0f, 150.0f);
  8. CGContextStrokePath(context); //执行绘制
  9. UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
  10. UIGraphicsEndImageContext();
  11. /*---------------------------------*/
  12. UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
  13. [imageView setImage:im];
  14. [self.view addSubview:imageView];

连续绘制线条,并设置交界样式

上下文在绘制一条直线后会定格在刚才绘制的线的末端,这样我们可以再通过CGContextAddLineToPoint方法来设置立即进行下一条线的绘制。在线的交界处,我们可以设置交界的样式(CGLineJoin),这个枚举中有三种样式:

  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
  2. CGContextRef context = UIGraphicsGetCurrentContext();
  3. //DrawingLinesContinuously
  4. CGContextSetLineWidth(context, 6.0f);
  5. CGContextSetLineJoin(context, kCGLineJoinRound); //线条交汇处样式:圆角
  6. CGContextMoveToPoint(context, 20.0f, 150.0f);
  7. CGContextAddLineToPoint(context, 20.0f, 80.0f);
  8. CGContextAddLineToPoint(context, 200.0f, 80.0f);
  9. CGContextStrokePath(context);
  10. UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
  11. UIGraphicsEndImageContext();
  12. /*---------------------------------*/
  13. UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
  14. [imageView setImage:im];
  15. [self.view addSubview:imageView];

绘制矩形

  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
  2. CGContextRef context = UIGraphicsGetCurrentContext();
  3. //DrawingRect
  4. CGRect strokeRect = CGRectMake(0, 85, 200, 60);
  5. CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
  6. CGContextSetLineWidth(context, 2.0f);
  7. CGContextStrokeRect(context, strokeRect);
  8. UIColor *clearRed = [UIColor colorWithRed:0.5 green:0.0 blue:0.0 alpha:0.2];
  9. CGContextSetFillColorWithColor(context, clearRed.CGColor);
  10. CGContextFillRect(context, strokeRect);
  11. UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
  12. UIGraphicsEndImageContext();
  13. /*---------------------------------*/
  14. UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 250, 200)];
  15. imageView.contentMode = UIViewContentModeScaleAspectFit;
  16. [imageView setImage:im];
  17. [self.view addSubview:imageView];

绘制线性渐变效果

  1. UIGraphicsBeginImageContextWithOptions(CGSizeMake(200,200), NO, 0);
  2. CGContextRef context = UIGraphicsGetCurrentContext();
  3. CGRect strokeRect = CGRectMake(0, 85, 200, 60);
  4. //DrawingRect
  5. CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
  6. NSArray *colors = @[(__bridge id)[UIColor colorWithRed:0.3 green:0.0 blue:0.0 alpha:0.2].CGColor,
  7. (__bridge id)[UIColor colorWithRed:0.0 green:0.0 blue:1.0 alpha:0.8].CGColor];
  8. const CGFloat locations[] = {0.0, 1.0};
  9. CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
  10. CGPoint startPoint = CGPointMake(CGRectGetMinX(strokeRect), CGRectGetMinY(strokeRect)); //矩形最小x,y
  11. CGPoint endPoint = CGPointMake(CGRectGetMaxX(strokeRect), CGRectGetMaxY(strokeRect)); //矩形最大x,y
  12. CGContextSaveGState(context);
  13. CGContextAddRect(context, strokeRect);
  14. CGContextClip(context);
  15. CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0); //开始绘制
  16. CGContextRestoreGState(context);
  17. //释放资源
  18. CGGradientRelease(gradient);
  19. CGColorSpaceRelease(colorSpace);
  20. UIImage* im = UIGraphicsGetImageFromCurrentImageContext();
  21. UIGraphicsEndImageContext();
  22. /*---------------------------------*/
  23. UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 100, 250, 200)];
  24. imageView.contentMode = UIViewContentModeScaleAspectFit;
  25. [imageView setImage:im];
  26. [self.view addSubview:imageView];

离屏渲染

离屏渲染有两种方式:

所以说有时候CPU渲染其实是把一部分GPU的操作转移到了CPU上,节省了GPU的时间,在因为GPU渲染时间过长造成的卡顿中我们就可以使用这种方式防止卡顿。为什么会这样呢?原因如下:

在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等。随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染。随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

从上面的图中可以看到,CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化。

参考

UIKit和Core Graphics绘图——字符串,线条,矩形,渐变

iOS核心动画高级技巧

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注