普通视图
08-iOS 多媒体技术|图形编程框架OpenGL、OpenGL ES、Metal要点回顾【渲染流水线、坐标系、着色器程序、GLSL语言等】
07-iOS 多媒体技术| 滤镜框架Core Image、GPUImage要点回顾【滤镜链、内置滤镜、自定义滤镜、GPUImage的简单使用等】
06-iOS 多媒体技术| Core Graphics要点回顾【UIBezierPath绘制、CGContext绘制、图像处理、富文本处理、经典第三方库等】
前言
我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:
- 进而 用 两篇文章 对 其中的
UIKit相关要点 进行了分述:- 然后我们 针对 Core Animation框架的要点 进一步展开分述:
- 我们 在此篇文章 ,将 针对 2D绘图引擎Core Graphics框架的要点 进一步展开分述:
一、Core Graphics 简介
Core Graphics(Quartz 2D)是iOS和macOS平台上的2D绘图引擎,它是一套C-based API,用于实现图形绘制、图像处理、文本渲染、自定义UI控件等功能。
Core Graphics在开发中比较常用的是截屏/裁剪/自定义UI控件,Core Graphics在iOS开发中的主要价值是自定义UI控件‘
-
Core Graphics(使用Quartz 2D引擎)- 当开发者需要在
运行时创建图像时,可以使用 Core Graphics 去绘制,运行时实时计算、绘制一系列图像帧来实现动画。 - 与之相对的是
运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)
- 当开发者需要在
-
Quartz 2D- Quartz 2D是Core Graphics中的2D 绘制呈现引擎。
- Quartz 2D能够与所有的图形和动画技术(如
Core Animation,OpenGL ES, 和UIKit等)一起使用。Quartz 2D采用paint模式进行绘制。
1. 几个重要概念
1.1 Page(画布)
Quartz 2D在图像中使用了绘画者模型(painter’s model)。在绘画者模型中,简单说,当前的图层A,在上面添加图层B,A被B遮盖的地方将不会显示出来,而只显示A未被覆盖的地方和B层。
Page可以是一张纸(如果输出设备是打印机),也可以是虚拟的纸张(如果输出设备是PDF文件),还可以是bitmap图像。这根据实际使用的graphics context而定。
1.2 Path(路径)
Path(路径)可以理解成🖌画笔,我们在画布上绘制,是通过Path来处理的。在UIKit中,有一个UIBezierPath也是专门用于绘制各种图像的。
- 路径是由直线、曲线、圆弧等基本形状组成的图形对象。
- 可以使用路径来描述和绘制各种图形,包括简单的几何图形和复杂的曲线图形。
1.3 Graphics Context(图形上下文)
图形上下文(CGContext)是一种不透明数据类型,我们不需要关心它的实现以及它是怎么绘制的,只需把我们要绘制的内容告诉它即可。
- Core Graphics使用图形上下文(Graphics Context)来管理绘图环境的状态和属性。
- 通过设置
图形上下文的属性来控制绘图效果,如颜色、线宽、填充模式等 - Quartz提供了以下几种类型的Graphics Context绘图上下文,详细的介绍将在后续章节说明:
-
位图图形上下文(Bitmap Graphics Context)
位图图形上下文用于将RGB图像,GMYK图像或者黑白图像绘制到一个位图(bitmap)对象中。 -
PDF图形上下文(PDF Graphics Context)
PDF图形上下文可以帮助开发者创建PDF文件,将内容绘制进PDF文件中,其与位图上下文最大的区别在于PDF数据可以保存多页图像。 -
窗口上下文(Window Graphics Context)
用于OS系统中的窗口绘制。 -
图层上下文(Layer Context)
用于将内容绘制在Layer图层上。 -
打印上下文(Post Graphics Context)
使用Mac打印功能时,此上下文用于将内容绘制在打印输出源上。
-
位图图形上下文(Bitmap Graphics Context)
- 在UIView中,系统会默认创建一个Layer Graphics Context,它对应UIView的layer属性
- 该图形上下文可以在drawRect:方法中获取,开发者只能获取,不能自己重新创建
- 在该图层上下文中绘制的图形,最终会通过CALayer显示出来
- 因此,View之所以能显示东西,完全是因为它内部的layer
获取当前上下文
- 在iOS开发中,视图显示在屏幕上及它的内容需要更新时会被调用drawRect:方法。
- 我们只需要实现drawRect:方法,并且把我们需要绘制的内容提供给获取到的上下文就可以实现绘制。
- (void)drawRect:(CGRect)rect {
// 获取当前上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
...
}
设置绘图的上下文(context)
- UIGraphicsPushContext(context) :把context压入栈中,并把context设置为当前绘图上下文
- UIGraphicsPopContext() :将栈顶的上下文弹出,恢复先前的上下文,但是绘图状态不变
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
UIGraphicsPushContext(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
UIGraphicsPopContext();
UIRectFill(CGRectMake(100, 100, 100, 100)); // black color
}
设置绘图的状态(state)
- CGContextSaveGState(context) :保存context的状态
- CGContextRestoreGState:恢复保存context的状态
- (void)drawRect:(CGRect)rect {
[[UIColor redColor] setFill];
CGContextSaveGState(UIGraphicsGetCurrentContext());
[[UIColor blackColor] setFill];
CGContextRestoreGState(UIGraphicsGetCurrentContext());
UIRectFill(CGRectMake(100, 100, 100, 100)); // red color
}
1.4 坐标系
- Core Graphics使用笛卡尔坐标系,原点在左下角,x轴向右延伸,y轴向上延伸。
- 开发者可以通过坐标变换来实现坐标系的转换和调整,实现复杂的绘图效果和布局。
UIKit的坐标与Core Graphics的坐标是不一样的,UIKit的坐标默认原点在左上角,而Core Graphics的原点在左下角
![]()
通常两个坐标系之间是需要转换的,左图是修改前,右图是修改后: ![]()
那为什么我们在drawRect方法中使用CoreGraphics方法绘制内容的时候可以使用UIKit的坐标系?
因为iOS系统在drawRect返回CGContext的时候,默认帮我们进行了一次变换,以方便开发者直接用UIKit坐标系进行渲染。
转换坐标的API
/*
平移坐标系统。该方法相当于把原来位于 (0, 0) 位置的坐标原点平移到 (tx, ty) 点。
在平移后的坐标系统上绘制图形时,所有坐标点的 X 坐标都相当于增加了 tx,
所有点的 Y 坐标都相当于增加了 ty。
*/
CGContextTranslateCTM(CGContextRef _Nullable c, CGFloat tx, CGFloat ty)
/*
缩放坐标系统。该方法控制坐标系统水平方向上缩放 sx,垂直方向上缩放 sy。
在缩放后的坐标系统上绘制图形时,所有点的 X 坐标都相当于乘以 sx 因子,
所有点的 Y 坐标都相当于乘以 sy 因子。
*/
CGContextScaleCTM(CGContextRef _Nullable c, CGFloat sx, CGFloat sy)
/*
旋转坐标系统。该方法控制坐标系统旋转 angle 弧度。
在缩放后的坐标系统上绘制图形时,所有坐标点的 X、Y 坐标都相当于旋转了 angle弧度之后的坐标。
*/
CGContextRotateCTM(CGContextRef _Nullable c, CGFloat angle)
2. 功能概述
2.1. 绘图功能:
- Core Graphics提供了丰富的绘图功能,包括:
- 绘制形状(
直线、矩形、椭圆等) - 绘制路径(
自定义路径、曲线路径等) - 绘制图像(
位图图像、矢量图像等) - ...
- 绘制形状(
- 开发者可以在不同类型的上下文中进行绘制操作,创建自定义的绘图效果,实现各种绘图需求
2.2. 图像处理:
- Core Graphics支持图像的
加载、绘制、裁剪、变换、合成、渲染等操作 实现图像的特效和优化
2.3. 文本渲染:
- Core Graphics提供了文本渲染功能,可以渲染
文本内容,可以控制文本样式、排版布局等实现文本的自定义渲染。
2.4. 颜色和渲染:
- Core Graphics支持颜色管理和渲染操作,可以设置
填充颜色、描边颜色、渐变色等。
2.5. 图形变换:
- Core Graphics提供了图形变换的功能,可以实现
平移、旋转、缩放等变换操作。
2.6. 性能优化:
- Core Graphics利用GPU硬件加速来实现图形渲染和处理,提高绘图效率和性能。
- 开发者可以通过合理使用Core Graphics的API和功能来优化绘图性能,实现流畅的图形绘制和处理
3. 核心类介绍
Core Graphics(Quartz 2D)框架中的核心类主要包括以下几个:
3.1. CGContext:
- CGContext是Core Graphics中的
绘图上下文,用于执行绘图操作和渲染图形。 - CGContext提供了绘制路径、图像、文本等的功能,是实现图形绘制的核心类。
CGContextAPI简介:
-
CGContextSaveGState:压栈操作,
保存一份当前图形上下文 -
CGContextRestoreGState:出栈操作,
恢复一份当前图形上下文
3.1.1 图形上下文的坐标空间变换
CTM(current transformation matrix当前转换矩阵)
- CGContextScaleCTM:坐标系X,Y缩放
- CGContextTranslateCTM:坐标系平移
- CGContextRotateCTM:坐标系旋转
- CGContextConcatCTM:
- CGContextGetCTM:获得一份CTM
3.1.2 设置Line属性及连接样式
- CGContextSetLineWidth: 设置线宽
- CGContextSetLineCap: 用于设置线条的端点样式
- CGContextSetLineJoin: 用于设置路径的线条连接样式
- CGContextSetMiterLimit:设置斜角连接的限制比例
- CGContextSetLineDash: 设置虚线样式
- CGContextSetFlatness: 设置线条的平直度
- CGContextSetAlpha: 设置透明度
CGContextSetAlpha(context, 0.2)
- CGContextSetBlendMode:设置图形上下文的混合模式
3.1.3 设置Path绘制
- CGContextBeginPath:告诉图形上下文开始新的路径,以便后续的路径操作可以添加到这个新路径中
- CGContextMoveToPoint:画笔移动到该点开始画线
- CGContextAddLineToPoint:画直线到该点
- CGContextAddCurveToPoint:画三次曲线函数
CGContextMoveToPoint(context, 200, 300);//设置Path的起点
CGContextAddCurveToPoint(context,250, 280, 250, 400, 280, 300);//设置贝塞尔曲线的控制点坐标和控制点坐标终点坐标
CGContextStrokePath(context);
- CGContextAddQuadCurveToPoint:画二次曲线
CGContextMoveToPoint(context, 120, 300);//设置Path的起点
CGContextAddQuadCurveToPoint(context,190, 310, 120, 390);//设置贝塞尔曲线的控制点坐标和终点坐标
CGContextStrokePath(context);
- CGContextClosePath:闭合曲线
- CGContextAddRect:画矩形
CGContextAddRect(context,CGRectMake(140, 120, 60, 30));//画方框
- CGContextAddRects:
- CGContextAddLines:
/*画三角形*/
//只要三个点就行跟画一条线方式一样,把三点连接起来
CGPoint sPoints[3];//坐标点
sPoints[0] =CGPointMake(100, 220);//坐标1
sPoints[1] =CGPointMake(130, 220);//坐标2
sPoints[2] =CGPointMake(130, 160);//坐标3
CGContextAddLines(context, sPoints, 3);//添加线
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFillStroke); //根据坐标绘制路径
- CGContextAddEllipseInRect:
//画椭圆
CGContextAddEllipseInRect(context, CGRectMake(160, 180, 20, 8)); //椭圆
CGContextDrawPath(context, kCGPathFillStroke);
- CGContextAddArc:
- CGContextAddArcToPoint:
/*画圆角矩形*/
float fw = 180;
float fh = 280;
CGContextMoveToPoint(context, fw, fh-20); // 开始坐标右边开始
CGContextAddArcToPoint(context, fw, fh, fw-20, fh, 10); // 右下角角度
CGContextAddArcToPoint(context, 120, fh, 120, fh-20, 10); // 左下角角度
CGContextAddArcToPoint(context, 120, 250, fw-20, 250, 10); // 左上角
CGContextAddArcToPoint(context, fw, 250, fw, fh-20, 10); // 右上角
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFillStroke); //根据坐标绘制路径
- CGContextAddPath:
- CGContextCopyPath:
3.1.4 获取路径信息
- CGContextReplacePathWithStrokedPath:
- CGContextIsPathEmpty:表示目前的图形上下文是否包含任何的子路径。
bool isPathEmpty = CGContextIsPathEmpty(ctf);
- CGContextGetPathCurrentPoint:返回一个非空的路径中的当前点。
CGPoint currentPoint = CGContextGetPathCurrentPoint(ctf);// 获取当前画笔处于最后的那一个点
- CGContextGetPathBoundingBox:返回包含当前路径的最小矩形。
CGRect boxRect = CGContextGetPathBoundingBox(ctf);// 包含路径的最小矩形
- CGContextPathContainsPoint:检查当前路径中是否包含指定的点。
3.1.5 路径绘制
- CGContextStrokePath:
- CGContextDrawPath:
两者区别:
/*CGPathDrawingMode是填充方式,枚举类型
kCGPathFill:只有填充(非零缠绕数填充),不绘制边框
kCGPathEOFill:奇偶规则填充(多条路径交叉时,奇数交叉填充,偶交叉不填充)
kCGPathStroke:只有边框
kCGPathFillStroke:既有边框又有填充
kCGPathEOFillStroke:奇偶填充并绘制边框
*/
CGContextStrokePath(context); 直接在图形上下文中渲染路径
CGContextDrawPath(context, kCGPathFillStroke); //指定模式下渲染路径
-
CGContextFillPath://填充路径
-
CGContextEOFillPath://奇偶填充
-
CGContextFillRect://
-
CGContextStrokeRect:
/*画矩形*/
CGContextStrokeRect(context,CGRectMake(100, 120, 10, 10));//画方框
CGContextFillRect(context,CGRectMake(120, 120, 10, 10));//填充框
- CGContextFillRects:
- CGContextStrokeRectWithWidth:
- CGContextClearRect:
- CGContextFillEllipseInRect:
- CGContextStrokeEllipseInRect:
- CGContextStrokeLineSegments:
3.1.6 修改剪裁路径
- CGContextClip:
- CGContextEOClip:
- CGContextClipToMask:
- CGContextGetClipBoundingBox:
- CGContextClipToRect:
- CGContextClipToRects:
3.1.7 设置颜色、色彩空间及阴影值
-
Quartz 中的颜色是用一组数值来表示。而颜色空间用于解析这些颜色信息,常用颜色空间有 RGB 、CMYK等。
-
Quartz 支持通用颜色空间、设备独立颜色空间、设备依赖颜色空间、索引颜色空间和模式(Pattern)颜色空间。
-
iOS不支持设备独立颜色空间和通用颜色空间。iOS应用程序必须使用设备颜色空间。
-
CGContextSetFillColorWithColor://设置填充颜色
-
CGContextSetStrokeColorWithColor://设置描边颜色
-
调用如下函数来便捷的使用 CGColor 设置颜色值并使用 CGColor 指定的颜色空间
//设置填充颜色
UIColor *aColor = [UIColor blueColor];//blue蓝色
CGContextSetFillColorWithColor(context, aColor.CGColor);//填充颜色
//设置描边颜色
aColor = [UIColor yellowColor];
CGContextSetStrokeColorWithColor(context, aColor.CGColor);//线框颜色
3.1.8 创建设备依赖颜色空间
Quartz 中的颜色是用一组数值来表示。而颜色空间用于解析这些颜色信息,常用颜色空间有 RGB 、CMYK等。
Quartz 支持通用颜色空间、设备独立颜色空间、设备依赖颜色空间、索引颜色空间和模式(Pattern)颜色空间。
iOS不支持设备独立颜色空间和通用颜色空间。iOS应用程序必须使用设备颜色空间。
-
CGColorSpaceCreateDeviceGray() 创建设备依赖灰度颜色空间。
-
CGColorSpaceCreateDeviceRGB() 创建设备依赖RGB颜色空间。
-
CGColorSpaceCreateDeviceCMYK() 创建设备依赖CMYK颜色
空间。 -
CGContextSetFillColorSpace:
-
CGContextSetStrokeColorSpace:
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextSetFillColorSpace(context, colorSpace);
CGContextSetStrokeColorSpace(context, colorSpace);
- CGContextSetFillColor:
- CGContextSetStrokeColor:
//Device RGB.
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextSetRGBFillColor(context, 1, 0, 0, 1);
-
CGContextSetGrayFillColor:
-
CGContextSetGrayStrokeColor:
-
调用如下函数来便捷的设置设备依赖CMYK颜色空间并设置颜色值。
//Device Gray.
CGContextSetGrayStrokeColor(context, 0.5, 1);
CGContextSetGrayFillColor(context, 0.5, 1);
- CGContextSetRGBFillColor:
- CGContextSetRGBStrokeColor:
CGContextSetRGBStrokeColor (context, 142.0/ 255.0, 161.0/ 255.0, 189.0/ 255.0, 1.0);
-
CGContextSetCMYKFillColor:
-
CGContextSetCMYKStrokeColor:
-
调用如下函数来便捷的设置设备依赖CMYK颜色空间并设置颜色值。
//Device CMYK.
CGContextSetCMYKStrokeColor(context, 1, 0, 0, 0, 1);
CGContextSetCMYKFillColor(context, 1, 0, 0, 0, 1);
- CGContextSetRenderingIntent:设置再现意图
每个设备都有固定的可复制的颜色范围(gamut),这是设备的物理性质决定的。当图像从一个颜色空间向另一个颜色空间转换时,有些源设备颜色空间中呈现的颜色,不能在目标设备颜色空间中复制出来,这些不能复制的颜色叫色域外(out-of-gamut)颜色。比如 RGB 颜色空间比 CMYK 的颜色空间要大,有些在显示器上能显示的颜色不能在打印机上同样打印出来。因为我们不能在目标设备颜色空间中复制出色域外颜色,我们必须用一些其他颜色来替代他们。颜色空间转换时颜色替换调整的规则就是再现意图。更详细的说明可以查看 这里 和 这里
- 再现意图用于指定如何将源颜色空间的颜色映射到图形上下文的目标颜色空间的颜色范围内。
- 如果不显式的指定再现意图,Quartz 使用“相对色度再现意图”应用于所有绘制(不包含位图图像)。
- 对于位图图像,Quartz默认使用“感知再现意图”。
- 调用 CGContextSetRenderingIntent(context, kCGRenderingIntentDefault) 来设置再现意图。
- 再现意图共有以下 5 种
typedef CF_ENUM (int32_t, CGColorRenderingIntent) {
kCGRenderingIntentDefault,
kCGRenderingIntentAbsoluteColorimetric,
kCGRenderingIntentRelativeColorimetric,
kCGRenderingIntentPerceptual,
kCGRenderingIntentSaturation
};
1 kCGRenderingIntentDefault:默认再现意图。
2 kCGRenderingIntentAbsoluteColorimetric:绝对色度再现意图。将输出设备颜色域外的颜色映射为输出设备域内与之最接近的颜色。这可以产生一个裁减效果,因为色域外的两个不同的颜色值可能被映射为色域内的同一个颜色值。当图形使用的颜色值同时包含在源色域及目标色域内时,这种方法是最好的。常用于logo或者使用专色(spot color)时。
3 kCGRenderingIntentRelativeColorimetric:相对色度再现意图。转换所有的颜色(包括色域内的),以补偿图形上下文的白点与输出设备白点之间的色差。
4 kCGRenderingIntentPerceptual:感知再现意图。通过压缩图形上下文的色域来适应输出设备的色域,并保持源颜色空间的颜色之间的相对性。感知渲染意图适用于相片及其它复杂的高细度图片。
5 kCGRenderingIntentSaturation:饱和度再现意图。把颜色转换到输出设备色域内时,保持颜色的相对饱和度。结果是包含亮度、饱和度颜色的图片。饱和度意图适用于生成低细度的图片,如描述性图表。
- CGContextDrawImage:在指定区域画图片
/*图片*/
UIImage *image = [UIImage imageNamed:@"apple.jpg"];
[image drawInRect:CGRectMake(60, 340, 20, 20)];//在坐标中画出图片
// [image drawAtPoint:CGPointMake(100, 340)];//保持图片大小在point点开始画图片,可以把注释去掉看看
CGContextDrawImage(context, CGRectMake(100, 340, 20, 20), image.CGImage);
- CGContextDrawTiledImage:
- CGContextGetInterpolationQuality:返回当前的图形上下文的插值(插值(Interpolation)是在不天生像素的环境下增长图像像素大小的一种方法,在周围像素色彩
- 的根蒂根基上用算术公式计算亡失像素的色彩。)质量水平
- CGContextSetInterpolationQuality:设置图形上下文的插值质量水平。
3.1.9 设置阴影
- CGContextSetShadowWithColor:
- CGContextSetShadow:
阴影是如何工作的
Quartz中的阴影是图形状态的一部分。我们可以调用函数CGContextSetShadow来创建,并传入一个图形上下文、偏移值及模糊值。阴影被设置后,任何绘制的对象都有一个阴影,且该阴影在设备RGB颜色空间中呈现出黑色的且alpha值为1/3。换句话说,阴影是用RGBA值{0, 0, 0, 1.0/3.0}设置的。
我们可以调用函数CGContextSetShadowWithColor来设置彩色阴影,并传递一个图形上下文、 偏移值、模糊值有CGColor颜色对象。颜色值依赖于颜色空间。
如何在调用CGContextSetShadow或CGContextSetShadowWithColor之前保存了图形状态,我们可以通过恢复图形状态来关闭阴影。我们也可以通过设置阴影颜色为NULL来关闭阴影
阴影有三个属性:
x偏移值,用于指定阴影相对于图片在水平方向上的偏移值。
y偏移值,用于指定阴影相对于图片在竖直方向上的偏移值。
模糊(blur)值,用于指定图像是有一个硬边
CGContextSetShadow(context, CGSizeMake(10, -20), 10);
3.1.10 绘制渐变色
-
CGContextDrawLinearGradient:
-
CGContextDrawRadialGradient:
//第二种填充方式 CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB(); CGFloat colors[] = { 1,1,1, 1.00, 1,1,0, 1.00, 1,0,0, 1.00, 1,0,1, 1.00, 0,1,1, 1.00, 0,1,0, 1.00, 0,0,1, 1.00, 0,0,0, 1.00, }; CGGradientRef gradient = CGGradientCreateWithColorComponents (rgb, colors, NULL, sizeof(colors)/(sizeof(colors[0])*4));//形成梯形,渐变的效果 CGColorSpaceRelease(rgb); //画线形成一个矩形 //CGContextSaveGState与CGContextRestoreGState的作用 /* CGContextSaveGState函数的作用是将当前图形状态推入堆栈。之后,您对图形状态所做的修改会影响随后的描画操作,但不影响存储在堆栈中的拷贝。在修改完成后,您可以通过CGContextRestoreGState函数把堆栈顶部的状态弹出,返回到之前的图形状态。这种推入和弹出的方式是回到之前图形状态的快速方法,避免逐个撤消所有的状态修改;这也是将某些状态(比如裁剪路径)恢复到原有设置的唯一方式。 */ CGContextSaveGState(context); CGContextMoveToPoint(context, 220, 90); CGContextAddLineToPoint(context, 240, 90); CGContextAddLineToPoint(context, 240, 110); CGContextAddLineToPoint(context, 220, 110); CGContextClip(context);//context裁剪路径,后续操作的路径 //CGContextDrawLinearGradient(CGContextRef context,CGGradientRef gradient, CGPoint startPoint, CGPoint endPoint,CGGradientDrawingOptions options) //gradient渐变颜色,startPoint开始渐变的起始位置,endPoint结束坐标,options开始坐标之前or开始之后开始渐变 CGContextDrawLinearGradient(context, gradient,CGPointMake (220,90) ,CGPointMake(240,110), kCGGradientDrawsAfterEndLocation); CGContextRestoreGState(context);// 恢复到之前的context //再写一个看看效果 CGContextSaveGState(context); CGContextMoveToPoint(context, 260, 90); CGContextAddLineToPoint(context, 280, 90); CGContextAddLineToPoint(context, 280, 190); CGContextAddLineToPoint(context, 260, 190); CGContextClip(context);//裁剪路径 //说白了,开始坐标和结束坐标是控制渐变的方向和形状 CGContextDrawLinearGradient(context, gradient,CGPointMake (260, 90) ,CGPointMake(260, 190), kCGGradientDrawsAfterEndLocation); CGContextRestoreGState(context);// 恢复到之前的context //下面再看一个颜色渐变的圆 //参数1:图形上下文 //参数2:渐变色 //参数3:开始中心点 //参数4:开始半径 //参数5:结束中心点 //参数6:结束半径 //参数7:渲染模式 CGContextDrawRadialGradient(context, gradient, CGPointMake(400, 100), 0.0, CGPointMake(350, 100), 30, kCGGradientDrawsBeforeStartLocation); -
CGContextDrawShading:
3.1.11 绘制文本
- CGContextSetCharacterSpacing:
- CGContextSetTextPosition:
- CGContextGetTextPosition:
- CGContextSetTextMatrix:
- CGContextGetTextMatrix:
- CGContextSetTextDrawingMode:
- CGContextSetFont:
- CGContextSetFontSize:
- CGContextShowGlyphsAtPositions:
3.1.12 PDF
** CGContextDrawPDFPage**: 绘制一个PDF页面到当前的用户空间
3.1.13 建立一个基于页面的图形上下文
- CGContextBeginPage:
- CGContextEndPage:
3.1.14 管理图形上下文
- CGContextRetain:
- CGContextRelease:
- CGContextSynchronize:
3.1.15 锯齿功能
- CGContextSetShouldAntialias:设置图形上下文的抗锯齿开启或关闭。
- CGContextSetAllowsAntialiasing:
3.1.16 字体展示功能
- CGContextSetShouldSmoothFonts:
- CGContextSetAllowsFontSmoothing:
- CGContextSetShouldSubpixelPositionFonts:
- CGContextSetAllowsFontSubpixelPositioning:
- CGContextSetShouldSubpixelQuantizeFonts:
- CGContextSetAllowsFontSubpixelQuantization:
3.1.17 使用透明图层
Quartz的透明层类似于许多流行的图形应用中的层。层是独立的实体。Quartz维护为每个上下文维护一个透明层栈,并且透明层是可以嵌套的。但由于层通常是栈的一部分,所以我们不能单独操作它们。
我们通过调用函数CGContextBeginTransparencyLayer来开始一个透明层,该函数需要两个参数:图形上下文与CFDictionary对象。字典中包含我们所提供的指定层额外信息的选项,但由于Quartz 2D API中没有使用字典,所以我们传递一个NULL。在调用这个函数后,图形状态参数保持不变,除了alpha值[默认设置为1]、阴影[默认关闭]、混合模式[默认设置为normal]、及其它影响最终组合的参数。
在开始透明层操作后,我们可以绘制任何想显示在层上的对象。指定上下文中的绘制操作将被当成一个组合对象绘制到一个透明背景上。这个背景被当作一个独立于图形上下文的目标缓存。
当绘制完成后,我们调用函数CGContextEndTransparencyLayer。Quartz将结合对象放入上下文,并使用上下文的全局alpha值、阴影状态及裁减区域作用于组合对象。
在透明层中绘制需要三步:
- 调用函数CGContextBeginTransparencyLayer
- 在透明层中绘制需要组合的对象
- 调用函数CGContextEndTransparencyLayer
- CGContextBeginTransparencyLayer:直到相应的调用CGContextEndTransparencyLayer,在指定范围内的所有后续绘制操作组合到一个完全透明的背景(它被视为一个单独的目标缓冲区从上下文)
- CGContextBeginTransparencyLayerWithRect:
- CGContextEndTransparencyLayer:
//代码示例:
CGContextBeginTransparencyLayer(context, NULL);
CGFloat wd = 300;
CGFloat ht = 300;
CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
CGContextFillRect(context, CGRectMake (wd/3 + 50, ht/2, wd/4, ht/4));
CGContextEndTransparencyLayer(context);
3.1.18 用户空间与设备空间互换
- CGContextGetUserSpaceToDeviceSpaceTransform:
- CGContextConvertPointToDeviceSpace:
- CGContextConvertPointToUserSpace:
- CGContextConvertSizeToDeviceSpace:
- CGContextConvertSizeToUserSpace:
- CGContextConvertRectToDeviceSpace:
- CGContextConvertRectToUserSpace:
3.2. CGPath:
- CGPath是Core Graphics中
表示路径的类,用于创建和管理路径对象。 - CGPath可以包含
直线、曲线、矩形、椭圆等形状,用于定义绘制的轮廓和形状。 - CGPath能够被填充和stroke
常用API
在Objective-C中,CGPath是Core Graphics框架中用于创建和管理路径对象的类。下面是一些常用的CGPath相关的Objective-C API:
-
创建路径:
CGPathRef CGPathCreateMutable(void); CGPathRef CGPathCreateMutableCopy(CGPathRef path); CGPathRef CGPathCreateWithRect(CGRect rect, const CGAffineTransform *transform); CGPathRef CGPathCreateWithEllipseInRect(CGRect rect, const CGAffineTransform *transform); CGPathRef CGPathCreateWithRoundedRect(CGRect rect, CGFloat cornerWidth, CGFloat cornerHeight, const CGAffineTransform *transform); -
释放路径:
void CGPathRelease(CGPathRef path); -
添加路径元素:
void CGPathMoveToPoint(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y); void CGPathAddLineToPoint(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y); void CGPathAddRect(CGMutablePathRef path, const CGAffineTransform *m, CGRect rect); void CGPathAddEllipseInRect(CGMutablePathRef path, const CGAffineTransform *m, CGRect rect); void CGPathAddArc(CGMutablePathRef path, const CGAffineTransform *m, CGFloat x, CGFloat y, CGFloat radius, CGFloat startAngle, CGFloat endAngle, bool clockwise); -
其他操作:
bool CGPathContainsPoint(CGPathRef path, const CGAffineTransform *m, CGPoint point, bool eoFill); CGRect CGPathGetBoundingBox(CGPathRef path); bool CGPathEqualToPath(CGPathRef path1, CGPathRef path2); -
路径属性:
CGPathRef CGPathCreateCopyByTransformingPath(CGPathRef path, const CGAffineTransform *transform); CGPathRef CGPathCreateCopyByDashingPath(CGPathRef path, const CGAffineTransform *transform, CGFloat phase, const CGFloat *lengths, size_t count); CGPathRef CGPathCreateCopyByStrokingPath(CGPathRef path, const CGAffineTransform *tran
3.3. CGImage:
- CGImage是Core Graphics中
表示图像的类,用于加载、创建和处理位图图像。 - CGImage可以从文件、数据或其他来源创建,用于图像的绘制和处理。
常用API
-
创建
CGImage:
CGImageRef CGImageCreate(size_t width,
size_t height,
size_t bitsPerComponent,
size_t bitsPerPixel,
size_t bytesPerRow,
CGColorSpaceRef space,
CGBitmapInfo bitmapInfo,
CGDataProviderRef provider,
const CGFloat *decode,
bool shouldInterpolate,
CGColorRenderingIntent intent);
- 从UIImage创建CGImage:
CGImageRef CGImageRetain(UIImage *image);
- 释放CGImage:
void CGImageRelease(CGImageRef image);
- 获取CGImage的属性:
size_t CGImageGetWidth(CGImageRef image);
size_t CGImageGetHeight(CGImageRef image);
size_t CGImageGetBitsPerComponent(CGImageRef image);
size_t CGImageGetBitsPerPixel(CGImageRef image);
size_t CGImageGetBytesPerRow(CGImageRef image);
CGColorSpaceRef CGImageGetColorSpace(CGImageRef image);
CGBitmapInfo CGImageGetBitmapInfo(CGImageRef image);
CGDataProviderRef CGImageGetDataProvider(CGImageRef image);
- 绘制CGImage:
void CGContextDrawImage(CGContextRef c, CGRect rect, CGImageRef image);
- 从CGImage创建UIImage:
UIImage *UIImageWithCGImage(CGImageRef cgImage);
- 从CGImage创建CIImage:
CIImage *CIImageWithCGImage(CGImageRef image);
3.4. CGColor:
- CGColor是Core Graphics中
表示颜色的类,用于定义绘制和填充的颜色。 - CGColor可以表示RGB、RGBA、灰度等颜色空间,用于设置绘制和填充的颜色值。
-
CGColor和CGColorSpace;用来进行颜色和颜色空间管理;
3.5. CGGradient:
- CGGradient是Core Graphics中
表示渐变的类,用于创建和管理颜色渐变效果。 - CGGradient可以
定义线性渐变、径向渐变等效果,用于实现丰富的颜色渐变效果。 -
CGShading和CGGradient:用于绘制剃度;
3.6. CGFont:
- CGFont是Core Text框架中
表示字体的类,用于处理文本的字体和排版。 - CGFont可以设置字体的
样式、大小、粗细等属性,用于文本的渲染和显示。
3.7 其他常用类:
-
CGLayer:用来表示一个能够用于重复绘制和offscreen绘制的绘制层; -
CGPattern:用来表示Pattern,用于重复绘制; -
CGPDFContentStream、CGPDFScanner、CGPDFPage、CGPDFObject,CGPDFStream,CGPDFString等用来进行pdf文件的创建、解析和显示
二、CGContext常用功能
在日常开发中,一般绘图都是用UIBezierPath居多,偶尔也会与CGContext配合绘图。因此,此处列举CGContext绘图只是一些简单、常规的使用方法。
- 推荐一篇关于
CGContext的变换过程好文: CGContext的变换过程详解
1. 绘制曲线
- 获取当前控件的图形上下文
- 描述绘画图形内容
a. 创建图形起始点
b. 添加控制点和终点 - 设置图形上下文状态
- 渲染图形上下文
- (void)drawRect:(CGRect)rect {
// 1. 获取当前控件的图形上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 2. 描述绘画图形内容
// a. 创建图形起始点
CGContextMoveToPoint(context, 0, 0);
// b. 添加控制点和终点,控制点(300,200),终点(0,250)
CGContextAddQuadCurveToPoint(context, 300, 200, 0, 250);
// 3. 设置图形上下文状态
// 设置颜色
[[UIColor redColor] set];
// 设置线宽
CGContextSetLineWidth(context, 10);
// 4. 渲染图形上下文
CGContextStrokePath(context);
}
2. 绘制图形
常见问题:
- 关闭路径closePath:从路径的终点连接到起点
- 填充路径CGContextFillPath:有了封闭的路径就能填充。
- 设置填充颜色 [[UIColor blueColor] setFill];
- 设置描边颜色 [[UIColor redColor] setStroke];
- 不显示描边颜色,为什么?没有设置线宽
- 设置线宽,还是不显示,为什么?因为绘制路径不对。
- 即填充又描边CGContextDrawPath:kCGPathFillStroke。
- 圆的起点在圆右边水平线 (一). 圆角矩形
// RoundedRect: 坐标与宽高
// cornerRadius: 角半径
UIBezierPath *oblongPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(50, 50, 100, 150) cornerRadius:100];
// 颜色
[[UIColor redColor] set];
// 填充
[oblongPath fill];
(二). 扇形
// 扇形
// Center:圆心
// startAngle:弧度
// clockwise:YES:顺时针 NO:逆时针
CGPoint center = CGPointMake(100, 100);
UIBezierPath *sectorPath = [UIBezierPath bezierPathWithArcCenter:center radius:50 startAngle:0 endAngle:M_PI clockwise:YES];
// 从终点连线到圆心
[sectorPath addLineToPoint:center];
// 颜色
[[UIColor blueColor] set];
// // 关闭路径,从终点连线到起始点
// [sectorPath closePath];
// // 描边
// [sectorPath stroke];
// 填充:必须是一个完整的封闭路径,默认就会自动关闭路径
[sectorPath fill];
3. 图片水印
- 开启位图上下文
- 描述绘画内容
a. 绘制图片
b. 绘制文字
c. 绘制图形等 - 从位图上下文获取生成的图片
- 关闭位图上下文
- (void)viewDidLoad {
[super viewDidLoad];
// 创建图片
UIImage *logoImage = [UIImage imageNamed:@"小黄人"];
// 1. 开启位图上下文
// 注意: 位图上下文跟view无关联,所以不需要在drawRect中获取上下文
// size: 位图上下文的尺寸(绘制出新图片的尺寸)
// opaque: 是否透明,YES:不透明 NO:透明,通常设置成透明的上下文
// scale: 缩放上下文,取值0表示不缩放,通常不需要缩放上下文
UIGraphicsBeginImageContextWithOptions(logoImage.size, NO, 0);
// 2. 描述绘画内容
// 绘制原生图片
[logoImage drawAtPoint:CGPointZero];
// 绘制文字
NSString *logo = @"小黄人";
// 创建字典属性
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSForegroundColorAttributeName] = [UIColor redColor];
dict[NSFontAttributeName] = [UIFont systemFontOfSize:20];
[logo drawAtPoint:CGPointMake(51, 27) withAttributes:dict];
// 绘制图形
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 30, 20, 20)];
[[UIColor redColor] set];
[path fill];
// 3. 从上下文获取生成的图片
_logoImage.image = UIGraphicsGetImageFromCurrentImageContext();
// 4. 关闭位图上下文
UIGraphicsEndImageContext();
}
提示:
- 位图上下文跟view无关联,所以不需要在drawRect中获取上下文
- UIGraphicsGetCurrentContext()函数可以获取的不同类型的上下文,CGContextRef变量可以指向不同类型上下文
- 在位图上下文上绘制图形,必须获取位图上下文生成的图片,再显示图片才可以看见绘制的图形
- 位图上下文的获取方式跟layer上下文不一样,位图上下文需要手动创建
4. 图片裁剪
- 开启位图上下文
- 设置裁剪区
- 绘制图片
- 从位图上下文获取生成的图片
- 关闭位图上下文
- (void)viewDidLoad {
[super viewDidLoad];
// 创建图片
UIImage *image = [UIImage imageNamed:@"阿狸头像"];
// 1. 开启上下文
UIGraphicsBeginImageContextWithOptions(image.size, NO, 0);
// 2. 设置裁剪区
// 创建图形路径
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, image.size.width, image.size.height)];
// 把图形路径设置为裁剪区
[path addClip];
// 3. 绘制图形
[image drawAtPoint:CGPointZero];
// 4. 从位图上下文获取图片
_SJMImage.image = UIGraphicsGetImageFromCurrentImageContext();
// 5. 关闭上下文
UIGraphicsEndImageContext();
}
5. 屏幕截屏
- 开启一个位图上下文
- 获取位图上下文
- 把屏幕上的图层渲染到图形上下文
- 从位图上下文获取图片
- 关闭上下文
- 存储图片
- (void)viewDidLoad {
// 1. 开启一个位图上下文
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, NO, 0);
// 2. 获取位图上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 3. 把屏幕上的图层渲染到图形上下文
[self.view.layer renderInContext:ctx];
// 4. 从位图上下文获取图片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 5. 关闭上下文
UIGraphicsEndImageContext();
// 6. 存储图片
// image转data
// compressionQuality:图片质量 1:最高质量
NSData *data = UIImageJPEGRepresentation(image,1);
[data writeToFile:@"/Users/SJM/Desktop/view.png" atomically:YES];
}
三、UIBezierPath绘图
1. UIBezierPath简介
UIBezierPath这个类在UIKit中, 是Core Graphics框架关于path的一个封装,使用此类可以定义简单的形状,比如我们常用到,矩形,圆形,椭圆,弧,或者不规则的多边形
贝塞尔曲线的数理知识
1.1 一次贝塞尔曲线
给定点P0,P1一次贝塞尔曲线只是一条两点之间的直线。这条线由下式给出:
![]()
一次贝塞尔曲线函数中的t会经由起始点P0至终止点P1的B(t)所描述的曲线。例如当t=0.25时,B(t)即一条由点P0至P1路径的四分之一处。就像由0至1的连续t,B(t)描述一条由P0至P1的直线.
![]()
1.2 二次贝塞尔曲线
二次方贝塞尔曲线的路径由给定点P0、P1、P2的函数B(t)追踪:
![]()
原理:由 P0 至 P1 的连续点 Q0,描述一条线段。
由 P1 至 P2 的连续点 Q1,描述一条线段。
由 Q0 至 Q1 的连续点 B(t),描述一条二次贝塞尔曲线。
经验:P1-P0为曲线在P0处的切线。
![]()
1.3 三次贝塞尔曲线
对于三次曲线,可由线性贝塞尔曲线描述的中介点Q0、Q1、Q2,和由二次曲线描述的点R0、R1所建构:
![]()
![]()
更多关于贝塞尔曲线的介绍,可参考维基百科的相关介绍
从下面这篇文章可以了解控制点与弧线的关系
1.4 初始化方法
// 初始化方法,需要用实例方法添加线条.使用比较多,可以根据需要任意定制样式,画任何我们想画的图形.
+ (instancetype)bezierPath;
// 返回一个矩形 path
+ (instancetype)bezierPathWithRect:(CGRect)rect;
// 返回一个圆形或者椭圆形 path
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
// 返回一个带圆角的矩形 path ,矩形的四个角都是圆角;
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius;
// 返回一个带圆角的矩形 path , UIRectCorner 枚举值可以设置只绘制某个圆角;
+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
// 返回一段圆弧,参数说明: center: 弧线中心点的坐标 radius: 弧线所在圆的半径 startAngle: 弧线开始的角度值 endAngle: 弧线结束的角度值 clockwise: 是否顺时针画弧线.
+ (instancetype)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 用一条 CGpath 初始化
+ (instancetype)bezierPathWithCGPath:(CGPathRef)CGPath;
// 返回一个反转当前路径的路径对象.(反方向绘制path)
- (UIBezierPath *)bezierPathByReversingPath;
1.5 UIBezierPath 常用属性
1.CGPath: 将UIBezierPath类转换成CGPath
2.currentPoint: 当前path的位置,可以理解为path的终点
3.lineWidth: 线条宽度
4.lineCapStyle: 端点样式
5.lineJoinStyle: 连接类型
6.flatness: 绘线的精细程度,默认为0.6,数值越大,需要处理的时间越长
7.usesEvenOddFillRule: 判断奇偶数组的规则绘制图像,图形复杂时填充颜色的一种规则。类似棋盘
8.miterLimit: 最大斜接长度(只有在使用kCGLineJoinMiter是才有效,最大限制为10), 边角的角度越小,斜接长度就会越大,为了避免斜接长度过长,使用lineLimit属性限制,如果斜接长度超过miterLimit,边角就会以KCALineJoinBevel类型来显示
9.- setLineDash:count:phase:为path绘制虚线,dash数组存放各段虚线的长度,count是数组元素数量,phase是起始位置
// lineCapStyle // 端点类型
kCGLineCapButt, // 无端点
kCGLineCapRound, // 圆形端点
kCGLineCapSquare // 方形端点(样式上和kCGLineCapButt是一样的,但是比kCGLineCapButt长一点)
// lineJoinStyle // 交叉点的类型
kCGLineJoinMiter, // 尖角衔接
kCGLineJoinRound, // 圆角衔接
kCGLineJoinBevel // 斜角衔接
1.6 UIBezierPath 构建Path
- (void)moveToPoint:(CGPoint)point;
// 以 point点 开始作为起点, 一般用`+ (instancetype)bezierPath`创建的贝塞尔曲线,先用该方法标注一个起点,再调用其他的创建线条的方法来绘制曲线
// 绘制二次贝塞尔曲线的关键方法,即从path的最后一点开始添加一条线到point点
- (void)addLineToPoint:(CGPoint)point;
// 绘制二次贝塞尔曲线的关键方法,和`-moveToPoint:`配合使用. endPoint为终止点,controlPoint为控制点.
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint;
// 绘制三次贝塞尔曲线的关键方法,以三个点画一段曲线. 一般和moveToPoint:配合使用.
// 其中,起始点由`-moveToPoint:`设置,终止点位为`endPoint:`, 控制点1的坐标controlPoint1,控制点2的坐标是controlPoint2.
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2;
// 绘制一段圆弧, center:原点坐标,radius:半径,startAngle:起始角度,endAngle:终止角度,clockwise顺时针/逆时针方向绘制
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise;
// 闭合线
- (void)closePath;
// 移除所有的点,从而有效地删除所有子路径
- (void)removeAllPoints;
// 追加指定的bezierPath到路径上
- (void)appendPath:(UIBezierPath *)bezierPath;
// 用仿射变换矩阵变换路径的所有点
- (void)applyTransform:(CGAffineTransform)transform;
1.7 图形上下文中的路径操作
// 填充路径
- (void)fill;
// 各个点连线
- (void)stroke;
// 填充模式, alpha 设置
// blendMode : https://onevcat.com/2013/04/using-blending-in-ios/
- (void)fillWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
// 链接模式, alpha 设置
- (void)strokeWithBlendMode:(CGBlendMode)blendMode alpha:(CGFloat)alpha;
// 图形绘制超出当前路径范围,则不可见
- (void)addClip;
2. UIBezierPath 基本使用方法
UIBezierPath对象是CGPathRef数据类型的封装。path如果是基于矢量形状的,都用直线或曲线去创建。我们一般使用UIBezierPath都是在重写view的drawRect方法这种情形。我们用直线去创建矩形或多边形,使用曲线创建弧或者圆。创建和使用path对象步骤:
1、 重写View的drawRect方法
2、 创建UIBezierPath的对象
3、 使用方法moveToPoint: 设置初始点
4、 根据具体要求使用UIBezierPath类方法绘图(比如要画线、矩形、圆、弧?等)
5、 设置UIBezierPath对象相关属性 (比如lineWidth、lineJoinStyle、aPath.lineCapStyle、color)
6、 使用stroke 或者 fill方法结束绘图
比如我们想要画一条线demo:
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set]; //设置线条颜色
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(200, 80)];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound; //线条拐角
path.lineJoinStyle = kCGLineJoinRound; //终点处理
[path stroke];
}
![]()
3. 其他基本使用方法
在介绍其他使用方法之前,我们先来看一下 path的几个属性,以便下面我进行设置。
- 1、
[color set];设置线条颜色,也就是相当于画笔颜色 - 2、
path.lineWidth = 5.0;这个很好理解了,就是划线的宽度 - 3、
path.lineCapStyle这个线段起点是终点的样式,这个样式有三种:- 1、
kCGLineCapButt该属性值指定不绘制端点, 线条结尾处直接结束。这是默认值。 - 2、
kCGLineCapRound该属性值指定绘制圆形端点, 线条结尾处绘制一个直径为线条宽度的半圆。 - 3、
kCGLineCapSquare该属性值指定绘制方形端点。 线条结尾处绘制半个边长为线条宽度的正方形。需要说明的是,这种形状的端点与“butt”形状的端点十分相似,只是采用这种形式的端点的线条略长一点而已*
- 1、
- 4、
path.lineJoinStyle这个属性是用来设置两条线连结点的样式,同样它也有三种样式供我们选择- 1、
kCGLineJoinMiter斜接 - 2、
kCGLineJoinRound圆滑衔接 - 3、
kCGLineJoinBevel斜角连接
- 1、
- 5、
[path stroke];用 stroke 得到的是不被填充的 view ,[path fill];用 fill 得到的内部被填充的 view,这点在下面的代码还有绘制得到的图片中有,可以体会一下这两者的不同。
4. 绘制多边形
![]()
- 绘制多边形,实际上就是由一些直线条连成,主要使用
moveToPoint:和addLineToPoint:方法去创建 -
moveToPoint:这个方法是设置起始点,意味着从这个点开始 - 我们就可以使用
addLineToPoint:去设置我们想要创建的多边形经过的点,也就是两线相交的那个点 - 用
addLineToPoint:去创建一个形状的线段,我们可以连续创建line,每一个line的起点都是先前的终点,终点就是指定的点,将线段连接起来就是我们想要创建的多边形了。
#import "DrawPolygonView.h"
@implementation DrawPolygonView
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set]; //设置线条颜色
UIBezierPath* path = [UIBezierPath bezierPath];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound; //线条拐角
path.lineJoinStyle = kCGLineJoinRound; //终点处理
[path moveToPoint:CGPointMake(200.0, 50.0)];//起点
// Draw the lines
[path addLineToPoint:CGPointMake(300.0, 100.0)];
[path addLineToPoint:CGPointMake(260, 200)];
[path addLineToPoint:CGPointMake(100.0, 200)];
[path addLineToPoint:CGPointMake(100, 70.0)];
[path closePath];//第五条线通过调用closePath方法得到的
// [path stroke];//Draws line 根据坐标点连线
[path fill];//颜色填充
}
- 在这里我们可以看到最后第五条线是用
[path closePath];得到的 - closePath方法不仅结束一个shape的subpath表述,它也在最后一个点和第一个点之间画一条线段,这个一个便利的方法我们不需要去画最后一条线了。
- 这里我们用到的是
[path fill];//颜色填充进行坐标连点,但是我们看见的是五边形内部被颜色填充了 - 如果我们使用
[path stroke];那我们得到的就是一个用线画的五边形。
4.1 添加不规则图形layer,且配置点击事件
我们将上面的代码改一改,添加不规则图形layer,且配置点击事件
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set]; //设置线条颜色
UIBezierPath* path = [UIBezierPath bezierPath];
path.lineWidth = 3.0;
path.lineCapStyle = kCGLineCapRound; //线条拐角
path.lineJoinStyle = kCGLineJoinRound; //终点处理
[path moveToPoint:CGPointMake(200.0, 50.0)];//起点
// Draw the lines
[path addLineToPoint:CGPointMake(300.0, 100.0)];
[path addLineToPoint:CGPointMake(260, 200)];
[path addLineToPoint:CGPointMake(100.0, 200)];
[path addLineToPoint:CGPointMake(100, 70.0)];
[path closePath];//第五条线通过调用closePath方法得到的
[path stroke];//Draws line 根据坐标点连线
// [path fill];//颜色填充
// 将path 设置为 自定义形状的layer的path.
CGRect boundingRect = path.bounds;
// 获取不规则图形的边界矩形
// 计算边界矩形的中心点坐标
CGPoint center = CGPointMake(CGRectGetMidX(boundingRect), CGRectGetMidY(boundingRect));
CAShapeLayer *layer = [CAShapeLayer layer];
layer.fillColor = [UIColor orangeColor].CGColor;
// 关键点1|设置Path设置形状(不规则图形)
layer.path = path.CGPath;
// 关键点2|设置尺寸
layer.bounds = boundingRect;
// 关键点3|设置位置
layer.position = center;
[self.layer addSublayer:layer];
}
// 点击不规则图形的点击事件处理
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
UITouch *touch = [touches.allObjects lastObject];
CGPoint loc = [touch locationInView:self];
CALayer *sublayer = [self.layer.sublayers firstObject];
if ([sublayer containsPoint:loc]) {
// if(l == self.diyLayer){
#if DEBUG
NSLog(@"点击了layer1");
#endif
} else {
NSLog(@"点击了其它区域1");
}
}
5. 画矩形或者正方形
![]()
- 使用
+ (UIBezierPath *)bezierPathWithRect:(CGRect)rect这个方法,设置好坐标 frame 就好了,就像我们创建 view 一样,好理解。
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set]; //设置线条颜色
UIBezierPath* path = [UIBezierPath bezierPathWithRect:CGRectMake(20, 20, 100, 80)];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound; //线条拐角
path.lineJoinStyle = kCGLineJoinRound; //终点处理
[path stroke];
}
6. 创建圆形或者椭圆形
![]()
- 使用
+ (UIBezierPath *)bezierPathWithOvalInRect:(CGRect)rect这个方法创建圆形或者椭圆形。 - 传入的rect矩形参数绘制一个内切曲线,如果我们传入的rect是矩形就得到矩形的内切椭圆,如果传入的是 正方形得到的就是正方形的内切圆。
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set];
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 30, 100, 100)];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[path stroke];
}
7. 创建一段弧线
使用+ (UIBezierPath *)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwis这个方法创建一段弧线,介绍一下这个方法中的参数:
/*
ArcCenter: 原点
radius: 半径
startAngle: 开始角度
endAngle: 结束角度
clockwise: 是否顺时针方向
*/
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set];
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:90 startAngle:0 endAngle:TO_RADIAUS(120) clockwise:YES];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[path stroke];
}
弧线的参考系:
![]()
8. 绘制二次贝塞尔曲线
![]()
- 使用
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint这个方法绘制二次贝塞尔曲线。 - 曲线段在当前点开始,在指定的点结束,
- 一个控制点的切线定义。下图显示了两种曲线类型的相似,以及控制点和curve形状的关系:
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set];
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
/*
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint
Parameters
endPoint
The end point of the curve.
controlPoint
The control point of the curve.
*/
[path moveToPoint:CGPointMake(40, 150)];
[path addQuadCurveToPoint:CGPointMake(140, 200) controlPoint:CGPointMake(20, 40)];
[path stroke];
}
9. 绘制三次贝塞尔曲线
![]()
使用这个方法绘制三次贝塞尔曲线
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
Parameters
这个方法绘制三次贝塞尔曲线。曲线段在当前点开始,在指定的点结束,两个控制点的切线定义。下图显示了两种曲线类型的相似,以及控制点和curve形状的关系:
![]()
- (void)drawRect:(CGRect)rect {
/*
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2
Parameters
endPoint
The end point of the curve.
controlPoint1
The first control point to use when computing the curve.
controlPoint2
The second control point to use when computing the curve.
*/
UIColor *color = [UIColor redColor];
[color set];
UIBezierPath *path = [UIBezierPath bezierPath];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
[path moveToPoint:CGPointMake(20, 200)];
[path addCurveToPoint:CGPointMake(260, 200) controlPoint1:CGPointMake(140, 0) controlPoint2:CGPointMake(140, 400)];
[path stroke];
}
10. 画带圆角的矩形
![]()
使用+ (instancetype)bezierPathWithRect:(CGRect)rect;这个方法绘制,这个方法和bezierPathWithRect:类似,绘制一个带内切圆的矩形。
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set]; //设置线条颜色
UIBezierPath* path = [UIBezierPath bezierPathWithRect:CGRectMake(20, 20, 100, 80)];
path.lineWidth = 5.0;
path.lineCapStyle = kCGLineCapRound; //线条拐角
path.lineJoinStyle = kCGLineJoinRound; //终点处理
[path stroke];
}
11. 指定矩形的某个角为圆角
![]()
使用+ (instancetype)bezierPathWithRoundedRect:(CGRect)rect byRoundingCorners:(UIRectCorner)corners cornerRadii:(CGSize)cornerRadii;
这个方法绘制。参数的意思:rect 绘制矩形的 frame,corners指定使哪个角为圆角,圆角类型为:
typedef NS_OPTIONS(NSUInteger, UIRectCorner) {
UIRectCornerTopLeft = 1 << 0,
UIRectCornerTopRight = 1 << 1,
UIRectCornerBottomLeft = 1 << 2,
UIRectCornerBottomRight = 1 << 3,
UIRectCornerAllCorners = ~0UL
};
用来指定需要设置的角。cornerRadii 圆角的半径
- (void)drawRect:(CGRect)rect {
UIColor *color = [UIColor redColor];
[color set];
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 100, 100) byRoundingCorners:UIRectCornerTopRight cornerRadii:CGSizeMake(20, 20)];
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
path.lineWidth = 5.0;
[path stroke];
}
四、图像处理
1. 图片拼接
// 注意, liangzhang图片的Rect若是发生重叠,则会导致图片覆盖
- (UIImage *)combine:(UIImage*)leftImage :(UIImage*)rightImage {
CGFloat width = leftImage.size.width * 2;
CGFloat height = leftImage.size.height;
CGSize offScreenSize = CGSizeMake(width, height);
UIGraphicsBeginImageContext(offScreenSize);
CGRect rect = CGRectMake(0, 0, width/2, height);
[leftImage drawInRect:rect];
rect.origin.x += width/2;
[rightImage drawInRect:rect];
UIImage* imagez = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return imagez;
}
2. 图片截取
/// 截图 保证清晰度
+ (UIImage *)editImageWithOldImage:(UIImage *)image OldFrame:(CGRect)oldFrame CropFrame:(CGRect)cropFrame Scale:(float)scale {
image = [self fixOrientation:image];
CGSize editSize = oldFrame.size;
CGSize imgSize = image.size;
CGFloat widthScale;
CGRect rct;
if (imgSize.width < editSize.width) {
widthScale = imgSize.width / editSize.width;
rct = CGRectMake(cropFrame.origin.x*widthScale, cropFrame.origin.y*widthScale, cropFrame.size.width*widthScale, cropFrame.size.height*widthScale);
} else {
widthScale = editSize.width / imgSize.width;
rct = CGRectMake(cropFrame.origin.x/widthScale, cropFrame.origin.y/widthScale, cropFrame.size.width/widthScale, cropFrame.size.height/widthScale);
}
CGPoint origin = CGPointMake(-rct.origin.x, -rct.origin.y);
UIImage *img = nil;
UIGraphicsBeginImageContextWithOptions(rct.size, NO, image.scale);
[image drawAtPoint:origin];
img = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return img;
}
/// 图片方向调整
+ (UIImage *)fixOrientation:(UIImage *)aImage {
// No-op if the orientation is already correct
if (aImage.imageOrientation == UIImageOrientationUp)
return aImage;
// We need to calculate the proper transformation to make the image upright.
// We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored.
CGAffineTransform transform = CGAffineTransformIdentity;
switch (aImage.imageOrientation) {
case UIImageOrientationDown:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, aImage.size.height);
transform = CGAffineTransformRotate(transform, M_PI);
break;
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
transform = CGAffineTransformRotate(transform, M_PI_2);
break;
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, 0, aImage.size.height);
transform = CGAffineTransformRotate(transform, -M_PI_2);
break;
default:
break;
}
switch (aImage.imageOrientation) {
case UIImageOrientationUpMirrored:
case UIImageOrientationDownMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.width, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
case UIImageOrientationLeftMirrored:
case UIImageOrientationRightMirrored:
transform = CGAffineTransformTranslate(transform, aImage.size.height, 0);
transform = CGAffineTransformScale(transform, -1, 1);
break;
default:
break;
}
// Now we draw the underlying CGImage into a new context, applying the transform
// calculated above.
CGContextRef ctx = CGBitmapContextCreate(NULL, aImage.size.width, aImage.size.height,
CGImageGetBitsPerComponent(aImage.CGImage), 0,
CGImageGetColorSpace(aImage.CGImage),
CGImageGetBitmapInfo(aImage.CGImage));
CGContextConcatCTM(ctx, transform);
switch (aImage.imageOrientation) {
case UIImageOrientationLeft:
case UIImageOrientationLeftMirrored:
case UIImageOrientationRight:
case UIImageOrientationRightMirrored:
// Grr...
CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.height,aImage.size.width), aImage.CGImage);
break;
default:
CGContextDrawImage(ctx, CGRectMake(0,0,aImage.size.width,aImage.size.height), aImage.CGImage);
break;
}
// And now we just create a new UIImage from the drawing context
CGImageRef cgimg = CGBitmapContextCreateImage(ctx);
UIImage *img = [UIImage imageWithCGImage:cgimg];
CGContextRelease(ctx);
CGImageRelease(cgimg);
return img;
}
示意图:
拍照后只取框内部分![]()
确认时只取框内部分
![]()
处理后就这样
![]()
3. 加水印
@interface UIImage(WaterMask)
- (UIImage*)addWaterMask:(NSString*)txt;
@end
@implementation UIImage(WaterMask)
- (UIImage*)addWaterMask:(NSString*)txt{
//1.创建位图上下文(size:开启多大的上下文,就会生成多大的图片)
UIGraphicsBeginImageContext(self.size);
//2.把图片绘制到上下文当中
[self drawAtPoint:CGPointZero];
//3.绘制水印(虽说UILabel可以快速实现这种效果,但是我们也可以绘制出来)
NSString *str = txt;
// 水印的 字体、颜色等样式 根据项目 按需调整
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSFontAttributeName] = [UIFont systemFontOfSize:20];
dict[NSForegroundColorAttributeName] = [UIColor redColor];
[str drawAtPoint:CGPointZero withAttributes:dict];
//4.从上下文当中生成一张图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
//5.关闭位图上下文
UIGraphicsEndImageContext();
return newImage;
}
@end
4. 截屏处理
@interface UIView(ScreenShot)
- (UIImage*)screenshot;
@end
@implementation UIView(ScreenShot)
- (UIImage*)screenshot{
//生成图片
//1.开启一个位图上下文
UIGraphicsBeginImageContext(self.bounds.size);
//2.把View的内容绘制到上下文当中
CGContextRef ctx = UIGraphicsGetCurrentContext();
//UIView内容想要绘制到上下文当中, 必须使用渲染的方式
[self.layer renderInContext:ctx];
//3.从上下文当中生成一张图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
//4.关闭上下文
UIGraphicsEndImageContext();
//把图片转成二进制流
// //NSData *data = UIImageJPEGRepresentation(newImage, 1);
// NSData *data = UIImagePNGRepresentation(newImage);
//
// [data writeToFile:@"/Users/lifengfeng/Downloads/imlifengfeng.jpg" atomically:YES];
return newImage;
}
@end
5. 图片解码
关于 Core Graphics 图片解码 相关原理和过程,我推荐看这几篇文章:
-
- YYWebImage: github.com/ibireme/YYW…
五、其它功能
1. 绘图功能|绘制形状(直线、曲线、矩形、椭圆等)
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制直线
CGContextMoveToPoint(context, 50, 50);
CGContextAddLineToPoint(context, 150, 150);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextStrokePath(context);
// 绘制曲线
CGContextMoveToPoint(context, 50, 100);
CGContextAddQuadCurveToPoint(context, 100, 50, 150, 100);
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextStrokePath(context);
// 绘制矩形
CGContextAddRect(context, CGRectMake(50, 150, 100, 50));
CGContextSetFillColorWithColor(context, [UIColor greenColor].CGColor);
CGContextFillPath(context);
// 绘制椭圆
CGContextAddEllipseInRect(context, CGRectMake(50, 200, 100, 50));
CGContextSetStrokeColorWithColor(context, [UIColor purpleColor].CGColor);
CGContextStrokePath(context);
}
Swift示例:
// Swift示例:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 绘制直线
context.move(to: CGPoint(x: 50, y: 50))
context.addLine(to: CGPoint(x: 150, y: 150))
context.setStrokeColor(UIColor.red.cgColor)
context.strokePath()
// 绘制曲线
context.move(to: CGPoint(x: 50, y: 100))
context.addQuadCurve(to: CGPoint(x: 150, y: 100), control: CGPoint(x: 100, y: 50))
context.setStrokeColor(UIColor.blue.cgColor)
context.strokePath()
// 绘制矩形
context.addRect(CGRect(x: 50, y: 150, width: 100, height: 50))
context.setFillColor(UIColor.green.cgColor)
context.fillPath()
// 绘制椭圆
context.addEllipse(in: CGRect(x: 50, y: 200, width: 100, height: 50))
context.setStrokeColor(UIColor.purple.cgColor)
context.strokePath()
}
}
2 绘图功能|绘制路径(自定义路径、曲线路径等)
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制自定义路径
CGContextMoveToPoint(context, 50, 50);
CGContextAddLineToPoint(context, 150, 50);
CGContextAddLineToPoint(context, 100, 100);
CGContextClosePath(context);
// 绘制曲线路径
CGContextMoveToPoint(context, 50, 150);
CGContextAddQuadCurveToPoint(context, 100, 200, 150, 150);
// 设置路径样式
CGContextSetLineWidth(context, 2.0);
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
// 绘制路径
CGContextDrawPath(context, kCGPathFillStroke);
}
Swift示例:
// Swift示例:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 绘制自定义路径
context.move(to: CGPoint(x: 50, y: 50))
context.addLine(to: CGPoint(x: 150, y: 50))
context.addLine(to: CGPoint(x: 100, y: 100))
context.closePath()
// 绘制曲线路径
context.move(to: CGPoint(x: 50, y: 150))
context.addQuadCurve(to: CGPoint(x: 150, y: 150), control: CGPoint(x: 100, y: 200))
// 设置路径样式
context.setLineWidth(2.0)
context.setStrokeColor(UIColor.blue.cgColor)
// 绘制路径
context.drawPath(using: .fillStroke)
}
}
3 绘图功能|绘制图像(位图图像、矢量图像等)
Objective-C示例:
// Objective-C示例:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建绘图上下文
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 200), NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制矩形
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextFillRect(context, CGRectMake(50, 50, 100, 100));
// 绘制图像
UIImage *image = [UIImage imageNamed:@"exampleImage"];
[image drawInRect:CGRectMake(20, 20, 50, 50)];
// 获取绘制的图像
UIImage *resultImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// 显示绘制的图像
UIImageView *imageView = [[UIImageView alloc] initWithImage:resultImage];
[self.view addSubview:imageView];
}
@end
Swift示例:
// Swift示例:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
// 创建绘图上下文
UIGraphicsBeginImageContextWithOptions(CGSize(width: 200, height: 200), false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
// 绘制矩形
context.setFillColor(UIColor.blue.cgColor)
context.fill(CGRect(x: 50, y: 50, width: 100, height: 100))
// 绘制图像
if let image = UIImage(named: "exampleImage") {
image.draw(in: CGRect(x: 20, y: 20, width: 50, height: 50))
}
// 获取绘制的图像
if let resultImage = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
// 显示绘制的图像
let imageView = UIImageView(image: resultImage)
imageView.frame = CGRect(x: 100, y: 100, width: 200, height: 200)
view.addSubview(imageView)
}
}
}
}
4 图像处理|加载、绘制、裁剪、变换、合成、渲染
Objective-C示例:
// Objective-C示例:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) UIImageView *imageView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *originalImage = [UIImage imageNamed:@"original_image"];
// 创建绘图上下文
UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制原始图像
[originalImage drawInRect:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height)];
// 裁剪图像
CGContextClipToRect(context, CGRectMake(0, 0, originalImage.size.width / 2, originalImage.size.height));
// 创建变换矩阵
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
CGContextConcatCTM(context, transform);
// 绘制变换后的图像
UIImage *transformedImage = UIGraphicsGetImageFromCurrentImageContext();
// 合成图像
UIImage *compositeImage = [UIImage imageNamed:@"overlay_image"];
[compositeImage drawInRect:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height) blendMode:kCGBlendModeNormal alpha:0.5];
// 渲染图像
self.imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height)];
self.imageView.image = UIGraphicsGetImageFromCurrentImageContext();
[self.view addSubview:self.imageView];
UIGraphicsEndImageContext();
}
@end
Swift示例:
// Swift示例:
import UIKit
class ViewController: UIViewController {
var imageView: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
if let originalImage = UIImage(named: "original_image") {
// 创建绘图上下文
UIGraphicsBeginImageContextWithOptions(originalImage.size, false, 0.0)
if let context = UIGraphicsGetCurrentContext() {
// 绘制原始图像
originalImage.draw(in: CGRect(x: 0, y: 0, width: originalImage.size.width, height: originalImage.size.height))
// 裁剪图像
context.clip(to: CGRect(x: 0, y: 0, width: originalImage.size.width / 2, height: originalImage.size.height))
// 创建变换矩阵
let transform = CGAffineTransform(rotationAngle: CGFloat.pi / 4)
context.concatenate(transform)
// 绘制变换后的图像
let transformedImage = UIGraphicsGetImageFromCurrentImageContext()
// 合成图像
if let compositeImage = UIImage(named: "overlay_image") {
compositeImage.draw(in: CGRect(x: 0, y: 0, width: originalImage.size.width, height: originalImage.size.height), blendMode: .normal, alpha: 0.5)
}
// 渲染图像
imageView = UIImageView(frame: CGRect(x: 0, y: 0, width: originalImage.size.width, height: originalImage.size.height))
imageView.image = UIGraphicsGetImageFromCurrentImageContext()
view.addSubview(imageView)
UIGraphicsEndImageContext()
}
}
}
}
5 文本渲染|渲染文本内容,控制文本样式、排版布局
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
- (void)drawTextWithCustomStyle {
CGContextRef context = UIGraphicsGetCurrentContext();
NSString *text = @"Hello, Core Graphics!";
UIFont *font = [UIFont systemFontOfSize:16];
UIColor *textColor = [UIColor redColor];
NSDictionary *attributes = @{NSFontAttributeName: font, NSForegroundColorAttributeName: textColor};
CGSize textSize = [text sizeWithAttributes:attributes];
CGRect textRect = CGRectMake(50, 50, textSize.width, textSize.height);
[text drawInRect:textRect withAttributes:attributes];
}
Swift示例:
// Swift示例:
import UIKit
func drawTextWithCustomStyle() {
if let context = UIGraphicsGetCurrentContext() {
let text = "Hello, Core Graphics!"
let font = UIFont.systemFont(ofSize: 16)
let textColor = UIColor.red
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
let attributes: [NSAttributedString.Key: Any] = [
.font: font,
.foregroundColor: textColor,
.paragraphStyle: paragraphStyle
]
let textSize = text.size(withAttributes: attributes)
let textRect = CGRect(x: 50, y: 50, width: textSize.width, height: textSize.height)
text.draw(in: textRect, withAttributes: attributes)
}
}
6 图形上下文|绘图 颜色、线宽、填充模式
Objective-C示例:
// Objective-C示例:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 设置绘图颜色
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
// 设置线宽
CGContextSetLineWidth(context, 2.0);
// 绘制矩形
CGContextAddRect(context, CGRectMake(50, 50, 100, 100));
// 填充矩形
CGContextFillPath(context);
}
Swift示例:
// Swift示例:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 设置绘图颜色
context.setFillColor(UIColor.blue.cgColor)
context.setStrokeColor(UIColor.red.cgColor)
// 设置线宽
context.setLineWidth(2.0)
// 绘制矩形
context.addRect(CGRect(x: 50, y: 50, width: 100, height: 100))
// 填充矩形
context.fillPath()
}
}
7 颜色和渲染|填充颜色、描边颜色、渐变色
Objective-C示例:
// Objective-C示例:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 填充颜色
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextFillRect(context, rect);
// 描边颜色
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextStrokeRect(context, CGRectMake(50, 50, 100, 100));
// 创建渐变色
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = {0.0, 1.0};
NSArray *colors = @[(id)[UIColor redColor].CGColor, (id)[UIColor blueColor].CGColor];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
// 绘制渐变色
CGContextDrawLinearGradient(context, gradient, CGPointMake(0, 0), CGPointMake(0, CGRectGetHeight(rect)), kCGGradientDrawsBeforeStartLocation);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
}
Swift示例:
// Swift示例:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 填充颜色
context.setFillColor(UIColor.blue.cgColor)
context.fill(rect)
// 描边颜色
context.setStrokeColor(UIColor.red.cgColor)
context.stroke(CGRect(x: 50, y: 50, width: 100, height: 100))
// 创建渐变色
let colorSpace = CGColorSpaceCreateDeviceRGB()
let locations: [CGFloat] = [0.0, 1.0]
if let gradient = CGGradient(colorsSpace: colorSpace, colors: [UIColor.red.cgColor, UIColor.blue.cgColor] as CFArray, locations: locations) {
// 绘制渐变色
context.drawLinearGradient(gradient, start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: rect.height), options: [])
}
}
}
8 图形变换|平移、旋转、缩放
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 平移
CGContextTranslateCTM(context, 50, 50);
// 旋转
CGContextRotateCTM(context, M_PI_4); // 45度
// 缩放
CGContextScaleCTM(context, 1.5, 1.5);
// 绘制一个矩形
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextFillRect(context, CGRectMake(0, 0, 100, 100));
}
Swift示例:
// Swift示例:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 平移
context.translateBy(x: 50, y: 50)
// 旋转
context.rotate(by: CGFloat.pi / 4) // 45度
// 缩放
context.scaleBy(x: 1.5, y: 1.5)
// 绘制一个矩形
context.setFillColor(UIColor.blue.cgColor)
context.fill(CGRect(x: 0, y: 0, width: 100, height: 100))
}
}
9 性能优化
Objective-C示例:
// Objective-C示例:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 设置绘制属性,如颜色、线宽等
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2.0);
// 性能优化:禁止图形上下文的自动平滑处理
CGContextSetAllowsAntialiasing(context, NO);
// 性能优化:设置图形上下文的属性
CGContextSetShouldAntialias(context, YES); // 开启抗锯齿
CGContextSetAllowsAntialiasing(context, YES); // 允许抗锯齿
CGContextSetInterpolationQuality(context, kCGInterpolationHigh); // 设置插值质量为高
// 绘制大量图形
for (int i = 0; i < 1000; i++) {
CGContextMoveToPoint(context, i, 0);
CGContextAddLineToPoint(context, i, 100);
}
// 执行绘制
CGContextStrokePath(context);
}
Swift示例:
// Swift示例:
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 设置绘制属性,如颜色、线宽等
context.setStrokeColor(UIColor.red.cgColor)
context.setLineWidth(2.0)
// 性能优化:禁止图形上下文的自动平滑处理
context.setAllowsAntialiasing(false)
// 性能优化:设置图形上下文的属性
context.setShouldAntialias(true) // 开启抗锯齿
context.setAllowsAntialiasing(true) // 允许抗锯齿
context.setInterpolationQuality(.high) // 设置插值质量为高
// 绘制大量图形
for i in 0..<1000 {
context.move(to: CGPoint(x: i, y: 0))
context.addLine(to: CGPoint(x: i, y: 100))
}
// 执行绘制
context.strokePath()
}
}
六、经典第三方库
关于文本的处理,在这里推荐一个第三方库:YYText(有此处提及的Core Graphics文本处理引出,但与Core Graphics框架无关)
关于K线图、柱状图、饼状图的绘制,第三方库推荐(我们在实际开发中,也许只是借鉴人家三方库的写法,去简化封装我们自己需要的图)
-
AAChartKit
- 内部是JS实现,WebView呈现
-
Charts
- 内部是CoreGraphics、Layer、UIBezierPath等绘制
-
YYStock
- k线图
- 专业版: YYKline
- PNChart
- ...
04-iOS 多媒体技术| Core Animation要点回顾1【CAlayer与UIView树、核心动画、Core Animation渲染原理等】
05-iOS 多媒体技术| Core Animation要点回顾2【UIKit动画、Core Animation动画、粒子动画、经典第三方动画库等】
03-iOS 多媒体技术| 图形处理框架-UIKit要点回顾2【UIView、UIViewController、UIWindow、生命周期、事件响应者链等】
02-iOS 多媒体技术| 图形处理框架-UIKit要点回顾1【UITouch、UIEvent、手势处理、UIResponder、UIApplication等】
01-iOS 多媒体技术| 图形处理框架简介:Core Animation、Core Graphics、OpenGL ES、Metal、GPUImage等
01-iOS 图层处理相关| 了解 移动端图片格式 和编解码【静态图片、动态图片】 (转)
原文链接: blog.ibireme.com
图片通常是移动端流量耗费最多的部分,并且占据着重要的视觉空间。合理的图片格式选用和优化可以为你节省带宽、提升视觉效果。在这篇文章里我会分析一下目前主流和新兴的几种图片格式的特点、性能分析、参数调优,以及相关开源库的选择。
几种图片格式的简介
首先谈一下大家耳熟能详的几种老牌的图片格式吧:
JPEG 是目前最常见的图片格式,它诞生于 1992 年,是一个很古老的格式。它只支持有损压缩,其压缩算法可以精确控制压缩比,以图像质量换得存储空间。由于它太过常见,以至于许多移动设备的 CPU 都支持针对它的硬编码与硬解码。
PNG 诞生在 1995 年,比 JPEG 晚几年。它本身的设计目的是替代 GIF 格式,所以它与 GIF 有更多相似的地方。PNG 只支持无损压缩,所以它的压缩比是有上限的。相对于 JPEG 和 GIF 来说,它最大的优势在于支持完整的透明通道。
GIF 诞生于 1987 年,随着初代互联网流行开来。它有很多缺点,比如通常情况下只支持 256 种颜色、透明通道只有 1 bit、文件压缩比不高。它唯一的优势就是支持多帧动画,凭借这个特性,它得以从 Windows 1.0 时代流行至今,而且仍然大受欢迎。
在上面这些图片格式诞生后,也有不少公司或团体尝试对他们进行改进,或者创造其他更加优秀的图片格式,比如 JPEG 小组的 JPEG 2000、微软的 JPEG-XR、Google 的 WebP、个人开发者发布的 BPG、FLIF 等。它们相对于老牌的那几个图片格式来说有了很大的进步,但出于各种各样的原因,只有少数几个格式能够流行开来。下面三种就是目前实力比较强的新兴格式了:
APNG 是 Mozilla 在 2008 年发布的一种图片格式,旨在替换掉画质低劣的 GIF 动画。它实际上只是相当于 PNG 格式的一个扩展,所以 Mozilla 一直想把它合并到 PNG 标准里面去。然而 PNG 开发组并没有接受 APNG 这个扩展,而是一直在推进它自己的 MNG 动图格式。MNG 格式过于复杂以至于并没有什么系统或浏览器支持,而 APNG 格式由于简单容易实现,目前已经渐渐流行开来。Mozilla 自己的 Firefox 首先支持了 APNG,随后苹果的 Safari 也开始有了支持, Chrome 目前也已经尝试开始支持 ,可以说未来前景很好。
WebP 是 Google 在 2010 年发布的图片格式,希望以更高的压缩比替代 JPEG。它用 VP8 视频帧内编码作为其算法基础,取得了不错的压缩效果。它支持有损和无损压缩、支持完整的透明通道、也支持多帧动画,并且没有版权问题,是一种非常理想的图片格式。借由 Google 在网络世界的影响力,WebP 在几年的时间内已经得到了广泛的应用。看看你手机里的 App:微博、微信、QQ、淘宝、网易新闻等等,每个 App 里都有 WebP 的身影。Facebook 则更进一步,用 WebP 来显示聊天界面的贴纸动画。
BPG 是著名程序员 Fabrice Bellard 在去年 (2014年) 发布的一款超高压缩比的图片格式。这个程序员大家可能有些面生,但说起他的作品 FFmpeg、QEMU 大家想必是都知道的。BPG 使用 HEVC (即 H.265) 帧内编码作为其算法基础,就这点而言,它毋庸置疑是当下最为先进的图片压缩格式。相对于 JP2、JPEG-XR、WebP 来说,同等体积下 BPG 能提供更高的图像质量。另外,得益于它本身基于视频编码算法的特性,它能以非常小的文件体积保存多帧动画。 Fabrice Bellard 聪明的地方在于,他知道自己一个人无法得到各大浏览器厂商的支持,所以他还特地开发了 Javascript 版的解码器,任何浏览器只要加载了这个 76KB 大小的 JS 文件,就可以直接显示 BPG 格式的图片了。目前阻碍它流行的原因就是 HEVC 的版权问题和它较长的编码解码时间。尽管这个图片格式才刚刚发布一年,但已经有不少厂子开始试用了,比如阿里和腾讯。
移动端图片类型的支持情况
目前主流的移动端对图片格式的支持情况如何呢?我们分别来看一下 Android 和 iOS 目前的图片编解码架构吧:
![]()
Android 的图片编码解码是由 Skia 图形库负责的,Skia 通过挂接第三方开源库实现了常见的图片格式的编解码支持。
- 目前来说,Android 原生支持的格式只有
JPEG、PNG、GIF、BMP和WebP(Android 4.0 加入),在上层能直接调用的编码方式也只有JPEG、PNG、WebP这三种。 - 目前来说 Android 还不支持直接的动图编解码。
iOS 底层是用 ImageIO.framework 实现的图片编解码。
- 目前 iOS 原生支持的格式有:
JPEG、JPEG2000、PNG、GIF、BMP、ICO、TIFF、PICT - 自 iOS 8.0 起,ImageIO 又加入了
APNG、SVG、RAW格式的支持。在上层,开发者可以直接调用 ImageIO 对上面这些图片格式进行编码和解码。对于动图来说,开发者可以解码动画GIF和APNG、可以编码动画GIF。
两个平台在导入第三方编解码库时,都多少对他们进行了一些修改,比如 Android 对 libjpeg 等进行的调整以更好的控制内存,iOS 对 libpng 进行了修改以支持 APNG,并增加了多线程编解码的特性。除此之外,iOS 专门针对 JPEG 的编解码开发了 AppleJPEG.framework,实现了性能更高的硬编码和硬解码,只有当硬编码解码失败时,libjpeg 才会被用到。
静态图片的编码与解码
由于我目前主要是做 iOS 开发,所以下面的性能评测都是基于 iPhone 的,主要测试代码可以在这里看到。测试素材很少,只有两个:
第一张是Dribbble 的 Logo,包含 Alpha 通道,用于测试简单的、图形类的图像。
第二张经典的 Lena 图,用于测试照片类的、具有丰富细节的图像。
每个图像都有 64x64、128x128、256x256、512x512 四种分辨率。
JPEG
目前比较知名的 JPEG 库有以下三个:
libjpeg:开发时间最早,使用最广泛的 JPEG 库。由于 JPEG 标准过于复杂和模糊,并没有其他人去实现,所以这个库是 JPEG 的事实标准。
libjpeg-turbo:一个致力于提升编解码速度的 JPEG 库。它基于 libjpeg 进行了改造,用 SIMD 指令集 (MMX、SSE2、NEON) 重写了部分代码,官网称相对于 libjpeg 有 2 到 4 倍的性能提升。
MozJPEG: 一个致力于提升压缩比的 JPEG 库。它是 Mozilla 在 2014 年发布的基于 libjpeg-turbo 进行改造的库,相对于 libjpeg 有 5% ~ 15%的压缩比提升,但相应的其编码速度也慢了很多。
除了上面这三个库,苹果自己也开发了一个 AppleJPEG,但并没有开源。其调用了芯片提供的 DSP 硬编码和硬解码的功能。虽然它不如上面这三个库功能完善,但其性能非常高。在我的测试中,其编解码速度通常是 libjpeg-turbo 的 1~2 倍。可惜的是,目前开发者并不能直接访问这个库。
下面是 ImageIO (AppleJPEG/libpng) 在 iPhone 6 上的编解码性能:
可以看到,JPEG 编码中 quality 越小,图片体积就越小,质量越也差,编码时间也越短。解码时间并没有很大的差距,可能是其大部分时间消耗在了函数调用、硬件调用上。苹果在自己的相册 Demo 中提供的 quality 的默认值是 0.9,在这个值附近,图像质量和体积、编码解码时间之间都能取得不错的平衡。
PNG
相对于 JPEG 来说,PNG 标准更为清晰和简单,因此有很多公司或个人都有自己的 PNG 编码解码实现。但目前使用最广的还是 PNG 官方发布的 libpng 库。iOS 和 Android 底层都是调用这个库实现的 PNG 编解码。
下面是 PNG 在 iPhone 6 上的编解码性能:
可以看到,在编解码图形类型(颜色少、细节少)的图片时,PNG 和 JPEG 差距并不大;但是对于照片类型(颜色和细节丰富)的图片来说,PNG 在文件体积、编解码速度上都差 JPEG 不少了。
和 JPEG 不同,PNG 是无损压缩,其并不能提供压缩比的选项,其压缩比是有上限的。目前网上有很多针对 PNG 进行优化的工具和服务,旨在提升 PNG 的压缩比。下面是常见的几个 PNG 压缩工具的性能对比:
pngcrush 是 Xcode 自带的 PNG 压缩工具,相对于设计师用 Photoshop 生成的图片来说,它能取得不错的压缩效果。ImageOptim 则更进一步,对每张图用多种缩算法进行比对,选择压缩比更高的结果,进一步缩小了文件体积。TinyPNG.com 相对于其他工具来说,压缩比高得不像话。它启用了类似 GIF 那样的颜色索引表对 PNG 进行压缩,所以会导致颜色丰富的图片丢失掉一部分细节。如果使用 TinyPNG 的话,最好在压缩完成后让设计师看一下颜色效果是否可以接受。
WebP
WebP 标准是 Google 定制的,迄今为止也只有 Google 发布的 libwebp 实现了该的编解码 。 所以这个库也是该格式的事实标准。
WebP 编码主要有几个参数:
lossless: YES:有损编码 NO:无损编码。WebP 主要优势在于有损编码,其无损编码的性能和压缩比表现一般。
quality: [0~100] 图像质量,0表示最差质量,文件体积最小,细节损失严重,100表示最高图像质量,文件体积较大。该参数只针对有损压缩有明显效果。Google 官方的建议是 75,腾讯在对 WebP 评测时给出的建议也是 75。在这个值附近,WebP 能在压缩比、图像质量上取得较好的平衡。
method: [0~6] 压缩比,0表示快速压缩,耗时短,压缩质量一般,6表示极限压缩,耗时长,压缩质量好。该参数也只针对有损压缩有明显效果。调节该参数最高能带来 20% ~ 40% 的更高压缩比,但相应的编码时间会增加 5~20 倍。Google 推荐的值是 4。
对于
- 编码无损图片来说,quality=0, method=
0~3是相对来说比较合适的参数,能够节省编码时间,同时也有不错的压缩比。 - 无损编码图片,quality=75, method=
2~4是比较合适的参数,能在编码时间、图片质量、文件体积之间有着不错的平衡。
WebP 解码有三个参数:
use_threads: 是否启用 pthread 多线程解码。该参数只对宽度大于 512 的有损图片起作用。开启后内部会用多线程解码,CPU 占用会更高,解码时间平均能缩短 10%~20%。
bypass_filtering: 是否禁用滤波。该参数只对有损图片起作用,开启后大约能缩短 5%~10% 的解码时间,但会造成一些颜色过渡平滑的区域产生色带(banding)。
no_fancy_upsampling: 是否禁用上采样。该参数只对有损图片起作用。在我的测试中,开启该参数后,解码时间反而会增加 5~25%,同时会造成一些图像细节的丢失,线条边缘会增加杂色,显得不自然。
通常情况下,这三个参数都设为 NO 即可,如果要追求更高的解码速度,则可以尝试开启 use_threads 和 bypass_filtering 这两个参数。而 no_fancy_upsampling 在任何情况下都没必要开启。
由于 WebP 测试数据较多,这里只贴一下 512x512 大小的一部分测试结果,感兴趣的可以看文章结尾处的 Excel 附件。
对于简单的图形类型的图像(比如 App 内的各种 UI 素材),WebP 无损压缩的文件体积和解码速度已经比 PNG 还要理想了,如果你想要对 App 安装包体积进行优化,WebP 已经是个不错的选择了。
对于复杂的图像(比如照片)来说,WebP 无损编码表现并不好,但有损编码表现却非常棒。相近质量的图片解码速度 WebP 相距 JPEG 已经差不了太多了,而文件压缩比却能提升不少。
BPG
BPG 是目前已知最优秀的有损压缩格式了,它能在相同质量下比 JPEG 减少 50% 的体积。下面是经典的 Lena 图的对比,你也可以在这里看到大量其他图片的 BPG、JPEG、JPEG2000、JPEG-XR、WebP 压缩效果的在线对比,效果非常明显。
![]()
BPG 目前只有作者发布的 libbpg 可用。但作者基于 libbpg 编译出了一个 Javascript 解码器,很大的扩展了可用范围。bpg 可以以无损和有损压缩两种方式进行编码,有损压缩时可以用 quality 参数控制压缩比,可选范围为 0~51,数值越大压缩比越高。通常来说,25 附近是一个不错的选择,BPG 官方工具默认值是 28。
libbpg 目前并没有针对 ARM NEON 做优化,所以其在移动端的性能表现一般。下面是 iPhone 6 上的性能测试:
![]()
由于 bpg 编码时间太长,我并没有将数据放到表格里。可以看到相同质量下,BPG 的解码速度还是差 JPEG 太多,大约慢了 3~5 倍。目前来说,BPG 适用于那些对流量非常敏感,但对解码时间不敏感的地方。从网上的新闻来看,手机淘宝和手机QQ都已经有所尝试,但不清楚他们是否对 BPG 解码进行了优化。
小结
-
JPEG压缩速度快, 压缩质量较弱于PNG(结合 压缩质量 和压缩速度 综合考虑,用JPEG格式比较多) -
PNG- 在编解码图形类型(颜色少、细节少)的图片时,PNG 和 JPEG 差距并不大;
- 但是对于照片类型(颜色和细节丰富)的图片来说,PNG 在文件体积、编解码速度上都差 JPEG 不少了。
- 和
JPEG不同,PNG是无损压缩,其并不能提供压缩比的选项,其压缩比是有上限的。
-
WebP主要优势在于有损编码,无损压缩性能表现一般 -
BPG是目前已知最优秀的有损压缩格式了,它能在相同质量下比 JPEG 减少 50% 的体积。相同质量下,BPG速度比JPEG压缩速度慢很多
动态图片的编码与解码
动图在网络上非常受欢迎,它近似视频,但通常实现简单、文件体积小,应用范围非常广泛。动图的始祖是 GIF,它自 Windows 1.0 时代就在互联网上流行开来,直到今天仍然难以被其他格式取代。尽管它非常古老,但其所用的原理和今天几种新兴格式几乎一样。
下面是一张 GIF 格式的 QQ 大表情:
![]()
这张表情由 6 幅静态图构成,每幅图片有一定的存活时间,连贯播放就形成了动画:
![]()
这几张图中,大部分内容是相近的,为了压缩文件体积,通常动图格式都支持一些特殊的方式对相似图片进行裁剪,只保留前后帧不同的部分:
![]()
在解码动图时,解码器通常采用所谓"画布模式"进行渲染。想象一下:播放的区域是一张画布,第一帧播放前先把画布清空,然后完整的绘制上第一帧图;播放第二帧时,不再清空画布,而是只把和第一帧不同的区域覆盖到画布上,就像油画的创作那样。
像这样的第一帧就被称为关键帧(即 I 帧,帧内编码帧),而后续的那些通过补偿计算得到的帧被称为预测编码帧(P帧)。一个压缩的比较好的动图内,通常只有少量的关键帧,而其余都是预测编码帧;一个较差的压缩工具制作的动图内,则基本都是关键帧。不同的动图压缩工具通常能得到不同的结果。
除此之外,动图格式通常有更为详细的参数控制每一帧的绘制过程,下面是 GIF/APNG/WebP 通用的几个参数:
Disposal Method (清除方式)
Do Not Dispose:把当前帧增量绘制到画布上,不清空画布。
Restore to Background:绘制当前帧之前,先把画布清空为默认背景色。
Restore to Previous:绘制下一帧前,把先把画布恢复为当前帧的前一帧
Blend Mode (混合模式)
Blend None: 绘制时,全部通道(包含Alpha通道)都会覆盖到画布,相当于绘制前先清空画布的指定区域。
Blend over:绘制时,Alpha 通道会被合成到画布,即通常情况下两张图片重叠的效果。
上面这些技术,就是常见动图格式的基础了,下面分别介绍一下不同动图格式的特点。
GIF
GIF 缺陷非常明显:它通常只支持 256 色索引颜色,这导致它只能通过抖动、差值等方式模拟较多丰富的颜色;它的 Alpha 通道只有 1 bit,这意味着一个像素只能是完全透明或者完全不透明。
![]()
上面这是腾讯博客里的一张演示图,可以看到 GIF 由于 Alpha 通道的问题,产生了严重的"毛边"现象。目前通常的解决方案是在图片的边缘加一圈白边,以减轻这种视觉效果:
![]()
可以仔细观察一下 QQ、微信等 App 里面的动画表情,几乎每个表情都被一圈白边所环绕,不得不说是一种很无奈的解决方案。
GIF 的制作工具有很多,但效果好、压缩比高的工具非常少。对于已经制作好的 GIF 来说,用 imagemagick 处理一下可以把文件体积压缩不少。如果需要将视频转为 GIF,Cinemagraph Pro 是个不错的傻瓜化工具。这里有一篇文章介绍如何用 ffmpeg 压缩 GIF,虽然参数调节有点麻烦,但效果非常理想。
下面是没有经过优化的 GIF 和经过 ffmpeg 优化编码的 GIF,可以看到差距非常大。
[ ![]()
APNG
APNG 目前并没有被 PNG 官方所接受,所以 libpng 并不能直接解码 APNG。但由于 APNG 只是基于 PNG 的一个简单扩展,所以在已经支持 PNG 的平台上,可以很轻松的用少量代码实现 APNG 的编解码。Chromium 为了支持 APNG 播放,只增加了不到 600 行代码 ,我自己也用大概 500 行 C 代码实现了一个简单的 APNG 编解码工具。另外,在支持 canvas 的浏览器上,可以用 apng-canvas 直接显示 APNG 动画。APNG 压缩最好的工具目前是 apngasm,大部分图形化工具比如腾讯的 iSparta 都是基于这个工具开发的。
就目前而言, APNG 是 GIF 最好的替代了:实现简单,可用范围广,压缩比不错,显示效果好。
WebP
WebP 在 2010 年 发布时并没有支持动图。2012 年 libwebp v0.2 的时候,Google 才开始尝试支持动画,但其实现有很多问题,性能也非常差,以至于 Chrome 团队一直都没有接受。直到 2013 年,libwebp v0.4 时,动画格式才稳定下来才被 Chrome 所接受。
WebP 动图实际上是把多个单帧 WebP 数据简单打包到一个文件内,而并不是由单帧 WebP 扩展而来,以至于动图格式并不能向上兼容静态图。如果要支持动图,首先在编译 libwebp 时需要加上 demux 模块,解码 WebP 时需要先用 WebPDemuxer 尝试拆包,之后再把拆出来的单帧用 WebPDecode 解码。为了方便编译,我写了个脚本用于打包 iOS 的静态库,加入了 mux 和 demux 模块。
Google 提供了两个简单的命令行工具用于制作动图:gif2webp 能把 GIF 转换为 WebP, webpmux 能把多个 WebP 图片打包为动态图,并且有着很多参数可以调节。这两个工具对相近帧的压缩并不太理想,以至于有的情况下压缩比还不如 APNG,但除此以外也没有其他什么更好的工具可以用了。
BPG
BPG 本身是基于 HEVC (H.265) 视频编码的,其最开始设计时就考虑到了动图的实现。由于它充分利用了 HEVC 的高压缩比和视频编码的特性,其动图压缩比远超其他格式。这里和这里有几张 BPG 动图示例,可以看到相同质量下 BPG 动图只有 APNG/WebP/GIF 几十分之一的大小。
我在这里写了个简单的利用 libbpg 解码动图的方法,如有需要可以参考下。
动图性能对比
我把下面这张 GIF 分别转为 WebP、APNG、BPG 动图,并在 iPhone 6 上对其所有帧进行解码。
![]()
评测结果如下:
![]()
APNG 在文件体积上比 GIF 略有优势,解码时间相差不多。WebP 在体积和解码时间上都具有较大的优势。BPG 在体积上优势最大,但解码时间也最长。这么看来,APNG 和 WebP 都是不错的选择,而 BPG 还有待性能优化。
最后做一个小广告:如果你是 iOS 平台的开发者,可以试试我开发的 YYWebImage,它支持 APNG、WebP、GIF 动图的异步加载与播放、编码与解码,支持渐进式图像加载,可以替代 SDWebImage、PINRemoteImage、FLAnimatedImage 等开源库。
小结
-
GIF缺陷非常明显:它通常只支持 256 色索引颜色,这导致它只能通过抖动、差值等方式模拟较多丰富的颜色;它的 Alpha 通道只有 1 bit,这意味着一个像素只能是完全透明或者完全不透明。 -
APNG是 GIF 最好的替代了:实现简单,可用范围广,压缩比不错,显示效果好
评测数据
上面提到的所有评测数据表格:image_benchmark.xlsx 推荐用 Excel 打开查看。
iOS-Swift语法静态分析配置|统一编码规范【Fastlane+SwiftLint】
SwiftUI 快速上手推荐
前言
最近在回顾Flutter技术时,与老同事聊起SwiftUI,我们了解到SwiftUI和Flutter UI技术都是声明式语法,因此近两天写了一些Demo进行简单尝试
一、SwiftUI快速上手
为了快速上手,我找了一些对SwiftUI有所研究的博主,跟着对方的博客笔记敲击了十来个Demo:
在多次尝试之后,我发现,通过SwiftUI写UI,的确会一定程度上减少代码量。
二、博文推荐
SwiftUI快速上手推荐:
三、SwiftUI与UIKit的异同:
- 1. 声明式 vs 命令式:SwiftUI 是声明式的,你只需要描述界面应该是什么样子,而 SwiftUI 会处理实际的渲染过程。而 UIKit 是命令式的,你需要告诉 UIKit 如何一步步地创建和更新界面。
- 2. 跨平台:SwiftUI 可以在所有 Apple 平台(包括 iOS、macOS、watchOS 和 tvOS)上使用,而 UIKit 只能用于 iOS 和 tvOS。
- 3. 组件:SwiftUI 提供了一些 UIKit 中没有的组件,如 Grid 和 Form。同时,SwiftUI 的组件在所有 Apple 平台上都有一致的外观和行为。
- 4. 数据流:SwiftUI 引入了一些新的数据流概念,如 @State、@Binding、@ObservedObject 和 @EnvironmentObject,它们使得数据状态管理更加直观和一致。而在 UIKit 中,你通常需要手动管理数据状态和更新界面。
- 5. 兼容性:SwiftUI 需要 iOS 13 或更高版本,而 UIKit 可以在更早的 iOS 版本上使用。
- 6. 成熟度:UIKit 已经存在了很长时间,有大量的文档、教程和社区支持。而 SwiftUI 是在 2019 年才推出的,虽然它正在快速发展,但还没有 UIKit 那么成熟。
总的来说,SwiftUI 提供了一种更现代、更简洁的方式来构建界面,但如果你需要支持旧的 iOS 版本,或者需要使用 UIKit 提供的一些高级功能,那么 UIKit 仍然是一个很好的选择。
四、SwiftUI布局
写了几个Demo后,我们可以了解到,其实要用SwiftUI斜截面,掌握SwiftUI的布局方法很重要
在 SwiftUI 中,布局主要由容器视图(如 HStack、VStack 和 ZStack)和修饰符(如 padding、frame 和 offset)来控制。
以下是一些常用的容器视图:
- HStack:在水平方向上排列其子视图。
- VStack:在垂直方向上排列其子视图。
- ZStack:在深度方向上堆叠其子视图。
以下是一些常用的修饰符:
- padding:给视图添加内边距。
- frame:设置视图的尺寸和对齐方式。
- offset:移动视图的位置。
在HStack、VStack、ZStack中还可以用spacing来约束,容器内部子视图的间距
iOS13 Scene Delegate详解(转)
![]()
iOS13 项目中的SceneDelegate类有什么作用?自从Xcode11发布以来,当你使用新XCode创建一个新的iOS项目时,SceneDelegate会被默认创建,它到底有什么用呢。
在本文中,我们将深入探讨iOS 13和Xcode 11的一些变化。我们将重点关注SceneDelegate和AppDelegate,以及它们如何影响SwiftUI、Storyboard和基于XIB的UI项目。
通过阅读本文你将了解到:
- SceneDelegate和AppDelegate的新变化
- 他们是如何合作引导你的app启动的
- 在纯手写App中使用
SceneDelegate - 在Storyboards 和 SwiftUI项目中使用
SceneDelegate
让我们开始吧。
本篇文章基于 Xcode 11 和 iOS 13.
AppDelegate
你可能对AppDelegate已经熟悉,他是iOS app的入口,application(_:didFinishLaunchingWithOptions:)是你的app启动后系统调用的第一个函数。
AppDelegate类实现了UIKit库中的UIApplicationDelegate 协议。而到了iOS13 AppDelegate的角色将会发生变化,后面我们会详细讨论。
下面是你在iOS12中一般会在AppDelegate中做的事情:
- 创建app的第一个view controller也就是 rootViewController
- 配置并启动一些像日志记录和云服务之类的组件
- 注册推送通知处理程序,并响应发送到app的推送通知
- 响应应用程序生命周期事件,例如进入后台,恢复应用程序或退出应用程序(终止)
iOS12及以前,使用Storyboards的app,AppDelegate很简单。 像这样:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
return true
}
一个使用XIB的简单应用看起来像这样:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool
{
let timeline = TimelineViewController()
let navigation = UINavigationController(rootViewController: timeline)
let frame = UIScreen.main.bounds
window = UIWindow(frame: frame)
window!.rootViewController = navigation
window!.makeKeyAndVisible()
return true
}
在上面的代码中,我们创建一个ViewController,并将其放在navigation controller中。然后将其分配给UIWindow对象的rootViewController属性。 这个window对象是AppDelegate的属性,它是我们的应用的一个窗口。
应用程序的window是一个重要的概念。 本质上,窗口就是应用程序,大多数iOS应用程序只有一个窗口。 它包含您应用的用户界面(UI),将事件调度到视图,并提供了一个主要背景层来显示您的应用内容。 从某种意义上说,“ Windows”的概念就是微软定义的窗口,而在iOS上,这个概念没有什么不同。 (谢谢,Xerox!)
好了,下面让我们继续SceneDelegate。
如果“窗口”的概念仍然不了解,请查看iPhone上的应用程序切换器。 双击Home键或从iPhone底部向上滑动,然后您会看到当前正在运行的应用程序的窗口。 这就是应用程序切换器。
The Scene Delegate
在iOS 13(及以后版本)上,SceneDelegate将负责AppDelegate的某些功能。 最重要的是,window(窗口)的概念已被scene(场景)的概念所代替。 一个应用程序可以具有不止一个场景,而一个场景现在可以作为您应用程序的用户界面和内容的载体(背景)。
尤其是一个具有多场景的App的概念很有趣,因为它使您可以在iOS和iPadOS上构建多窗口应用程序。 例如,文档编辑器App中的每个文本文档都可以有自己的场景。 用户还可以创建场景的副本,同时运行一个应用程序的多个实例(类似多开)。
在Xcode 11中有三个地方可以明显地看到SceneDelegate的身影:
- 现在,一个新的iOS项目会自动创建一个
SceneDelegate类,其中包括我们熟悉的生命周期事件,例如active,resign和disconnect。 - AppDelegate类中多了两个与“scene sessions”相关的新方法:
application(_:configurationForConnecting:options:)和application(_:didDiscardSceneSessions:) - Info.plist文件中提供了”Application Scene Manifest“配置项,用于配置App的场景,包括它们的场景配置名,delegate类名和storyboard
让我们一次开看一看。
1. Scene Delegate Class
首先,SceneDelegate类:
![]()
SceneDelegate的最重要的函数是:scene(_:willConnectTo:options:)。 在某种程度上,它与iOS 12上的 application(_:didFinishLaunchingWithOptions:) 函数的作用最相似。当将场景添加到app中时scene(_:willConnectTo:options:)函数会被调用的,因此这里是配置场景的最理想地方。 在上面的代码中,我们手动地设置了视图控制器堆栈,稍后会进行详细介绍。
这里需要特别注意的是,“SceneDelegate”采用了协议模式,并且这个delegate通常会响应任何场景。 您使用一个Delegate来配置App中的所有场景。
SceneDelegate 还具有下面这些函数:
-
sceneDidDisconnect(_:)当场景与app断开连接是调用(注意,以后它可能被重新连接) -
sceneDidBecomeActive(_:)当用户开始与场景进行交互(例如从应用切换器中选择场景)时,会调用 -
sceneWillResignActive(_:)当用户停止与场景交互(例如通过切换器切换到另一个场景)时调用 -
sceneWillEnterForeground(_:)当场景变成活动窗口时调用,即从后台状态变成开始或恢复状态 -
sceneDidEnterBackground(_:)当场景进入后台时调用,即该应用已最小化但仍存活在后台中
看到函数的对称性了吗? Active/inactive, background/foreground, 和 “disconnect”. 。 这些是任何应用程序的典型生命周期事件。
2.App Delegate中的Scene Sessions
在iOS13中AppDelegate中有两个管理Senen Session的代理函数。在您的应用创建scene(场景)后,“scene session”对象将跟踪与该场景相关的所有信息。
这两个函数是:
-
application(_:configurationForConnecting:options:), 会返回一个创建场景时需要的UISceneConfiguration对象 -
application(_:didDiscardSceneSessions:), 当用户通过“应用切换器”关闭一个或多个场景时会被调用
目前,SceneSession被用于指定场景,例如“外部显示” 或“ CarPlay” 。 它还可用于还原场景的状态,如果您想使用【状态还原】,SceneSession将非常有用。 状态还原允许您在应用启动之间保留并重新创建UI。 您还可以将用户信息存储到场景会话中,它是一个可以放入任何内容的字典。
application(_:didDiscardSceneSessions:)很简单。 当用户通过“应用程序切换器”关闭一个或多个场景时,即会调用该方法。 您可以在该函数中销毁场景所使用的资源,因为不会再需要它们。
了解application(_:didDiscardSceneSessions:)与sceneDidDisconnect(_ :)的区别很重要,后者仅在场景断开连接时调用,不会被丢弃,它可能会重新连接。而application(_:didDiscardSceneSessions:)发生在使用【应用程序切换器】退出场景时。
3. Info.plist 中的Application Scene Manifest
您的应用支持的每个场景都需要在“Application Scene Manifest”(应用场景清单)中声明。 简而言之,清单列出了您的应用支持的每个场景。 大多数应用程序只有一个场景,但是您可以创建更多场景,例如用于响应推送通知或特定操作的特定场景。
Application Scene Manifest清单是Info.plist文件的一项,都知道该文件包含App的配置信息。 Info.plist包含诸如App的名称,版本,支持的设备方向以及现在支持的不同场景等配置。
请务必注意,您声明的是会话的“类型”,而不是会话实例。 您的应用程序可以支持一个场景,然后创建该场景的副本,来实现【多窗口】应用程序。
下面看一下的 Info.plist中清单的一些配置:
![]()
在红框内,您会看到Application Scene Manifest 这一条。 在它下面一条是Enable Multiple Windows,需要将其设置为“ YES”以支持多个窗口。 再往下Application Session Role的值是一个数组,用于在应用程序中声明场景。 你也可以在数组中添加一条【外部屏幕】的场景声明。
最重要的信息保存在Application Session Role数组中。 从中我们可以看到以下内容:
- Configuration的名称,必须是唯一的
- 场景的代理类名,通常为
SceneDelegate。 - 场景用于创建初始UI的storyboard名称
Storyboard名称这一项可能使您想起Main Interface设置,该设置可以在Xcode 12项目的Project Properties配置中找到。 现在,在iOS应用中,你可以在此处设置或更改主Storyboard名称。
AppDelegate中的SceneDelegate、UISceneSession和Application Scene Manifest是如何一起创建多窗口应用的呢?
- 首先,我们看
SceneDelegate类。 它管理场景的生命周期,处理各种响应,诸如sceneDidBecomeActive(_:)andsceneDidEnterBackground(_:)之类的事件。 - 然后,我们再看看
AppDelegate类中的新函数。 它管理场景会话(scene sessions),提供场景的配置数据,并响应用户丢弃场景的事件。 - 最后,我们看了一下Application Scene Manifest。 它列出了您的应用程序支持的场景,并将它们连接到delegate类并初始化storyboard。
Awesome! Now that we’ve got a playing field, let’s find out how scenes affects building UIs in Xcode 11.
太棒了! 现在让我们了解scenes(场景)是如何影响Xcode 11中的用户界面的吧。
在SwiftUI中使用Scene Delegate
不久将来,SwiftUI将是创建iOS 13项目最简单的方法。 简言之,SwiftUI应用程序主要依靠SceneDelegate来设置应用程序的初始UI。
首先,SwiftUI项目中“Application Scene Manifest ”将长这样:
![]()
特别注意,配置中没有设置“Storyboard Name”这一项。 请记住,如果要支持多个窗口,则需要将Enable Multiple Windows设置为YES。
我们将跳过“ AppDelegate”,因为它相当标准。在SwiftUI项目中,只会返回“true”。
接下来是SceneDelegate类。 正如我们之前讨论的,SceneDelegate负责设置您应用中的场景,以及设置首个页面。
像下面一样:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
let contentView = ContentView()
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
window.rootViewController = UIHostingController(rootView: contentView)
self.window = window
window.makeKeyAndVisible()
}
}
}
上面的代码中发生了什么?
- 首先,必须明确的是 在将新场景添加到应用中后 会调用
scene(_:willConnectTo:options:)代理函数。 它提供了一个scene对象(和一个session)。 这个“UIWindowScene”对象是由应用创建的,您无需进行其他操作。 - 其次,
window属性会在这里用到。 App仍然使用“ UIWindow”对象,但现在它们已成为scene(场景)的一部分。 在if let代码块中,您可以清楚地看到如何使用scene来初始化UIWindow对象的。 - 然后是设置window的rootViewController,将
window实例分配给了场景的window属性,并且设置窗口makeKeyAndVisible为true,即将该窗口置于App的前面。 - 接着为SwiftUI项目创建了ContentView实例,并通过使用UIHostingController将其添加为根视图控制器。 该控制器用于将基于SwiftUI的视图显示在屏幕上。
- 最后但并非不重要的一点,值得注意的是,UIScene的实例化对象scene实际上是UIWindowScene类型的对象。 这就是
as?对可选类型转换的原因。 (到目前为止,已创建的场景通常为“ UIWindowScene”类型,但我猜想将来还会看到更多类型的场景。)
所有这些看起来似乎很复杂,但是从高层次的概述来看,这很简单:
- 当
scene(_:willConnectTo:options:)被调用时,SceneDelegate会在正确的时间配置场景。 - AppDelegate和Manifest的默认配置,他们没有涉及storyboard的任何东西。
-
scene(_:willConnectTo:options :)函数内,创建一个SwiftUI视图,将其放置在托管控制器中,然后将控制器分配给window属性的根视图控制器,并将该窗口放置在应用程序UI的前面 。
太棒了! 让我们继续。
您可以通过选择File(文件)→New(新建)→Project(项目)来建立一个基本的Xcode 11项目。 然后,选择
Single View App, 在User Interface处选择SwiftUI来创建一个SwiftUI项目
在Storyboards项目中使用SceneDelegate
Storyboards和XIB是为iOS应用程序构建UI的有效方法。 在iOS 13上也是如此。 在将来,我们将看到更多的SwiftUI应用,但目前,Storyboards更常见。
有趣的是,即使有了SceneDelegate,通过Storyboards创建iOS项目你也不需要做任何额外的事情 只需选择File → New → Project。 然后,选择Single View App。 最后,为 User Interface 处选择 Storyboard ,就完成了。
设置方法如下:
- 如我们前面提到过的,您可以在Info.plist中的“ Application Scene Manifest”中找到
Mainstoryboard的设置地方。 - 默认情况下,AppDelegate将使用默认的UISceneConfiguration。
- SceneDelegate会设置一个“UIWindow”对象,并使用“Main.storyboard”来创建初始UI。
纯代码编写UI
许多开发人员喜欢手写UI,而随着SwiftUI的兴起,使用SwiftUI手写代码将会越来越常见。 如果您不使用storyboards,而使用XIB创建应用程序UI,该怎么办? 让我们看看SceneDelegate如何修改。
首先,AppDelegate和Application Scene Manifest中保持默认值。 我们不使用storyboard,所以需要在SceneDelegate类的scene(_:willConnectTo:options:)函数中设置初始视图控制器。
像下面这样:
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow?
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions)
{
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)
let timeline = TimelineViewController()
let navigation = UINavigationController(rootViewController: timeline)
window.rootViewController = navigation
self.window = window
window.makeKeyAndVisible()
}
}
}
上面代码很简单我们就不详细介绍了。
很简单,对吧? 使用SceneDelegate的核心是将一些代码从AppDelegate移至到SceneDelegate中,并正确配置 Application Scene Manifest 。
想要为现有的iOS项目添加scene(场景)支持? 可以查看Apple官方文档https://developer.apple.com/documentation/uikit/app_and_environment/scenes/specifying_the_scenes_your_app_supports。
作者:Reinder de Vries
翻译:乐Coding
作者:乐Coding
链接:https://juejin.cn/post/6844903993496305671
Swift 加密方案与密钥生成方法|加密方案:【SwiftyRSA与CryptoSwift】、密钥生成方法:【RSA 密钥、 ECC 密钥、国密密钥】
一、SwiftyRSA与CryptoSwift
推荐两个加密方案开源框架:
-
SwiftyRSA
- RSA加密解密
- RSA签名与验证
-
CryptoSwift
- AES
- DES
- MD5
- HASH算法
- ......
具体的使用方式,参照链接中的README.md
二、密钥
根据开发的业务需求,查看生成密钥的方法。密钥包括 RSA 密钥、ECC 密钥、国密密钥。
前置条件
已通过 OpenSSL 官网 下载并安装 OpenSSL 工具(1.1.1 或以上版本)
生成 RSA 密钥
-
打开 OpenSSL 工具,使用以下命令行生成 RSA 私钥。您可以选择生成 1024 或 2048 位的私钥。
openssl genpkey -algorithm RSA -out private_key.pem -pkeyopt rsa_keygen_bits:2048 -
根据 RSA 私钥生成 RSA 公钥。
openssl rsa -pubout -in private_key.pem -out public_key.pem
生成 ECC 密钥
-
打开 OpenSSL 工具,使用以下命令行生成 ECC 的密钥对。您必须选择 secp256k1 椭圆曲线算法。
openssl ecparam -name secp256k1 -genkey -noout -out secp256k1-key.pem -
根据
secp256k1-key.pem密钥对生成 ECC 公钥。
openssl ec -in secp256k1-key.pem -pubout -out ecpubkey.pem
生成国密密钥
-
打开 OpenSSL 工具,使用以下命令行生成国密 SM2 私钥
sm2-key.pem。
openssl ecparam -name SM2 -genkey -noout -out sm2-key.pem -
根据
sm2-key.pem密钥对生成国密 SM2 公钥sm2pubkey.pem。
openssl ec -in sm2-key.pem -pubout -out sm2pubkey.pem
RxSwift以及基于RxSwift封装的库|RxSwift、RxCocoa(转)
前言
最近需要维护公司的老项目,工程代码内部实现基本都是基于RxSwift库、基于RxSwift封装的库实现的代码逻辑;故,筛选转发RxSwift的博文,方便自己快速上手老项目代码。
一、RxSwift
翻了好多博文,最终觉得来自字节跳动的同学写的最精简,容易上手。
系列文章如下:
- RxSwift官方使用示例之<创建和订阅Observables>
- RxSwift官方使用示例之<使用Subjects>
- RxSwift官方使用示例之<组合操作符>
- RxSwift官方使用示例之<转换操作符>
- RxSwift官方使用示例之<过滤及条件操作符>
- RxSwift官方使用示例之<数学和聚合操作符>
- RxSwift官方使用示例之<可连接操作符>
- RxSwift官方使用示例之<错误处理操作符>
二、RxCocoa
01-H5与Native交互-JS与Native互调|WebViewJavaScriptBridge 基本使用(转)
一、iOS中的WebView
在 iOS 开发 Hybrid App 的时候,有两个 WebView 可以选择:
UIWebViewWKWebView
这两个 WebView 控件,可以完全只借助 iOS 自带的框架进行 OC & JS 交互。
-
UIWebView使用JavaScriptCore. -
WKWebView使用WKUserContentController.
二、WebView 与 原生的交互原理
UIWebView 原生的交互原理
通过一个 JSContext 获取 UIWebView 的 JS 执行上下文。
然后通过这个上下文,进行 OC & JS 的双端交互。
_jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
_jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
NSLog(@"%@",@"获取 WebView JS 执行环境失败了!");
};
WKWebView 原生交互原理
通过 userContentController 把需要观察的 JS 执行函数注册起来。
然后通过一个协议方法,将所有注册过的 JS 函数执行的参数传递到此协议方法中。
注册 需要 观察的 JS 执行函数
[webView.configuration.userContentController addScriptMessageHandler:self name:@"jsFunc"];
在 JS 中调用这个函数并传递参数数据
window.webkit.messageHandlers.jsFunc.postMessage({name : "李四",age : 22});
OC 中遵守 WKScriptMessageHandler 协议。
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
此协议方法里的 WKScriptMessage 有 name & body 两个属性。 name 可以用来判断是哪个 JSFunc 调用了。body 则是 JSFunc 传递到 OC 的参数。
三、WebViewJavaScriptBridge
WebViewJavaScriptBridge 用于 WKWebView & UIWebView 中 OC 和 JS 交互。
它的基本原理是:
把 OC 的方法注册到桥梁中,让 JS 去调用。
把 JS 的方法注册在桥梁中,让 OC 去调用。
WebViewJavascriptBridge 基本原理
![]()
注册自己,调用它人。
四、WebViewJavaScriptBridge 使用的基本步骤
- 首先在项目中导入
WebViewJavaScriptBridge 框架。
pod ‘WebViewJavascriptBridge’
- 导入头文件
#import <WebViewJavascriptBridge.h>。 - 建立 WebViewJavaScriptBridge 和 WebView 之间的关系。
_jsBridge = [WebViewJavascriptBridge bridgeForWebView:_webView];
- 在HTML 文件中,复制粘贴这两段 JS 函数。
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback]; // 创建一个 WVJBCallbacks 全局属性数组,并将 callback 插入到数组中。
var WVJBIframe = document.createElement('iframe'); // 创建一个 iframe 元素
WVJBIframe.style.display = 'none'; // 不显示
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__'; // 设置 iframe 的 src 属性
document.documentElement.appendChild(WVJBIframe); // 把 iframe 添加到当前文导航上。
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
// 这里用于注册 Native 将要调用的 JS 方法。
setupWebViewJavascriptBridge(function(bridge){
});
到此为止,基本的准备工作就做完了。现在需要往桥梁中注入 OC 方法 和 JS 函数了。
往桥梁中注入 OC 方法 和 JS 函数
往桥梁中注入 OC 方法。
[_jsBridge registerHandler:@"scanClick" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"dataFrom JS : %@",data[@"data"]);
responseCallback(@"扫描结果 : www.baidu.com");
}];
这段代码的意思:
- scanClick 是 OC block 的一个别名。
- block 本身,是 JS 通过某种方式调用到 scanClick 的时候,执行的代码块。
- data ,由于 OC 这端由 JS 调用,所以 data 是 JS 端传递过来的数据。
- responseCallback OC 端的 block 执行完毕之后,往 JS 端传递的数据。
往桥梁中注入 JS 函数.
OC 方法,在 OC 中注入。JS 的方法所以必然就需要在 JS 中注入的。(好像是废话)
在 JS 的方法如何注入到桥梁呢?
之前,在准备工作的时候,有两段 JS 代码。
需要在第二段 JS 代码中,注入 JS 的函数。
// 这里主要是注册 OC 将要调用的 JS 方法。
setupWebViewJavascriptBridge(function(bridge){
// 声明 OC 需要调用的 JS 方法。
bridge.registerHanlder('testJavaScriptFunction',function(data,responseCallback){
// data 是 OC 传递过来的数据.
// responseCallback 是 JS 调用完毕之后传递给 OC 的数据
alert("JS 被 OC 调用了.");
responseCallback({data: "js 的数据",from : "JS"});
})
});
这段代码的意思:
- testJavaScriptFunction 是注入到桥梁中 JS 函数的别名。以供 OC 端调用。
- 回调函数的 data。 既然 JS 函数由 OC 调用,所以 data 是 OC 端传递过来的数据。
- responseCallback 。 JS 调用在被 OC 调用完毕之后,向 OC 端传递的数据。
基本就是:
OC 端注册 OC 的方法,OC 端调用 JS 的函数。
JS 端注册 JS 的函数,JS 端调用 OC 的方法。
五、原生与Web互调场景
1.JS -> OC 的交互
在 HTML 中,有个按钮,点击这个按钮,修改 NavigationBar 的颜色。
- 在 OC 端,往桥梁注入一个修改 NavigationBar 颜色的 block.
- 在 JS 端,调用这个 block,来间接的达到修改颜色的目的。
![]()
首先,在 OC 中,通过 WebViewJavascriptBridge 注册一个修改 navigationBar 颜色的 Block。
[_jsBridge registerHandler:@"colorClick" handler:^(id data, WVJBResponseCallback responseCallback) {
self.navigationController.navigationBar.barTintColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0 green:arc4random_uniform(256) / 255.0 blue:arc4random_uniform(256) / 255.0 alpha:1.0];
responseCallback(@"颜色修改完毕!");
}];
然后再 JS 中,通过某种方式去调用这个 OC 的 block。
WebViewJavascriptBridge.callHandler('colorClick',function(dataFromOC) {
alert("JS 调用了 OC 注册的 colorClick 方法");
document.getElementById("returnValue").value = dataFromOC;
})
这里通过某种方式就是使用 WebViewJavascriptBridge.callHandler('OC 中block 别名',callback) 的方式来调用。
2.OC -> JS 的交互
OC 上有一个UIButton,点击这儿按钮,把 HTML body 的颜色修改成橙色。
首先,往桥梁中,注入一个修改 HTML body 颜色的 JSFunction。
// 在这里声明 OC 需要主动调用 JS 的方法。
setupWebViewJavascriptBridge(function(bridge) {
bridge.registerHandler('changeBGColor',function(data,responseCallback){
// alert('aaaaaa');
document.body.style.backgroundColor = "orange";
document.getElementById("returnValue").value = data;
});
});
然后在 OC 端通过桥梁调用这个 changeBGColor。
[_jsBridge callHandler:@"changeBGColor" data:@"把 HTML 的背景颜色改成橙色!!!!"];
执行效果:
![]()
六、补充
OC 调用 JS 的三种情况。
// 单纯的调用 JSFunction,不往 JS 传递参数,也不需要 JSFunction 的返回值。
[_jsBridge callHandler:@"changeBGColor"];
// 调用 JSFunction,并向 JS 传递参数,但不需要 JSFunciton 的返回值。
[_jsBridge callHandler:@"changeBGColor" data:@"把 HTML 的背景颜色改成橙色!!!!"];
// 调用 JSFunction ,并向 JS 传递参数,也需要 JSFunction 的返回值。
[_jsBridge callHandler:@"changeBGColor" data:@"传递给 JS 的参数" responseCallback:^(id responseData) {
NSLog(@"JS 的返回值: %@",responseData);
}];
JS 调用 OC 的三种情况。
// JS 单纯的调用 OC 的 block
WebViewJavascriptBridge.callHandler('scanClick');
// JS 调用 OC 的 block,并传递 JS 参数
WebViewJavascriptBridge.callHandler('scanClick',"JS 参数");
// JS 调用 OC 的 block,传递 JS 参数,并接受 OC 的返回值。
WebViewJavascriptBridge.callHandler('scanClick',{data : "这是 JS 传递到 OC 的扫描数据"},function(dataFromOC){
alert("JS 调用了 OC 的扫描方法!");
document.getElementById("returnValue").value = dataFromOC;
});
可以根据实际情况,选择合适的方法。
关于在 OC 中,往桥梁中注入 block 的注意点。
在当前控制器消失的时候,要记得把注入到桥梁中的 OC block,从桥梁中删除。
否则,可能会出现控制器无法释放的情况。
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
[_jsBridge removeHandler:@"scanClick"];
[_jsBridge removeHandler:@"colorClick"];
[_jsBridge removeHandler:@"locationClick"];
[_jsBridge removeHandler:@"shareClick"];
[_jsBridge removeHandler:@"payClick"];
[_jsBridge removeHandler:@"goBackClick"];
}
最后总结:
- UIWebView & JavaScriptCore 等于原生的 JS & OC 交互方案。
- WKWebView & userContentController 等于原生了 JS & OC 交互方案。
- WebViewJavascriptBridge 可以搭配 UIWebView & WKWebView 进行 OC & JS 交互。
- WebViewJavascriptBridge 使用核心,OC 注入 OC 的方法,让 JS 调用。JS 注入 JS 函数,让 OC 调用。
- WebViewJavaScriptBridge 使用的需要 4个前提步骤。
推荐阅读
iOS事件的传递机制|【寻找事件最佳响应者、事件在响应者链上的传递】(转)
iOS的事件分为3大类型
- Touch Events(触摸事件)
- Motion Events(运动事件,比如重力感应和摇一摇等)
- Remote Events(远程事件,比如用耳机上得按键来控制手机)
在开发中,最常用到的就是Touch Events(触摸事件) ,基本贯穿于每个App中,也是本文的主角~ 因此文中所说事件均特指触摸事件。
接下来,记录、涉及的问题大致包括:
- 事件是怎么寻找事件的最佳响应者
- 事件的响应及在响应链中的传递
寻找事件的最佳响应者(Hit-Testing)
当我们触摸屏幕的某个可响应的功能点后,最终都会由UIView或者继承UIView的控件来响应
那我们先来看下UIView的两个方法:
// recursively calls -pointInside:withEvent:. point is in the receiver's coordinate system
//返回寻找到的最终响应这个事件的视图
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
// default returns YES if point is in bounds
//判断某一个点击的位置是否在视图范围内
- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;
每个UIView对象都有一个 hitTest: withEvent: 方法,这个方法是Hit-Testing过程中最核心的存在,其作用是询问事件在当前视图中的响应者,同时又是作为事件传递的桥梁。
看看它是什么时候被调用的
![]()
- 当手指接触屏幕,UIApplication接收到手指的触摸事件之后,就会去调用UIWindow的
hitTest: withEvent:方法 - 在
hitTest: withEvent:方法中会调用pointInside: withEvent:去判断当前点击的point是否属于UIWindow范围内,如果是,就会以倒序的方式遍历它的子视图,即越后添加的视图,越先遍历 - 子视图也调用自身的
hitTest: withEvent:方法,来查找最终响应的视图
再来看个示例:
![]()
视图层级如下(同一层级的视图越在下面,表示越后添加):
A
├── B
│ └── D
└── C
├── E
└── F
现在假设在E视图所处的屏幕位置触发一个触摸,App接收到这个触摸事件事件后,先将事件传递给UIWindow,然后自下而上开始在子视图中寻找最佳响应者。事件传递的顺序如下所示:
![]()
- UIWindow将事件传递给其子视图A
- A判断自身能响应该事件,继续将事件传递给C(因为视图C比视图B后添加,因此优先传给C)。
- C判断自身能响应事件,继续将事件传递给F(同理F比E后添加)。
- F判断自身不能响应事件,C又将事件传递给E。
- E判断自身能响应事件,同时E已经没有子视图,因此最终E就是最佳响应者。
以上,就是寻找最佳响应者的整个过程。
接下来,来看下hitTest: withEvent:方法里,都做些了什么?
我们已经知道事件在响应者之间的传递,是视图通过判断自身能否响应事件来决定是否继续向子视图传递,那么判断响应的条件是什么呢?
视图响应事件的条件:
- 允许交互:
userInteractionEnabled = YES - 禁止隐藏:
hidden = NO - 透明度:
alpha > 0.01 - 触摸点的位置:通过
pointInside: withEvent:方法判断触摸点是否在视图的坐标范围内
代码的表现大概如下:
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
//3种状态无法响应事件
if (!self.isUserInteractionEnabled || self.isHidden || self.alpha <= 0.01) {
return nil;
}
//触摸点若不在当前视图上则无法响应事件
if ([self pointInside:point withEvent:event]) {
//从后往前遍历子视图数组
for (UIView *subView in [self.subviews reverseObjectEnumerator]) {
// 坐标系的转换,把触摸点在当前视图上坐标转换为在子视图上的坐标
CGPoint convertedPoint = [subView convertPoint:point fromView:self];
//询问子视图层级中的最佳响应视图
UIView *hitTestView = [subView hitTest:convertedPoint withEvent:event];
if (hitTestView) {
//如果子视图中有更合适的就返回
return hitTestView;
}
}
//没有在子视图中找到更合适的响应视图,那么自身就是最合适的
return self;
}
return nil;
}
说了这么多,那我们可以运用hitTest: withEvent:来搞些什么事情呢
使超出父视图坐标范围的子视图也能响应事件
![]()
视图层级如下:
css
A
├── B
如上图所示,视图B有一部分是不在父视图A的坐标范围内的,当我们触摸视图B的上半部分,是不会响应事件的。当然,我们可以通过重写视图A的 hitTest: withEvent:方法来解决这个需求。
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event {
UIView *view = [super hitTest:point withEvent:event];
//如果找不到合适的响应者
if (view == nil) {
//视图B坐标系的转换
CGPoint newPoint = [self.deleteButton convertPoint:point fromView:self];
if (CGRectContainsPoint(self.deleteButton.bounds, newPoint)) {
// 满足条件,返回视图B
view = self.deleteButton;
}
}
return view;
}
在视图A的hitTest: withEvent:方法中判断触摸点,是否位于视图B的视图范围内,如果属于,则返回视图B。这样一来,当我们点击视图B的任何位置都可以响应事件了。
事件的响应及在响应链中的传递
经历Hit-Testing后,UIApplication已经知道事件的最佳响应者是谁了,接下来要做的事情就是:
- 将事件传递给最佳响应者响应
- 事件沿着响应链传递
事件传递给最佳响应者
最佳响应者具有最高的事件响应优先级,因此UIApplication会先将事件传递给它供其响应。
UIApplication中有个sendEvent:的方法,在UIWindow中同样也可以发现一个同样的方法。UIApplication是通过这个方法把事件发送给UIWindow,然后UIWindow通过同样的接口,把事件发送给最佳响应者。
以寻找事件的最佳响应者一节中点击视图E为例,在EView的 touchesBegan:withEvent: 上打个断点查看调用栈就能看清这一过程:
![]()
当事件传递给最佳响应者后,响应者响应这个事件,则这个事件到此就结束了,它会被释放。假设响应者没有响应这个事件,那么它将何去何从?事件将会沿着响应链自上而下传递。
注意: 寻找最佳响应者一节中也说到了事件的传递,与此处所说的事件的传递有本质区别。上面所说的事件传递的目的是为了寻找事件的最佳响应者,是自下而上(父视图到子视图)的传递;而这里的事件传递目的是响应者做出对事件的响应,这个过程是自上而下(子视图到父视图)的。前者为“寻找”,后者为“响应”。
事件沿着响应链传递
在UIKit中有一个类:UIResponder,它是所有可以响应事件的类的基类。来看下它的头文件的几个属性和方法
NS_CLASS_AVAILABLE_IOS(2_0) @interface UIResponder : NSObject <UIResponderStandardEditActions>
#if UIKIT_DEFINE_AS_PROPERTIES
@property(nonatomic, readonly, nullable) UIResponder *nextResponder;
#else
- (nullable UIResponder*)nextResponder;
#endif
--------------省略部分代码------------
// Generally, all responders which do custom touch handling should override all four of these methods.
// Your responder will receive either touchesEnded:withEvent: or touchesCancelled:withEvent: for each
// touch it is handling (those touches it received in touchesBegan:withEvent:).
// *** You must handle cancelled touches to ensure correct behavior in your application. Failure to
// do so is very likely to lead to incorrect behavior or crashes.
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEstimatedPropertiesUpdated:(NSSet<UITouch *> *)touches NS_AVAILABLE_IOS(9_1);
UIApplication,UIViewController和UIView都是继承自它,都有一个 nextResponder 方法,用于获取响应链中当前对象的下一个响应者,也通过nextResponder来串成响应链。
在App中,所有的视图都是根据树状层次结构组织起来的,因此,每个View都有自己的SuperView。当一个View被add到SuperView上的时候,它的nextResponder属性就会被指向它的SuperView,各个不同响应者的指向如下:
- UIView 若视图是控制器的根视图,则其
nextResponder为控制器对象;否则,其nextResponder为父视图。 - UIViewController 若控制器的视图是window的根视图,则其
nextResponder为窗口对象;若控制器是从别的控制器present出来的,则其nextResponder为presenting view controller。 - UIWindow
nextResponder为UIApplication对象。 - UIApplication 若当前应用的app delegate是一个UIResponder对象,且不是UIView、UIViewController或app本身,则UIApplication的
nextResponder为app delegate。
这样,整个App就通过nextResponder串成了一条链,也就是我们所说的响应链,子视图指向父视图构成的响应链。
看一下官网对于响应链的示例展示
![]()
若触摸发生在UITextField上,则事件的传递顺序是:
- UITextField ——> UIView ——> UIView ——> UIViewController ——> UIWindow ——> UIApplication ——> UIApplicationDelegte
图中虚线箭头是指若该UIView是作为UIViewController根视图存在的,则其nextResponder为UIViewController对象;若是直接add在UIWindow上的,则其nextResponder为UIWindow对象。
响应者对于事件的拦截以及传递都是通过 touchesBegan:withEvent: 方法控制的,该方法的默认实现是将事件沿着默认的响应链往下传递。
响应者对于接收到的事件有3种操作:
- 不拦截,默认操作 事件会自动沿着默认的响应链往下传递
- 拦截,不再往下分发事件 重写
touchesBegan:withEvent:进行事件处理,不调用父类的touchesBegan:withEvent: - 拦截,继续往下分发事件 重写
touchesBegan:withEvent:进行事件处理,同时调用父类的touchesBegan:withEvent:将事件往下传递
因此,你也可以通过 touchesBegan:withEvent:方法搞点事情~
总结
触摸事件先通过自下而上(父视图-->子视图)的传递方式寻找最佳响应者,
然后以自上而下(子视图-->父视图)的方式在响应链中传递。
iOS多环境配置(转)
基本概念
- Project:包含了项⽬所有的代码,资源⽂件,所有信息。
- Target:对指定代码和资源⽂件的具体构建方式。
- Scheme:对指定Target的环境配置。
通过不同的方式配置多环境
通过Duplicate Target方式配置
![]()
- 通过上述操作之后会多生成一个target,也会多出来一个info.plist
![]()
-
可以定义一个宏的方式,对代码做一些差异化的配置
-
oc在
build setting->Preprocessor Macros -
swift在
build setting->Other Swift Flags,需要注意的是swift中使用宏需要加上-D参数 -
这种方式配置起来缺点
- 生成多个info.plist
- 需要配置的点比较多,比较乱
通过配置Scheme的方式
![]()
- 再通过
Edit Scheme->Manage Schemes新增不同环境的scheme - 再将不同的
Scheme对应到不同的Build Configuration模式下
使用场景举例
- 在日常开发中,不同环境下的
host url的地址都会有不同的差异,通过定义一个宏的方式
![]()
- 再通过
info.plist文件暴露出来
![]()
- 使用方式如下
NSString *path = [[NSBundle mainBundle] pathForResource:@"Info" ofType:@".plist"];
NSDictionary *infoDict = [[NSDictionary alloc] initWithContentsOfFile:path];
NSLog(@"host url:%@",infoDict[@"HOST_URL"]);
复制代码
xcconfig文件
xcconfig指南
- xcconfig文件的语法比较简单,每个配置文件都由一系列键值分配组成,这些键值分配具有以下语法:
BUILD_SETTING_NAME = value
复制代码
-
Build Settingvalue对应字段的缩写,Build Setting字段对应查询
注释
- xcconfig文件只有一种注释方式\。
include导入其他设置
- 在创建xcconfig文件的时候,可以根据需求,创建多个。也就意味着,可以通过include关键字导入其他的xcconfig内的配置。通过include关键字后接上双引号:
#include "Debug.xcconfig"
复制代码
- 在搜索引入的文件时,如果是以/开头,代表绝对路径,例如:
// 表示确切的文件位置
#include "/Users/xx/Desktop/.../xxx.xcconfig"
复制代码
- 或者通过相对路径,以${SRCROOT}路径为开始:
#include "Pods/Target Support Files/xxx.xcconfig"
复制代码
变量
-
变量定义,按照OC命名规则,仅由大写字母,数字和下划线
(_)组成,原则上大写,也可以不。字符串可以是"也可以是'号。 -
变量有三种特殊情况:
-
- 在
xcconfig中定义的变量与Build Settings的一致,那么会发生覆盖。可以通过$(inherited),让当前变量继承变量原有值。例如:
- 在
OTHER_LDFLAGS = -framework SDWebImage OTHER_LDFLAGS = $(inherited) -framework AFNetworking // OTHER_LDFLAGS = -framework SDWebImage -framework AFNetworking 复制代码注意⚠️:有部分变量不能通过xcconfig配置到Build Settings中,例如:配 置PRODUCT_BUNDLE_IDENTIFIER不起作用。 复制代码-
- 引用变量,
$()和${}两种写法都可以:VALUE=value
- 引用变量,
TEACHER=$(VALUE)-${VALUE} 复制代码-
- 条件变量,根据
SDK、Arch和Configration对设置进行条件化,例如:
- 条件变量,根据
// 指定`Configration`是`Debug` // 指定`SDK`是模拟器,还有iphoneos*、macosx*等 // 指定生效架构为`x86_64` OTHER_LDFLAGS[config=Debug][sdk=iphonesimulator*[arch=x86_64]= $(inherited) -framework "Cat" 复制代码注意⚠️:在Xcode 11.4及以后版本,可以使用default,来指定变量为空时的默认值: $(BUILD_SETTING_NAME:default=value) 复制代码 -
优先级(由高到低):
-
- 手动配置Target Build Settings
-
- Target中配置的xcconfig文件
-
- 手动配置Project Build Settings
-
- Project中配置的xcconfig文件
iOS蓝牙知识快速入门(详尽版)(转)
![]()
以前写过几篇蓝牙相关的文章,但是没有涉及扫描、收发指令这些基础功能的实现。所以打算写一篇尽可能详尽的蓝牙知识汇总,一方面给有需要的同学看,一方面是对自己学习蓝牙的一个总结。
这篇文章的目的:教你实现设备的扫描,连接,数据收发,蓝牙数据解析。如果在实现上面任一功能遇到问题时,欢迎留下你的问题,我将进行补充,对于说法有误的地方也请老司机予以指正。
目录
思维导图
![]()
第一次做图,大家凑合着看哈。这张是我总结的蓝牙知识的结构图,下面的内容将围绕这些东西展开进行。
![]()
这张是蓝牙连接发送数据的流程图,下文进入coding阶段的讲解顺序,大家先有个大概印象,等阅读完本文再回来看这张图将理解的更深一些。
苹果对蓝牙设备有什么要求
BLE:bluetouch low energy,蓝牙4.0设备因为低功耗,所有也叫作BLE。苹果在iphone4s及之后的手机型号开始支持蓝牙4.0,这也是最常见的蓝牙设备。低于蓝牙4.0协议的设备需要进行MFI认证,关于MFI认证的申请工作可以看这里:关于MFI认证你所必须要知道的事情
在进行操作蓝牙设备前,我们先下载一个蓝牙工具LightBlue,它可以辅助我们的开发,在进行蓝牙开发之前建议先熟悉一下LightBlue这个工具。
操作蓝牙设备使用什么库
苹果自身有一个操作蓝牙的库CoreBluetooth.framework,这个是大多数人员进行蓝牙开发的首选框架,除此之外目前github还有一个比较流行的对原生框架进行封装的三方库BabyBluetooth,它的机制是将CoreBluetooth中众多的delegate写成了block方法,有兴趣的同学可以了解下。下面主要介绍的是原生蓝牙库的知识。
中心和外围设备
![]()
如图所示,电脑、Pad、手机作为中心,心跳监听器作为外设,这种中心外设模式是最常见的。简单理解就是,发起连接的是中心设备(Central),被连接的是外围设备(Peripheral),对应传统的客户机-服务器体系结构。Central能够扫描侦听到,正在播放广告包的外设。
服务与特征
外设可以包含一个或多个服务(CBService),服务是用于实现装置的功能或特征数据相关联的行为集合。 而每个服务又对应多个特征(CBCharacteristic),特征提供外设服务进一步的细节,外设,服务,特征对应的数据结构如下所示
![]()
如何扫描蓝牙
在进行扫描之前我们需要,首先新建一个类作为蓝牙类,例如FYBleManager,写成单例,作为处理蓝牙操作的管理类。引入头文件#import <CoreBluetooth/CoreBluetooth.h> CBCentralManager是蓝牙中心的管理类,控制着蓝牙的扫描,连接,蓝牙状态的改变。
1、初始化
dispatch_queue_t centralQueue = dispatch_queue_create(“centralQueue",DISPATCH_QUEUE_SERIAL);
NSDictionary *dic = @{
CBCentralManagerOptionShowPowerAlertKey : YES,
CBCentralManagerOptionRestoreIdentifierKey : @"unique identifier"
};
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:centralQueue options:dic];
-
CBCentralManagerOptionShowPowerAlertKey对应的BOOL值,当设为YES时,表示CentralManager初始化时,如果蓝牙没有打开,将弹出Alert提示框 -
CBCentralManagerOptionRestoreIdentifierKey对应的是一个唯一标识的字符串,用于蓝牙进程被杀掉恢复连接时用的。
2、扫描
//不重复扫描已发现设备
NSDictionary *option = @{
CBCentralManagerScanOptionAllowDuplicatesKey : [NSNumber numberWithBool:NO],
CBCentralManagerOptionShowPowerAlertKey:YES
};
[self.centralManager scanForPeripheralsWithServices:nil options:option];
- (void)scanForPeripheralsWithServices:(nullable NSArray<CBUUID *> *)serviceUUIDs options:(nullable NSDictionary<NSString *, id> *)options;
- 扫面方法
-
serviceUUIDs用于第一步的筛选,扫描此UUID的设备 - options有两个常用参数:
-
CBCentralManagerScanOptionAllowDuplicatesKey- 设置为NO表示不重复扫瞄已发现设备,为YES就是允许
CBCentralManagerOptionShowPowerAlertKey- 设置为YES就是在蓝牙未打开的时候显示弹框
-
-
3、CBCentralManagerDelegate代理方法
在初始化的时候我们调用了代理,在CoreBluetooth中有两个代理,
- CBCentralManagerDelegate
- CBPeripheralDelegate
iOS的命名很友好,我们通过名字就能看出,上面那个是关于中心设备的代理方法,下面是关于外设的代理方法。
我们这里先研究CBCentralManagerDelegate中的代理方法
- (void)centralManagerDidUpdateState:(CBCentralManager *)central;
这个方法标了@required是必须添加的,我们在self.centralManager初始换之后会调用这个方法,回调蓝牙的状态。状态有以下几种:
typedef NS_ENUM(NSInteger, CBCentralManagerState{
CBCentralManagerStateUnknown = CBManagerStateUnknown,//未知状态
CBCentralManagerStateResetting = CBManagerStateResetting,//重启状态
CBCentralManagerStateUnsupported = CBManagerStateUnsupported,//不支持
CBCentralManagerStateUnauthorized = CBManagerStateUnauthorized,//未授权
CBCentralManagerStatePoweredOff = CBManagerStatePoweredOff,//蓝牙未开启
CBCentralManagerStatePoweredOn = CBManagerStatePoweredOn,//蓝牙开启} NS_DEPRECATED(NA, NA, 5_0, 10_0, "Use CBManagerState instead”
);
该枚举在iOS10之后已经废除了,系统推荐使用CBManagerState,类型都是对应的
typedef NS_ENUM(NSInteger, CBManagerState{
CBManagerStateUnknown = 0,
CBManagerStateResetting,
CBManagerStateUnsupported,
CBManagerStateUnauthorized,
CBManagerStatePoweredOff,
CBManagerStatePoweredOn,
} NS_ENUM_AVAILABLE(NA, 10_0);
- (void)centralManager:(CBCentralManager *)central
didDiscoverPeripheral:(CBPeripheral *)peripheral
advertisementData:(NSDictionary<NSString *, id> *)advertisementData RSSI:(NSNumber *)RSSI;
- peripheral是外设类
-
advertisementData是广播的值,一般携带设备名,serviceUUIDs等信息 - RSSI绝对值越大,表示信号越差,设备离的越远。如果想转换成百分比强度,(RSSI+100)/100,(这是一个约数,蓝牙信号值并不一定是-100 - 0的值,但近似可以如此表示)
- (void)centralManager:(CBCentralManager *)central willRestoreState:(NSDictionary<NSString *, id> *)dict;
在蓝牙于后台被杀掉时,重连之后会首先调用此方法,可以获取蓝牙恢复时的各种状态
如何连接
在扫面的代理方法中,我们连接外设名是MI的蓝牙设备
- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI{
NSLog(@"advertisementData:%@,RSSI:%@",advertisementData,RSSI);
if([peripheral.name isEqualToString:@"MI"]){
[self.centralManager connectPeripheral:peripheral options:nil];//发起连接的命令
self.peripheral = peripheral;
}
}
连接的状态 对应另外的CBCentralManagerDelegate代理方法 连接成功的回调
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral;
连接失败的回调
- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
连接断开的回调
- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)peripheral error:(nullable NSError *)error;
连接成功之后并没有结束,还记得CBPeripheral中的CBService和CBService中的CBCharacteristic吗,对数据的读写是由CBCharacteristic控制的。
我们先用lightblue连接小米手环为例,来看一下,手环内部的数据是不是我们说的那样。
![]()
其中ADVERTISEMENT DATA显示的就是广播信息。
iOS蓝牙无法直接获取设备蓝牙MAC地址,可以将MAC地址放到这里广播出来
FEEO是ServiceUUIDs,里面的FF01、FF02是CBCharacteristic的UUID
Properties是特征的属性,可以看出FF01具有读的权限,FF02具有读写的权限。
特征拥有的权限类别有如下几种:
typedef NS_OPTIONS(NSUInteger, CBCharacteristicProperties{
CBCharacteristicPropertyBroadcast = 0x01,
CBCharacteristicPropertyRead = 0x02,
CBCharacteristicPropertyWriteWithoutResponse = 0x04,
CBCharacteristicPropertyWrite = 0x08,
CBCharacteristicPropertyNotify = 0x10,
CBCharacteristicPropertyIndicate = 0x20,
CBCharacteristicPropertyAuthenticatedSignedWrites = 0x40,
CBCharacteristicPropertyExtendedProperties = 0x80,
CBCharacteristicPropertyNotifyEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x100,
CBCharacteristicPropertyIndicateEncryptionRequired NS_ENUM_AVAILABLE(NA, 6_0) = 0x200
};
如何发送并接收数据
通过上面的步骤我们发现CBCentralManagerDelegate提供了蓝牙状态监测、扫描、连接的代理方法,但是CBPeripheralDelegate的代理方法却还没使用。别急,马上就要用到了,通过名称判断这个代理的作用,肯定是跟Peripheral有关,我们进入系统API,看它的代理方法都有什么,因为这里的代理方法较多,我就挑选几个常用的拿出来说明一下。
1、代理方法
//发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(nullable NSError *)error;
//发现特征的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(nullable NSError *)error;
//读数据的回调
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
//是否写入成功的回调
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(nullable NSError *)error;
2、步骤
通过这几个方法我们构建一个流程:连接成功->获取指定的服务->获取指定的特征->订阅指定特征值->通过具有写权限的特征值写数据->在didUpdateValueForCharacteristic回调中读取蓝牙反馈值
解释一下订阅特征值:特征值具有Notify权限才可以进行订阅,订阅之后该特征值的value发生变化才会回调didUpdateValueForCharacteristic
3、实现上面流程的实例代码
//连接成功
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)peripheral{
//连接成功之后寻找服务,传nil会寻找所有服务
[peripheral discoverServices:nil];
}
//发现服务的回调
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverServices:(NSError *)error{
if (!error) {
for (CBService *service in peripheral.services) {
NSLog(@"serviceUUID:%@", service.UUID.UUIDString);
if ([service.UUID.UUIDString isEqualToString:ST_SERVICE_UUID]) {
//发现特定服务的特征值
[service.peripheral discoverCharacteristics:nil forService:service];
}
}
}
}
//发现characteristics,由发现服务调用(上一步),获取读和写的characteristics
- (void)peripheral:(CBPeripheral *)peripheral didDiscoverCharacteristicsForService:(CBService *)service error:(NSError *)error {
for (CBCharacteristic *characteristic in service.characteristics) {
//有时读写的操作是由一个characteristic完成
if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]) {
self.read = characteristic;
[self.peripheral setNotifyValue:YES forCharacteristic:self.read];
} else if ([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_WRITE]) {
self.write = characteristic;
}
}
}
//是否写入成功的代理
- (void)peripheral:(CBPeripheral *)peripheral didWriteValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error{
if (error) {
NSLog(@"===写入错误:%@",error);
}else{
NSLog(@"===写入成功");
}
}
//数据接收
- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)error {
if([characteristic.UUID.UUIDString isEqualToString:ST_CHARACTERISTIC_UUID_READ]){ //获取订阅特征回复的数据
NSData *value = characteristic.value;
NSLog(@"蓝牙回复:%@",value);
}
}
比如我们要获取蓝牙电量,由硬件文档查询得知该指令是 0x1B9901,那么获取电量的方法就可以写成
- (void)getBattery{
Byte value[3]={0};
value[0]=x1B;
value[1]=x99;
value[2]=x01;
NSData * data = [NSData dataWithBytes:&value length:sizeof(value)];
//发送数据
[self.peripheral writeValue:data forCharacteristic:self.write type:CBCharacteristicWriteWithoutResponse];
}
如果写入成功,我们将会在didUpdateValueForCharacteristic方法中获取蓝牙回复的信息。
如何解析蓝牙数据
如果你顺利完成了上一步的操作,并且看到了蓝牙返回的数据,那么恭喜你,蓝牙的常用操作你已经了解大半了。因为蓝牙的任务大部分就是围绕发送指令,获取指令,将蓝牙数据呈现给用户。上一步我们已经获取了蓝牙指令,但是获取的却是0x567b0629这样的数据,这是什么意思呢。这时我们参考硬件协议文档,看到这样一段:
![]()
那么我们就可以得出设备电量是 60%。
对数据解析的流程就是:
- 判断校验和是否正确
- 是不是一条正确的数据->该条数据是不是我们需要的电量数据
- 即首字节为
0x567b->根据定义规则解析电量,传给view显示。
- 其中第一步校验数据,视情况而定,也有不需要的情况。