阅读视图
08-iOS 多媒体技术|图形编程框架OpenGL、OpenGL ES、Metal要点回顾【渲染流水线、坐标系、着色器程序、GLSL语言等】
前言
我们在前面,首先进行了针对 iOS中的多媒体技术相关几个框架概述:
- 进而 用 两篇文章 对 其中的
UIKit
相关要点 进行了分述:- 然后我们 针对 Core Animation框架的要点 进一步展开分述:
- 紧接着 我们快速回顾了 2D绘图引擎Core Graphics框架的要点
- 再然后 我们 围绕 滤镜框架Core Image、GPUImage框架的要点 进行了快速回顾:
- 我们 在此篇文章 ,将 针对 底层渲染框架Metal、OpenGL ES框架的要点 进一步展开分述:
一、图形API简介(OpenGL ES、Metal)
1. 走向3D图形世界
关于3D图形世界
的更详细介绍,我推荐阅读书籍: 《OpenGL超级宝典》第一章: 3D图形和OpenGL简介.
幸运的是,已经有图形开发爱好者,将一些重点整理在博文上:3D图形和OpenGL简介
三维(3D)表示一个正在描述或显示的物体具有3个维度:宽度
、高度
和深度
。例如
- 放在书桌上的一张画是一个二维物体,因为它没有可以令人感受得到的深度。
- 而旁边的药瓶却能感受到它是3D,因为它具备
高度
、深度
、宽度
。
几个世纪以来,艺术家们已经知道如何让一幅画有立体感
并且具有真实的深度。
- 通过颜料在二维画布上所创作的作品,它其实本质上画的是一个二维物体
- 类似,计算机3D图像实质上也是平面的,它只是在计算机屏幕上所显示的二维图像,但它可以提供深度的错觉。
2D + 透视 = 3D
2. 图形API简介
无论是2D还是3D图形界面,它们都有相关的编程接口,这里介绍几个图形API:
-
OpenGL(Open Graphics Library)
- 是一种
跨平台
的图形API,用于开发2D和3D图形应用
程序 - 它将计算机的资源抽象称为一个个OpenGL的对象
- 对这些资源的操作抽象为一个个的OpenGL指令。
- 是一种
-
OpenGL ES(OpenGL for Embedded Systems)
- 是OpenGL三维图形API的子集
- 针对手机、PDA和游戏主机等嵌入式设备而设计的,
去除了许多不必要和性能较低的API接口
-
DirectX
- 是由很多API组成的,DirectX并不是一个单纯的图形API。
- 最重要的是DirectX是属于Windows上一个多媒体处理API。
- 并不支持Windows以外的平台,所以不是跨平台框架。
- 按照性质分类,可分为四大部分:
-
显示部分
、声音部分
、输入部分
和网络部分
-
-
Metal
- Apple为游戏开发者推出了新的平台技术,该技术能够为3D图像提高10倍的渲染性能。
- Metal是Apple为解决3D渲染而推出的框架
3. 学习OpenGL ES和Metal的选择
我们首先 引入苹果 官方提供的OpenGL ES
编程指南:OpenGL ES Programming Guide
从指南中,苹果已经提及,在iOS12以后,对OpenGL ES弃用,转向对Metal的支持。
苹果都弃用了OpenGL/OpenGL ES,那还需要学OpenGL ES?
- 1、苹果自身系统迁移到Metal是花费了4年时间
- 2、在没有推出Metal时,苹果对于OpenGL ES是高度集成且配合相应图层和GLKit来辅助开发者能快速使用
OpenGL ES
- 3、
OpenGL ES
的弃用,只是针对苹果内部系统底层API依赖而言- 并不是想让iOS开发者从此不使用
OpenGL ES
- 只是角色变成了第三方,毕竟它的跨平台以及稳定是很难让现有的开发放弃,而这几点Metal目前很难做到
- 并不是想让iOS开发者从此不使用
- 4、目前大多数类似百度地图、高德地图和音视频处理的项目组已经很庞大了,暂时不会迁移到Metal,所以只学习Metal是不够用的
- 5、所以学习需要一步一步的走
OpenGL
->OpenGL ES
->Metal
4. 图形API用于解决什么问题
实现图形的底层渲染
比如:
- 在游戏开发中,对
游戏场景/游戏任务
的渲染 - 在音视频开发中,对于
视频解码后的数据
渲染 - 在地图引擎,对于
地图上的数据渲染
- 在动画中,
实现动画的绘制
- 在视频处理中,对于
视频加上滤镜
效果 - ...
图形API工作的本质
- 图形API工作的本质:就是利用GPU芯片来高效渲染图形图像
- 图形API是iOS开发者唯一接近GPU的方式
二、渲染工作流水线简介
以iOS平台为例。我们先回顾一下渲染工作流水线,再过渡到图形API(如OpenGL ES、Metal)的在流水线上的工作环节和工作流程。再介绍一下流程内涉及到的一些 3D图形技术 术语。如此,我们学习 3D图形技术就不那么突兀了。
1. 渲染工作流水线说明(点击查看详情)
- ①-应用交互前端UIKit/AppKit →
- ②-Core Animation →
- ③ OpenGL ES/ Metal →
- ④ GPU Driver →
- ⑤ GPU →
- ⑥ Screen Display
在屏幕成像的过程中,
CPU
和GPU
起着至关重要的作用
-
CPU(Central Processing Unit,中央处理器)
CPU的主要任务是进行对象的创建和销毁、对象属性的调整、布局计算、文本的计算和排版、图片的格式转换和解码、图像的绘制(Core Graphics) -
GPU(Graphics Processing Unit,图形处理器)
GPU的主要任务是对纹理的渲染 - 小结:
从CPU准备渲染数据(纹理),交由GPU进行渲染
2. Core Animation 渲染流水线详细说明(点击查看详情)
-
①-
CPU阶段
: Core Animation (Commit Transaction)-
CPU阶段
→ 数据缓存 →GPU阶段
- 注:
- CPU、GPU无法直接交换数据
- 从⼀块内存中将数据复制到另⼀块内存中, 传递速度是非常慢的,内存复制数据时, CPU和GPU 都不能 操作数据(避免引起错误)
- OpenGL API 可以 在CPU(
C端
)工作、也可以在GPU(S端
)工作,是两者的桥梁 - Buffers数据 用于
C端
与S端
交互
-
-
②-
GPU阶段
: → Render Server(OpenGL ES、Metal等图形API在工作)
3. GPU图形渲染流水线(点击查看详情)
1. (CPU 负责的时期)Application 应用处理阶段:得到图元
- 这个阶段具体指的就是图像在应用中被处理的阶段,此时还处于 CPU 负责的时期。
- 在这个阶段应用可能会对图像进行一系列的操作或者改变,最终将新的图像信息传给下一阶段
- 这部分信息被叫做图元(primitives)
- 图元(Primitive) 是指 OpenGL ES 中支持渲染的基本图形。
- OpenGL ES 只支持三种图元,分别是
三角形
、线段
、顶点
等。 - 复杂的图形得通过渲染
多个三角形
来实现。
2. Geometry 几何处理阶段:处理图元
- 进入这个阶段之后,以及之后的阶段,就都主要由 GPU 负责了
- 此时 GPU 可以拿到上一个阶段传递下来的图元信息,GPU 会对这部分图元进行处理,之后输出新的图元。这一系列阶段包括:
-
顶点着色器(Vertex Shader)
:
这个阶段中会将图元中的顶点信息进行视角转换
、添加光照信息
、增加纹理
等操作。 -
形状装配(Shape Assembly)
:
图元中的三角形
、线段
、点
分别对应三个 Vertex、两个 Vertex、一个 Vertex。
这个阶段会将 Vertex 连接成相对应的形状。 -
几何着色器(Geometry Shader)
:
额外添加额外的Vertex,将原始图元转换成新图元,以构建一个不一样的模型。
简单来说就是基于通过三角形、线段和点构建更复杂的几何图形。
-
3. Rasterization 光栅化阶段:图元转换为像素
光栅化的主要目的是将几何渲染之后的图元信息,转换为一系列的像素,以便后续显示在屏幕上
- 这个阶段中会根据图元信息,
计算出每个图元所覆盖的像素信息
等,从而将像素划分成不同的部分。
4. Pixel 像素处理阶段:处理像素,得到位图
- 经过上述光栅化阶段,我们得到了图元所对应的像素
- 此时,我们需要给这些像素
填充颜色
和效果
GPU 图形渲染流水线的主要工作可以被划分为两个部分:
- 把 3D 坐标转换为 2D 坐标
- 把 2D 坐标转变为实际的有颜色的像素
4. OpenGL ES工作过程(点击查看详情)
前面提及的 →②
几何处理阶段
→③光栅化阶段
→④像素处理阶段
都属于OpenGL ES
的工作环节。它又可以细分为,上图的6个小阶段:
-
②
几何处理阶段
顶点着色器(Vertex Shader)
形状装配(Shape Assembly)
几何着色器(Geometry Shader)
-
③
光栅化阶段
光栅化(Rasterization)
-
④
像素处理阶段
片段着色器(Fragment Shader)
测试与混合(Tests and Blending)
5. GLES 渲染三角形
简述
GLES 工作过程
:
渲染三角形的基本流程如下图所示,我们通过了解“怎么渲染三角形”,来理解一下 GLES 工作过程
:
其中,顶点着色器和片段着色器是可编程的部分:
- 着色器(Shader) 是一个小程序,它们运行在 GPU 上,在主程序运行的时候进行动态编译,而不用写死在代码里面。
- 编写着色器用的语言是 GLSL(OpenGL Shading Language) (相关的学习,附在下文)
下面介绍一下渲染流程的每一步都做了什么:
1、顶点数据
- 为了渲染一个三角形,我们需要传入一个包含 3 个三维顶点坐标的数组
- 每个顶点都有对应的顶点属性,顶点属性中可以包含任何我们想用的数据。
- 在上图的例子里,我们的每个顶点包含了一个颜色值。
- 并且,为了让 OpenGL ES 知道我们是要绘制三角形,而不是点或者线段,我们在调用绘制指令的时候,都会把图元信息传递给 OpenGL ES 。
2、顶点着色器
-
顶点着色器
会对每个顶点执行一次运算,它可以使用顶点数据
来计算该顶点的坐标
、颜色
、光照
、纹理坐标
等。 -
顶点着色器
的一个重要任务是进行坐标转换,例如将模型的原始坐标系
(一般是指其 3D 建模工具中的坐标)转换到屏幕坐标系
。
3、图元装配
- 在
顶点着色器
程序输出顶点坐标之后,各个顶点按照绘制命令中的图元类型参数
,以及顶点索引数组
被组装成一个个图元。 - 通过这一步,模型中 3D 的图元已经被转化为屏幕上 2D 的图元。
4、几何着色器
- 在「OpenGL」的版本中,
顶点着色器
和片段着色器
之间有一个可选的着色器,叫做几何着色器(Geometry Shader) 。
几何着色器把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的图元来生成其他形状。
OpenGL ES 目前还不支持几何着色器,这个部分我们可以先不关注。
5、光栅化
- 在光栅化阶段,
基本图元
被转换为供片段着色器
使用的片段。 - 片段表示可以被渲染到屏幕上的
像素
,它包含位置
、颜色
、纹理坐标
等信息,这些值是由图元的顶点信息
进行插值计算得到的。 - 在片段着色器运行之前会执行裁切,处于视图以外的所有像素会被裁切掉,用来提升执行效率。
6、片段着色器
-
片段着色器
的主要作用是计算每一个片段最终的颜色值(或者丢弃该片段)。 -
片段着色器
决定了最终屏幕上每一个像素点的颜色值。
7、测试与混合
- 在这一步,OpenGL ES 会根据片段
是否被遮挡
、视图上是否已存
在绘制好的片段等情况,对片段进行丢弃或着混合,最终被保留下来的片段会被写入帧缓存
中,最终呈现在设备屏幕上。
6. GLES 如何渲染多变形
- 由于 OpenGL ES 只能渲染三角形,因此
多边形需要由多个三角形来组成
。
如图所示,一个五边形,我们可以把它拆分成 3 个三角形来渲染。
- 渲染一个三角形,我们需要一个保存 3 个顶点的数组。
- 这意味着我们渲染一个五边形,需要用 9 个顶点。
- 而且我们可以看到,其中 V0 、 V2 、V3 都是重复的顶点,显得有点冗余。
那么有没有更简单的方式,可以让我们复用之前的顶点呢?答案是肯定的。
3种
绘制三角形模式
在 OpenGL ES 中,对于三角形有 3 种绘制模式。在给定的顶点数组相同的情况下,可以指定我们想要的连接方式。如下图所示:
-
1、GL_TRIANGLES
-
GL_TRIANGLES
就是我们一开始说的方式,没有复用顶点,以每三个顶点绘制一个三角形。 - 第一个三角形使用 V0 、 V1 、V2
- 第二个使用 V3 、 V4 、V5 ,以此类推。
- 如果顶点的个数不是 3 的倍数,那么最后的 1 个或者 2 个顶点会被舍弃。
-
-
2、GL_TRIANGLE_STRIP
-
GL_TRIANGLE_STRIP
在绘制三角形的时候,会复用前两个顶点
。 - 第一个三角形依然使用 V0 、 V1 、V2
- 第二个则会使用 V1 、 V2 、V3,以此类推。
- 第 n 个会使用 V(n-1) 、 V(n) 、V(n+1) 。
-
-
3、GL_TRIANGLE_FAN
-
GL_TRIANGLE_FAN
在绘制三角形的时候,会复用第一个顶点和前一个顶点。 - 第一个三角形依然使用 V0 、 V1 、V2
- 第二个则会使用 V0 、 V2 、V3,以此类推。
- 第 n 个会使用 V0 、 V(n) 、V(n+1) 。
- 这种方式看上去像是在绕着 V0 画扇形。
-
接下来,我们就由OpenGL ES
的工作环节,引入图形API相关的技术术语
三、3D图形技术概念
1. OpenGL状态机
-
OpenGL上下文
Context
- 上下文是一个非常庞大的状态机,保存了OpenGL中的各种状态
- 不管在哪个语言中,都是类似C语言一样面向过程的函数
- 我们可以配置多个上下文,通过调用
[EAGLContext setCurrentContext:context]
来切换
-
OpenGL状态机
- 描述了一个对象的生命周期经历各种状态,发生转变时的
动因
、条件
及转变中所执行的活动
- 有记忆功能
- 能记住其当前的状态(如当前所使用的颜色、是否开启了混合功能等):
-
glClearColor(1,1,1,1)
设置颜色 -
glEable(GL_DEPTH_TEST)
开启深度测试 -
glEable(GL_BLEND)
开启混合
- 可以接收输入
-
根据输入
的内容和自己的原先状态
,修改自己当前
状态,并且可以有对应输出
-
- 当进入特殊状态(停机状态)时便不再接收输入,停止工作
- 描述了一个对象的生命周期经历各种状态,发生转变时的
2. 渲染管线
-
渲染
:- 将
图形/图像数据
转换成3D空间图像的操作
叫做渲染(Rendering) - 即
数据->可视化界面
的过程,也就是我们口中所说的绘制
- 将
-
顶点数组
(VertexArray)和顶点缓冲区
(VertexBuffer):-
顶点数据
是由GPU处理的 -
顶点数组
是存在内存中,GPU通过操作内存来处理顶点数据
-
顶点缓冲区
存在显卡显存中,使得GPU的操作更为简单 - 在调用绘制方法的时候,直接有内存传入
顶点数据
,也就是这部分数据之前是存储在内存中的,被称为顶点数组
; - 性能更高的做法是,提前分配一块显存,将
顶点数据
预先传入到显存当中,这块显存就是顶点缓冲区
-
-
管线
:- 可以理解为流水线。在OpenGL下渲染图形,就会经历一个一个节点,这样的操作可以理解为管线。
- 之所以称为管线是因为显卡在处理数据的时候是按照一个固定的顺序来的
-
固定管线/存储着色器
:- 在早期OpenGL版本中,开发者只需要传入相应的参数,就能快速完成图形的渲染。
- 开发者只需要调用API使用封装好的固定shader程序段,并不需要关注底层实现原理
3. 着色器程序Shader
-
着色器程序Shader
- 是一段小程序代码,是用来操作GPU进行计算的,主要的着色器有:
- 顶点着色器(VertexShader)
- 片元着色器(Metal叫片元函数)/片段着色器(FragmentShader)/像素着色器(PixelShader)
- 几何着色器(GeometryShader)
- 曲面细分着色器(TessellationShader)
- 在绘制的时候
- 首先由
顶点着色器
对传入的顶点数据进行运算,将顶点转换为图元; - 然后进行
光栅化
转化为栅格化数据; - 最后传入
片元着色器
进行运算
- 首先由
- 是一段小程序代码,是用来操作GPU进行计算的,主要的着色器有:
-
顶点着色器
(VertexShader)- 用来处理图形每个顶点变换——旋转/平移/投影
- 每一个顶点都会执行一次
-
片元着色器
(FragmentShader)- 用来处理图形中每个像素点的颜色计算和填充
- 每个像素都会执行一次片元着色器(并行执行)
-
GLSL
(OpenGL Shading Language)- 是用来在OpenGL中着色编程的语言,即开发人员写的短小的自定义程序
- 代替了固定渲染管线,使渲染管线中不同层次具有可编程性,比如:视图转换、投影转换等
- 用来操作
顶点着色器
和片元着色器
学习GLSL语言
关于GLSL语言的学习,可以参考这几篇文章:
- GLSL 详解(基础篇):详细介绍 OpenGL ES 2.0 着色器语言 GLSL 基础语法。
- GLSL 详解(高级篇):详细介绍 OpenGL ES 2.0 着色器语言高级特性。
-
OpenGL ES 着色器语言
文章的内容包括:
- GLSL ES 版本介绍
- Shader 的结构
- GLSL ES 中的预处理
- GLSL ES 中的数据类型
- GLSL ES 中向量和矩阵的操作
- GLSL ES 中的限定符
- GLSL ES 中的函数
- GLSL ES 中的内置变量和内置函数
4. 纹理相关
-
光栅化
(Rasterization)- 是把
顶点数据
转换成片元
的过程,具有将图转化为一个个栅格组成的图像的作用(像素数据) - 其实就是将
几何图元变为二维图像
的过程。- 该过程包含了两部分工作:
- 决定窗口坐标中的那些整形栅格区域被基本图元占用;
- 分配
一个颜色值
和一个深度值
到各个区域
- 该过程包含了两部分工作:
- 把
物体的数学描述
以及与物体相关的颜色信息
转换为屏幕上用于对应位置的像素
及用于填充像素的颜色
,这个过程称为光栅化
- 是把
-
缓存
:- OpenGL ES 部分运行在 CPU 上,部分运行在 GPU 上,为了协调这两部分的数据交换,定义了缓存(Buffers) 的概念。
- CPU 和 GPU 都有独自控制的内存区域,缓存可以避免数据在这两块内存区域之间进行复制,提高效率。缓存实际上就是指一块连续的 RAM 。
-
纹理
: 可以简单理解成图片(位图)- 纹理是一个用来保存图像颜色的元素值的缓存
- 渲染是指将数据生成图像的过程。
- 纹理渲染则是将保存在内存中的颜色值等数据,生成图像(位图)的过程。
纹理相关的概念
-
纹素(Texel):
- 一个图像初始化为一个
纹理
缓存后,每个像素会变成一个纹素
。 -
纹理
的坐标是范围是 0 ~ 1,在这个单位长度内,可能包含任意多个纹素
。
- 一个图像初始化为一个
-
片段(Fragment):
- 视口坐标中的颜色像素。
-
没有使用
纹理
时,会使用对象顶点
来计算片段的颜色; -
使用
纹理
时,会根据纹素
来计算。
-
映射(Mapping):
- 对齐顶点和纹素的方式。
- 即将顶点坐标 (X, Y, Z) 与 纹理坐标 (U, V) 对应起来。
-
取样(Sampling):
- 在顶点固定后,每个片段根据计算出来的 (U, V) 坐标,去找相应纹素的过程。
-
帧缓存(Frame Buffer):
- 一个接收渲染结果的缓冲区,为 GPU 指定存储渲染结果的区域。
- 更通俗点,可以理解成存储屏幕上最终显示的一帧画面的区域。
注: (U, V) 可能会超出 0 ~ 1 这个范围,需要通过
glTextParameteri()
配置相应的方案,来映射到 S 轴和 T 轴。
5. 其它
-
混合
:两种颜色的视图叠在一起后的颜色就叫混合 -
变换矩阵
(Transformation):图形发生平移、缩放、旋转变换 -
投影矩阵
(Projection):将3D坐标转换为二维屏幕坐标 -
渲染上屏/交换缓冲区
(SwapBuffer)- 常规的OpenGL程序至少都会有两个缓冲区
- 显示在屏幕上的成为
帧缓冲区 frame buffer
,没有显示的成为离屏缓冲区 offscreen buffer
。 - 在一个缓冲区渲染完成之后,通过将屏幕缓冲区和离屏缓冲区交换,实现图像在屏幕上的显示
- 显示在屏幕上的成为
- 为了防止交换缓冲区的时候屏幕上下区域的图像分属于两个不同的帧,因此交换一般会等待显示器刷新完成的信号,在显示器两次刷新的间各种进行交换,这个信号就成为
垂直同步信号
,这个技术成为垂直同步
- 常规的OpenGL程序至少都会有两个缓冲区
6. 坐标系
6.1 2D 笛卡尔坐标系
- 2D笛卡尔坐标系:
拥有x轴、y轴的平面坐标系(用来描述平面图形)
6.2 3D 笛卡尔坐标系
- 3D笛卡尔坐标系:
拥有x轴、y轴、z轴(z轴表示深度)的立体坐标系(用来描述立体图形)
6.3 投影: 从 3D 到 2D
- 视口: 显示的窗口区域,OpenGL使用
glViewPort
来设置视口 - 投影方式
- 正投影: 用来渲染平面图形(远近物体大小一样)
- 透视投影: 用来渲染立体图形(远小近大)
- 正投影: 用来渲染平面图形(远近物体大小一样)
6.4 OpenGL ES 坐标系
OpenGL ES 坐标系的范围是 -1 ~ 1,是一个三维的坐标系,通常用 X、Y、Z 来表示。Z 轴的正方向指向屏幕外。在不考虑 Z 轴的情况下,左下角为 (-1, -1, 0),右上角为 (1, 1, 0)。
6.5 纹理坐标系
纹理坐标系的范围是 0 ~ 1,是一个二维坐标系,横轴称为 S 轴,纵轴称为 T 轴。在坐标系中,点的横坐标一般用 U 表示,点的纵坐标一般用 V 表示。左下角为 (0, 0),右上角为 (1, 1)。
注: UIKit 坐标系的 (0, 0) 点在左上角,其纵轴的方向和纹理坐标系纵轴的方向刚好相反。
7. 快速了解OpenGL
我在这里推荐几篇文章,可以帮助我们快速了解 Open GL 一些比较核心的基础:
-
***
01-《一看就懂的 OpenGL 基础概念》介绍:- OpenGL 的角色、渲染架构、状态机、渲染管线等内容;
- 要点备注:
- OpenGL 提供了 3 个通道来让我们从 Client 向 Server 中的顶点着色器(Vertex Shader)和片元着色器(Fragment Shader)传递参数和渲染信息
- 02-《一看就懂的 OpenGL 基础概念》:EGL,OpenGL 与设备的桥梁
- 03-《一看就懂的 OpenGL 基础概念》:各种 O 之 VBO、EBO、VAO
- 04-《一看就懂的 OpenGL 基础概念》:各种 O 之 FBO
9. GL ES 编程小Demo
-
github.com/VanZhang-CN…
-
- GLES环境搭建
-
- GLES|绘制三角形
-
- GLES|纹理渲染
-
四、OpenGL ES 开发方式
在iOS中开发 OpenGL ES,有两种方式:
-
- 通过
GLES代码
+系统的GLKit框架
渲染
- 通过
-
- 通过
纯GLES代码
+GLSL着色器小程序
渲染
- 通过
不管是哪种,都需要 对 缓存进行管理(前面提及的各种O,都需要用到缓存:FBO、RBO、VBO、VAO),所以我们先简单介绍下缓存的管理
1. 怎么使用缓存
在实际应用中,我们需要使用各种各样的缓存。比如:
- 在纹理渲染之前,需要生成一块保存了图像数据的纹理缓存。
下面介绍一下缓存管理的一般步骤:
使用缓存的过程可以分为 7 步:
-
生成(Generate):
生成缓存标识符glGenBuffers()
-
绑定(Bind):
对接下来的操作,绑定一个缓存glBindBuffer()
-
缓存数据(Buffer Data):
从CPU的内存复制数据到缓存的内存glBufferData()
/glBufferSubData()
-
启用(Enable)或者禁止(Disable):
设置在接下来的渲染中是否要使用缓存的数据glEnableVertexAttribArray()
/glDisableVertexAttribArray()
-
设置指针(Set Pointers):
告知缓存的数据类型,及相应数据的偏移量glVertexAttribPointer()
-
绘图(Draw):
使用缓存的数据进行绘制glDrawArrays()
/glDrawElements()
-
删除(Delete):
删除缓存,释放资源glDeleteBuffers()
2. 通过 GLKit 渲染
在 GLKit 中,苹果对 OpenGL ES 中的一些操作进行了封装,因此我们使用 GLKit 来渲染会省去一些步骤。
那么好奇的你肯定会问,在「纹理渲染」这件事情上,GLKit 帮我们做了什么呢?
2.1 获取顶点数据
定义顶点数据,用一个三维向量来保存 (X, Y, Z) 坐标,用一个二维向量来保存 (U, V) 坐标:
typedef struct {
GLKVector3 positionCoord; // (X, Y, Z)
GLKVector2 textureCoord; // (U, V)
} SenceVertex;
初始化顶点数据:
self.vertices = malloc(sizeof(SenceVertex) * 4); // 4 个顶点
self.vertices[0] = (SenceVertex){{-1, 1, 0}, {0, 1}}; // 左上角
self.vertices[1] = (SenceVertex){{-1, -1, 0}, {0, 0}}; // 左下角
self.vertices[2] = (SenceVertex){{1, 1, 0}, {1, 1}}; // 右上角
self.vertices[3] = (SenceVertex){{1, -1, 0}, {1, 0}}; // 右下角
退出的时候,记得手动释放内存:
- (void)dealloc {
// other code ...
if (_vertices) {
free(_vertices);
_vertices = nil;
}
}
2.2 初始化 GLKView 并设置上下文
// 创建上下文,使用 2.0 版本
EAGLContext *context = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2];
// 初始化 GLKView
CGRect frame = CGRectMake(0, 100, self.view.frame.size.width, self.view.frame.size.width);
self.glkView = [[GLKView alloc] initWithFrame:frame context:context];
self.glkView.backgroundColor = [UIColor clearColor];
self.glkView.delegate = self;
[self.view addSubview:self.glkView];
// 设置 glkView 的上下文为当前上下文
[EAGLContext setCurrentContext:self.glkView.context];
2.3 加载纹理
使用 GLKTextureLoader
来加载纹理,并用 GLKBaseEffect
保存纹理的 ID ,为后面渲染做准备。
NSString *imagePath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"sample.jpg"];
UIImage *image = [UIImage imageWithContentsOfFile:imagePath];
NSDictionary *options = @{GLKTextureLoaderOriginBottomLeft : @(YES)};
GLKTextureInfo *textureInfo = [GLKTextureLoader textureWithCGImage:[image CGImage]
options:options
error:NULL];
self.baseEffect = [[GLKBaseEffect alloc] init];
self.baseEffect.texture2d0.name = textureInfo.name;
self.baseEffect.texture2d0.target = textureInfo.target;
因为纹理坐标系和 UIKit 坐标系的纵轴方向是相反的,所以将 GLKTextureLoaderOriginBottomLeft
设置为 YES
,用来消除两个坐标系之间的差异。
注: 这里如果用
imageNamed:
来读取图片,在反复加载相同纹理的时候,会出现上下颠倒的错误。
2.4 实现 GLKView 的代理方法
在 glkView:drawInRect:
代理方法中,我们要去实现顶点数据和纹理数据的绘制逻辑。这一步是重点,注意观察「缓存管理的 7 个步骤」的具体用法。
代码如下:
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect {
[self.baseEffect prepareToDraw];
// 创建顶点缓存
GLuint vertexBuffer;
glGenBuffers(1, &vertexBuffer); // 步骤一:生成
glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer); // 步骤二:绑定
GLsizeiptr bufferSizeBytes = sizeof(SenceVertex) * 4;
glBufferData(GL_ARRAY_BUFFER, bufferSizeBytes, self.vertices, GL_STATIC_DRAW); // 步骤三:缓存数据
// 设置顶点数据
glEnableVertexAttribArray(GLKVertexAttribPosition); // 步骤四:启用或禁用
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord)); // 步骤五:设置指针
// 设置纹理数据
glEnableVertexAttribArray(GLKVertexAttribTexCoord0); // 步骤四:启用或禁用
glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord)); // 步骤五:设置指针
// 开始绘制
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); // 步骤六:绘图
// 删除顶点缓存
glDeleteBuffers(1, &vertexBuffer); // 步骤七:删除
vertexBuffer = 0;
}
2.5 开始绘制
我们调用 GLKView
的 display
方法,即可以触发 glkView:drawInRect:
回调,开始渲染的逻辑。
代码如下:
[self.glkView display];
至此,使用 GLKit 实现纹理渲染的过程就介绍完毕了。
3. 通过 GLSL 渲染
在不使用 GLKit 的情况下,怎么实现纹理渲染。我们会着重介绍与 GLKit 渲染不同的部分。
注: Demo可以在“第三节”的第9小节内部找到
3.1 着色器编写
首先,我们需要自己编写着色器,包括顶点着色器和片段着色器,使用的语言是 GLSL 。这里对于 GLSL 就不展开讲了,只解释一下我们等下会用到的部分,更详细的语法内容,可以参见 这里。
新建一个文件,一般顶点着色器用后缀 .vsh
,片段着色器用后缀 .fsh
(当然你不喜欢这么命名也可以,但是为了方便其他人阅读,最好是还是按照这个规范来),然后就可以写代码了。
顶点着色器
顶点着色器的代码如下:
attribute vec4 Position;
attribute vec2 TextureCoords;
varying vec2 TextureCoordsVarying;
void main (void) {
gl_Position = Position;
TextureCoordsVarying = TextureCoords;
}
片段着色器
片段着色器的代码如下:
precision mediump float;
uniform sampler2D Texture;
varying vec2 TextureCoordsVarying;
void main (void) {
vec4 mask = texture2D(Texture, TextureCoordsVarying);
gl_FragColor = vec4(mask.rgb, 1.0);
}
GLSL的简单解释
GLSL 是类 C 语言写成,如果学习过 C 语言,上手是很快的。下面对这两个着色器的代码做一下简单的解释。
-
attribute
修饰符只存在于顶点着色器中,用于储存每个顶点信息的输入,比如这里定义了Position
和TextureCoords
,用于接收顶点的位置和纹理信息。 -
vec4
和vec2
是数据类型,分别指四维向量和二维向量。 -
varying
修饰符指顶点着色器的输出,同时也是片段着色器的输入,要求顶点着色器和片段着色器中都同时声明,并完全一致,则在片段着色器中可以获取到顶点着色器中的数据。 -
gl_Position
和gl_FragColor
是内置变量,对这两个变量赋值,可以理解为向屏幕输出片段的位置信息和颜色信息。 -
precision
可以为数据类型指定默认精度,precision mediump float
这一句的意思是将float
类型的默认精度设置为mediump
。 -
uniform
用来保存传递进来的只读值,该值在顶点着色器和片段着色器中都不会被修改。顶点着色器和片段着色器共享了uniform
变量的命名空间,uniform
变量在全局区声明,同个uniform
变量在顶点着色器和片段着色器中都能访问到。 -
sampler2D
是纹理句柄类型,保存传递进来的纹理。 -
texture2D()
方法可以根据纹理坐标,获取对应的颜色信息。
那么这两段代码的含义就很明确了,顶点着色器将输入的顶点坐标信息直接输出,并将纹理坐标信息传递给片段着色器;片段着色器根据纹理坐标,获取到每个片段的颜色信息,输出到屏幕。
3.2 纹理的加载
少了 GLKTextureLoader
的相助,我们就只能自己去生成纹理了。生成纹理的步骤比较固定,以下封装成一个方法:
- (GLuint)createTextureWithImage:(UIImage *)image {
// 将 UIImage 转换为 CGImageRef
CGImageRef cgImageRef = [image CGImage];
GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
CGRect rect = CGRectMake(0, 0, width, height);
// 绘制图片
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
void *imageData = malloc(width * height * 4);
CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);
CGContextDrawImage(context, rect, cgImageRef);
// 生成纹理
GLuint textureID;
glGenTextures(1, &textureID);
glBindTexture(GL_TEXTURE_2D, textureID);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData); // 将图片数据写入纹理缓存
// 设置如何把纹素映射成像素
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// 解绑
glBindTexture(GL_TEXTURE_2D, 0);
// 释放内存
CGContextRelease(context);
free(imageData);
return textureID;
}
3.3 着色器的编译链接
对于写好的着色器,需要我们在程序运行的时候,动态地去编译链接。编译一个着色器的代码也比较固定,这里通过后缀名来区分着色器类型,直接看代码:
- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType {
// 查找 shader 文件
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"]; // 根据不同的类型确定后缀名
NSError *error;
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSAssert(NO, @"读取shader失败");
exit(1);
}
// 创建一个 shader 对象
GLuint shader = glCreateShader(shaderType);
// 获取 shader 的内容
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = (int)[shaderString length];
glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
// 编译shader
glCompileShader(shader);
// 查询 shader 是否编译成功
GLint compileSuccess;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSAssert(NO, @"shader编译失败:%@", messageString);
exit(1);
}
return shader;
}
顶点着色器和片段着色器同样都需要经过这个编译的过程,编译完成后,还需要生成一个着色器程序,将这两个着色器链接起来,代码如下:
- (GLuint)programWithShaderName:(NSString *)shaderName {
// 编译两个着色器
GLuint vertexShader = [self compileShaderWithName:shaderName type:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];
// 挂载 shader 到 program 上
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
// 链接 program
glLinkProgram(program);
// 检查链接是否成功
GLint linkSuccess;
glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSAssert(NO, @"program链接失败:%@", messageString);
exit(1);
}
return program;
}
这样,我们只要将两个着色器命名统一,按照规范添加后缀名。然后将着色器名称传入这个方法,就可以获得一个编译链接好的着色器程序。
有了着色器程序后,我们就需要往程序中传入数据,首先要获取着色器中定义的变量,具体操作如下:
注: 不同类型的变量获取方式不同。
GLuint positionSlot = glGetAttribLocation(program, "Position");
GLuint textureSlot = glGetUniformLocation(program, "Texture");
GLuint textureCoordsSlot = glGetAttribLocation(program, "TextureCoords");
传入生成的纹理 ID:
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, textureID);
glUniform1i(textureSlot, 0);
glUniform1i(textureSlot, 0)
的意思是,将 textureSlot
赋值为 0
,而 0
与 GL_TEXTURE0
对应,这里如果写 1
,glActiveTexture
也要传入 GL_TEXTURE1
才能对应起来。
设置顶点数据:
glEnableVertexAttribArray(positionSlot);
glVertexAttribPointer(positionSlot, 3, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, positionCoord));
设置纹理数据:
glEnableVertexAttribArray(textureCoordsSlot);
glVertexAttribPointer(textureCoordsSlot, 2, GL_FLOAT, GL_FALSE, sizeof(SenceVertex), NULL + offsetof(SenceVertex, textureCoord));
3.4 Viewport 的设置
在渲染纹理的时候,我们需要指定 Viewport 的尺寸,可以理解为渲染的窗口大小。调用 glViewport
方法来设置:
glViewport(0, 0, self.drawableWidth, self.drawableHeight);
// 获取渲染缓存宽度
- (GLint)drawableWidth {
GLint backingWidth;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &backingWidth);
return backingWidth;
}
// 获取渲染缓存高度
- (GLint)drawableHeight {
GLint backingHeight;
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &backingHeight);
return backingHeight;
}
3.5 渲染层的绑定
通过以上步骤,我们已经拥有了纹理,以及顶点的位置信息。现在到了最后一步,我们要怎么将缓存与视图关联起来?换句话说,假如屏幕上有两个视图,OpenGL ES 要怎么知道将图像渲染到哪个视图上?
所以我们要进行渲染层绑定。通过 renderbufferStorage:fromDrawable:
来实现:
- (void)bindRenderLayer:(CALayer <EAGLDrawable> *)layer {
GLuint renderBuffer; // 渲染缓存
GLuint frameBuffer; // 帧缓存
// 绑定渲染缓存要输出的 layer
glGenRenderbuffers(1, &renderBuffer);
glBindRenderbuffer(GL_RENDERBUFFER, renderBuffer);
[self.context renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer];
// 将渲染缓存绑定到帧缓存上
glGenFramebuffers(1, &frameBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, frameBuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER,
GL_COLOR_ATTACHMENT0,
GL_RENDERBUFFER,
renderBuffer);
}
以上代码生成了一个帧缓存和一个渲染缓存,并将渲染缓存挂载到帧缓存上,然后设置渲染缓存的输出层为 layer
。
最后,将绑定的渲染缓存呈现到屏幕上:
[self.context presentRenderbuffer:GL_RENDERBUFFER];
至此,使用 GLSL 渲染纹理的关键步骤就结束了。
最终效果:
4.GLKit 主要帮我们做了以下几个点:
- 着色器的编写: GLKit 内置了简单的着色器,不用我们自己去编写。
-
纹理的加载:
GLKTextureLoader
封装了一个将 Image 转化为 Texture 的方法。 -
着色器的编译链接:
GLKBaseEffect
内部实现了着色器的编译链接过程,我们在使用过程中基本可以忽略「着色器」这个概念。 -
Viewport 的设置: 在渲染纹理的时候,需要指定 Viewport 的大小,
GLKView
在调用display
方法的时候,会在内部去设置。 -
渲染层的绑定:
GLKView
内部会调用renderbufferStorage:fromDrawable:
将自身的layer
设置为渲染缓存的输出层。因此,在调用display
方法的时候,内部会调用presentRenderbuffer:
去将渲染缓存呈现到屏幕上。
待抽空整理
# 五、OpenGL ES
## 1. 绘制三角形
## 2. 绘制更多图形
## 3. 变换矩阵
## 4. 透视投影和正交投影
## 5. 摄像机
## 6. 绘制一个正方体
## 7. 基本光照
## 8. 基本纹理
...
# Metal
## 1. Metal 与 OpenGL ES 区别
## 2. Metal 与 Metal Kit 区别
## 3. Metal 与 OpenGL ES 对比学习
## 4. Metal Shading Language
Metal 程序中的Metal 着色语言
## 5. Metal 向量和矩阵数据类型
推荐
-
- 红宝书 OpenGL Programming Guide,出到第八版
-
- 蓝宝书 OpenGL SuperBible,出到第六版
-
- OpenGL ES 开篇 : 以 Q&A 的形式,列举出在学习 OpenGL ES 之前会存在的一些疑惑。权衡是否该继续学习 OpenGL ES。
-
- OpenGL ES 基础概念:扫盲篇,先介绍一些必须了解的知识,便于之后能直接进入实战阶段。
-
- OpenGL ES 环境搭建:详解 OpenGL ES 接入方式,以最基础效果(设置背景色)来阐述。
-
- OpenGL ES 渲染基本图元:详细介绍可编程图形渲染管线是如何工作的。
-
- GLSL 详解(基础篇):详细介绍 OpenGL ES 2.0 着色器语言 GLSL 基础语法。
-
- GLSL 详解(高级篇):详细介绍 OpenGL ES 2.0 着色器语言高级特性。
07-iOS 多媒体技术| 滤镜框架Core Image、GPUImage要点回顾【滤镜链、内置滤镜、自定义滤镜、GPUImage的简单使用等】
06-iOS 多媒体技术| Core Graphics要点回顾【UIBezierPath绘制、CGContext绘制、图像处理、富文本处理、经典第三方库等】
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等
前言
多媒体技术 包括 视觉图形处理
、音频视频处理
、XR技术
(AR、VR)等。都是有趣且富有含金量的技术实践方向。
本篇文章是 iOS 视觉图形处理
技术核心框架简介,为我们快速了解 iOS 视觉图形 技术,以便于在项目实战中进行技术选型、为提升自己在图形学上的技术能力指明方向,为后期我们在图形学方面的项目实践在某一个点的深入埋下伏笔。
我们在探索 iOS 图层渲染原理的时候,对 iOS 各个渲染框架 和 渲染技术栈 做了相关探索,
1. iOS的各个渲染框架
在介绍渲染框架之前,我们得先了解一下iOS系统的渲染流水和具体的渲染技术栈
1.渲染技术栈
iOS 的渲染框架依然符合渲染流水线的基本架构,具体的技术栈如上图所示
- 在硬件基础之上,iOS 中有
Core Graphics
、Core Animation
、Core Image
、OpenGL
等多种软件框架来绘制内容,在 CPU 与 GPU 之间进行了更高层地封装
一、iOS图像处理相关框架汇总
1. 苹果系统自带的框架:
- 图形界面
UIKit
- 核心动画
Core Animation
- 主要在GPU上工作,用于动画效果和图层管理
- 2D图形绘制和渲染
Core Graphics(Quartz 2D)
- 主要在CPU上工作,用于2D图形绘制和渲染
- 图像处理和滤镜 Core Image
- 主要在GPU上工作,用于图像处理和滤镜效果
- 图形渲染和计算 Metal
- 主要在GPU上工作,用于高性能图形渲染和计算
- 游戏引擎
- Scene Kit (3D)
- Sprite Kit (2D)
2. 优秀的第三方库:
- 滤镜处理 GPUImage
- 主要在GPU上工作,提供了丰富的图像处理和滤镜功能
- 计算机视觉 OpenCV
- 主要在CPU上工作,用于计算机视觉和图像处理
- 跨平台图形框架 OpenGL ES
- 主要在GPU上工作,用于实现3D图形渲染和游戏开发
毫无疑问,开发者们接触得最多的框架是以下几个,UIKit、Core Animation,Core Graphic, Core Image。下面简要介绍这几个框架,顺便介绍下GPUImage
:
二、UIKit框架
1. UIKit框架简介
UIKit是iOS应用程序开发的基础框架之一,也是iOS开发中的一个核心框架。它提供了一系列的类和组件,通过UIKit,开发者可以快速构建各种界面元素、实现用户交互和动画效果.
UIKit的主要功能和组件:
- 视图(View):用于构建应用程序的用户界面,包括
- 基本视图
- 容器视图
- 表视图
- 集合视图
- ...
- 控件(Control):提供了各种用户交互控件,如:
- 按钮、标签、文本框、滑块等
- 视图控制器(View Controller):用于管理视图的显示和交互,包括:
- UIViewController、UINavigationController、UITabBarController等。
- 动画(Animation):提供了动画效果的支持,如:
- 视图动画、过渡动画、关键帧动画等。
- 手势识别(Gesture Recognizer): 用于识别和处理用户手势操作,如:
- 点击、滑动、捏合等。
- 绘图(Drawing):提供了绘制图形和文本的功能,如:
- 绘制形状、渲染文本、处理图形上下文等。
- 1. 绘制形状:
- 使用UIBezierPath类可以绘制各种形状,如直线、曲线、矩形、圆角矩形、椭圆等。
- 通过设置路径的属性(如线宽、颜色、填充等)可以定制绘制效果。
- 2. 绘制文本:
- 使用NSString和NSAttributedString类可以绘制文本内容。
- 通过UILabel、UITextView等控件可以显示文本内容,也可以通过Core Text框架实现更复杂的文本排版效果。
- 3. 绘制图像:
- 使用UIImage类可以加载和显示图像。
- 通过UIImageView控件可以显示图像,也可以通过Core Graphics框架实现图像的绘制和处理。
- 4. 绘制图形上下文:
- 使用UIGraphicsBeginImageContextWithOptions函数可以创建一个图形上下文。
- 在图形上下文中可以进行绘制操作,如绘制形状、文本、图像等。
- 使用UIGraphicsGetImageFromCurrentImageContext函数可以获取绘制的图像。
- 5. 绘制动画:
- 使用UIView的动画方法(如animateWithDuration:animations:)可以实现简单的视图动画效果。
- 通过Core Animation框架可以实现更复杂的动画效果,如关键帧动画、过渡动画等。
- 6. 绘制路径:
- 使用UIBezierPath类可以创建和操作路径对象,实现复杂的路径绘制和操作。
- 通过CAShapeLayer图层可以将路径添加到视图中进行显示。
- 1. 绘制形状:
- 绘制形状、渲染文本、处理图形上下文等。
- 文本排版(Text Layout):支持文本的排版和显示,包括文本样式、字体、段落样式等。
- 多任务处理(Multitasking): 支持应用程序在多任务环境下的处理和适配。
- 1. 多任务处理模式:
- 前台模式:应用程序在前台运行,响应用户交互和显示界面。
- 后台模式:应用程序在后台运行,执行一些特定的任务,如音频播放、位置更新等。
- 多任务处理:应用程序可以同时执行多个任务,如下载数据、处理网络请求等。
- 2. 多任务处理功能:
- 后台执行任务:通过使用后台任务、后台会话等机制,应用程序可以在后台执行一些任务,如下载、上传数据等。
- 多线程处理:通过使用GCD(Grand Central Dispatch)和Operation Queue等多线程技术,应用程序可以在多个线程上执行并发任务,提高性能和响应速度。
- 多任务处理状态管理:应用程序可以通过UIApplication类的状态变化通知来管理多任务处理状态,如进入后台、恢复前台等。
- 3. 多任务处理场景:
- 音频播放:应用程序可以在后台继续播放音频。
- 位置更新:应用程序可以在后台更新位置信息。
- 网络请求:应用程序可以在后台执行网络请求和数据下载。
- 数据处理:应用程序可以在后台处理数据、计算等任务。
- 1. 多任务处理模式:
- 其它:
- 一些类: UIColor(颜色操作)、UIFont和UIScreen(提供字体和屏幕信息)
2. UIKit与Core Graphics的关系:
UIKit的绘图功能主要用于实现简单的图形绘制、文本显示和图像处理,适用于构建基本的用户界面元素和视图效果。
对于更复杂的绘图需求,可以结合Core Graphics
框架来实现更丰富用户界面和图形效果
以下是UIKit与Core Graphics结合使用的一些常见场景和方法:
-
1. 绘制自定义视图:
- 可以通过继承UIView类,并重写drawRect方法,在其中使用Core Graphics绘制自定义的图形、文本或图像。
- 在drawRect方法中,可以创建UIBezierPath对象、设置绘制属性(如颜色、线宽等),并调用Core Graphics的绘制方法来实现自定义绘制效果。
-
2. 绘制图形和文本:
- 使用UIKit提供的控件和视图来构建用户界面,同时可以使用Core Graphics来实现一些特殊的绘制效果,如渐变背景、阴影效果等。
- 通过Core Graphics的文本绘制功能,可以实现更灵活的文本排版和样式设置。
-
3. 图形上下文的管理:
- 可以通过UIGraphicsGetCurrentContext函数获取当前的图形上下文,然后使用Core Graphics在该上下文中进行绘制操作。
- 在绘制过程中,可以使用UIGraphicsPushContext和UIGraphicsPopContext函数来管理图形上下文的压栈和出栈。
-
4. 图形动画效果:
- 可以结合UIKit的动画方法和Core Graphics的绘制功能,实现一些复杂的图形动画效果。
- 通过UIView的动画方法和Core Graphics的绘制方法结合使用,可以实现视图的平滑过渡和动态效果。
-
5. 图形处理和滤镜效果:
- 可以使用Core Graphics对图像进行处理,如裁剪、缩放、旋转等操作。
- 结合Core Image框架,可以实现更复杂的图像处理和滤镜效果,为用户界面增加更多的视觉效果。
3. 代码示例:
在UIKit中,UIView类本身在绘制时自动创建一个图形环境,即Core Graphics
层的CGContext
类型,作为当前的图形绘制环境。
在绘制时可以调用 UIGraphicsGetCurrentContext
函数获得当前的图形环境;
3.1 绘制自定义视图|绘制路径:
Objective-C示例:
//这段代码就是在UIView的子类中调用 UIGraphicsGetCurrentContext 函数获得当前的图形环境,然后向该图形环境添加路径,最后绘制。
- (void)drawRect:(CGRect)rect {
//1.获取上下文
CGContextRef contextRef = UIGraphicsGetCurrentContext();
//2.描述路径
UIBezierPath * path = [UIBezierPath bezierPath];
//起点
[path moveToPoint:CGPointMake(10, 10)];
//终点
[path addLineToPoint:CGPointMake(100, 100)];
//设置颜色
[[UIColor whiteColor]setStroke];
//3.添加路径
CGContextAddPath(contextRef, path.CGPath);
//显示路径
CGContextStrokePath(contextRef);
}
// Objective-C 代码示例:
// CustomView.h
#import <UIKit/UIKit.h>
@interface CustomView : UIView
@end
// CustomView.m
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
//1.获取上下文
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制路径
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2.0);
CGContextMoveToPoint(context, 50, 50);
CGContextAddLineToPoint(context, 200, 200);
CGContextStrokePath(context);
}
@end
Swift示例:
// Swift 代码示例
// CustomView.swift
import UIKit
class CustomView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
let path = UIBezierPath()
path.move(to: CGPoint(x: 50, y: 50))
path.addLine(to: CGPoint(x: 150, y: 150))
path.addArc(withCenter: CGPoint(x: 100, y: 100), radius: 50, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true)
context.setStrokeColor(UIColor.red.cgColor)
context.setLineWidth(2.0)
context.addPath(path.cgPath)
context.strokePath()
}
}
}
3.2 绘制自定义视图|绘制形状、文本等:
Objective-C示例:
// CustomView.h
#import <UIKit/UIKit.h>
@interface CustomView : UIView
@end
// CustomView.m
#import "CustomView.h"
@implementation CustomView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制矩形
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextFillRect(context, CGRectMake(50, 50, 100, 100));
// 绘制圆形
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextFillEllipseInRect(context, CGRectMake(150, 150, 50, 50));
// 绘制文本
NSString *text = @"Hello, Core Graphics!";
[text drawAtPoint:CGPointMake(50, 200) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor greenColor]}];
}
@end
Swift示例:
// CustomView.swift
import UIKit
class CustomView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 绘制矩形
context.setFillColor(UIColor.blue.cgColor)
context.fill(CGRect(x: 50, y: 50, width: 100, height: 100))
// 绘制圆形
context.setFillColor(UIColor.red.cgColor)
context.fillEllipse(in: CGRect(x: 150, y: 150, width: 50, height: 50))
// 绘制文本
let text = "Hello, Core Graphics!"
let attributes: [NSAttributedString.Key: Any] = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.green]
text.draw(at: CGPoint(x: 50, y: 200), withAttributes: attributes)
}
}
}
3.3 创建自定义按钮:
Objective-C示例:
// Objective-C示例:
// CustomButton.h
#import <UIKit/UIKit.h>
@interface CustomButton : UIButton
@end
// CustomButton.m
#import "CustomButton.h"
@implementation CustomButton
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制背景
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextFillRect(context, rect);
// 绘制边框
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextStrokeRect(context, rect);
// 绘制文本
NSString *text = @"Custom Button";
NSDictionary *attributes = @{ NSFontAttributeName: [UIFont systemFontOfSize:16.0], NSForegroundColorAttributeName: [UIColor whiteColor] };
CGSize textSize = [text sizeWithAttributes:attributes];
CGPoint textOrigin = CGPointMake((CGRectGetWidth(rect) - textSize.width) / 2, (CGRectGetHeight(rect) - textSize.height) / 2);
[text drawAtPoint:textOrigin withAttributes:attributes];
}
@end
Swift示例:
// Swift示例:
// CustomButton.swift
import UIKit
class CustomButton: UIButton {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 绘制背景
context.setFillColor(UIColor.blue.cgColor)
context.fill(rect)
// 绘制边框
context.setStrokeColor(UIColor.white.cgColor)
context.stroke(rect)
// 绘制文本
let text = "Custom Button"
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16.0), NSAttributedString.Key.foregroundColor: UIColor.white]
let textSize = text.size(withAttributes: attributes)
let textOrigin = CGPoint(x: (rect.width - textSize.width) / 2, y: (rect.height - textSize.height) / 2)
text.draw(at: textOrigin, withAttributes: attributes)
}
}
}
3.4 绘制图像和文本:
Objective-C示例:
// Objective-C示例:
// 在UIView的drawRect方法中结合Core Graphics绘制图像和文本
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制图像
UIImage *image = [UIImage imageNamed:@"exampleImage"];
CGContextDrawImage(context, CGRectMake(20, 20, 100, 100), image.CGImage);
// 添加滤镜效果
CGContextSetBlendMode(context, kCGBlendModeMultiply);
CGContextSetAlpha(context, 0.5);
// 绘制文本
NSString *text = @"Hello, World!";
[text drawAtPoint:CGPointMake(20, 150) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
}
Swift示例:
// Swift示例:
// 在UIView的draw方法中结合Core Graphics绘制图像和文本
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 绘制图像
if let image = UIImage(named: "exampleImage") {
context.draw(image.cgImage!, in: CGRect(x: 20, y: 20, width: 100, height: 100))
}
// 添加滤镜效果
context.setBlendMode(.multiply)
context.setAlpha(0.5)
// 绘制文本
let text = "Hello, World!"
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.red]
text.draw(at: CGPoint(x: 20, y: 150), withAttributes: attributes)
}
}
3.5 绘制动画效果:
Objective-C示例:
// Objective-C示例:
#import <UIKit/UIKit.h>
@interface CustomView : UIView
@end
@implementation CustomView
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制路径
CGContextSetStrokeColorWithColor(context, [UIColor.redColor CGColor]);
CGContextSetLineWidth(context, 2.0);
CGContextMoveToPoint(context, 50, 50);
CGContextAddLineToPoint(context, 200, 200);
CGContextStrokePath(context);
// 创建路径动画
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:@"position"];
pathAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 50)];
pathAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(200, 200)];
pathAnimation.duration = 2.0;
// 添加动画
[self.layer addAnimation:pathAnimation forKey:@"position"];
}
@end
Swift示例:
// Swift示例:
import UIKit
class CustomView: UIView {
override func draw(_ rect: CGRect) {
if let context = UIGraphicsGetCurrentContext() {
// 绘制路径
context.setStrokeColor(UIColor.red.cgColor)
context.setLineWidth(2.0)
context.move(to: CGPoint(x: 50, y: 50))
context.addLine(to: CGPoint(x: 200, y: 200))
context.strokePath()
// 创建路径动画
let pathAnimation = CABasicAnimation(keyPath: "position")
pathAnimation.fromValue = CGPoint(x: 50, y: 50)
pathAnimation.toValue = CGPoint(x: 200, y: 200)
pathAnimation.duration = 2.0
// 添加动画
layer.add(pathAnimation, forKey: "position")
}
}
}
3.6 绘制图形上下文:
Objective-C示例:
// Objective-C示例:
- (UIImage *)drawCustomImage {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(200, 200), NO, 0.0);
CGContextRef context = UIGraphicsGetCurrentContext();
// 绘制矩形
CGContextSetFillColorWithColor(context, [UIColor blueColor].CGColor);
CGContextFillRect(context, CGRectMake(50, 50, 100, 100));
// 绘制文本
NSString *text = @"Hello, Core Graphics!";
[text drawAtPoint:CGPointMake(20, 20) withAttributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16], NSForegroundColorAttributeName: [UIColor redColor]}];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Swift示例:
// Swift示例:
func drawCustomImage() -> UIImage? {
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))
// 绘制文本
let text = "Hello, Core Graphics!"
let attributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), NSAttributedString.Key.foregroundColor: UIColor.red]
text.draw(at: CGPoint(x: 20, y: 20), withAttributes: attributes)
if let image = UIGraphicsGetImageFromCurrentImageContext() {
UIGraphicsEndImageContext()
return image
}
}
return nil
}
三、 Core Animation 框架
1. 主要特点和功能
Core Animation框架是iOS和macOS平台上用于实现动画效
果和图层管理
的核心框架。
它提供了一套强大的API,用于创建、组合和管理图层,实现各种动画效果和视觉效果。以下是Core Animation框架的一些主要特点和功能:
-
- 图层(CALayer)与图层管理:
- CALayer是Core Animation框架的核心,用于管理视图的显示和动画效果。
- CALayer提供了丰富的属性和方法,用于控制视图的外观、位置、大小等。
- CALayer负责视图的渲染、布局、动画等操作,是视图的可视化表示。
-
- 动画(CAAnimation):
- CAAnimation是Core Animation框架中用于实现动画效果的基类。
- 可以通过设置动画的属性和持续时间来实现各种动画效果。
- CAAnimation包括:
- 基础动画(CABasicAnimation)
- 关键帧动画(CAKeyframeAnimation)
- 过渡动画(CATransition)等类型。
-
- 图层组合:
- 可以通过图层组合和图层树的方式来管理和组织视图的层级关系。
- 可以创建复杂的图层结构,实现多层次的视图组合和效果叠加。
-
- 隐式动画:
- Core Animation框架支持隐式动画,即通过改变图层的属性值来自动触发动画效果。
- 可以通过设置图层的属性值来实现简单的动画效果,无需显式创建动画对象。
-
- 图层渲染:
- Core Animation框架利用GPU硬件加速来实现图层的渲染和动画效果。
- 可以实现流畅的动画效果和高性能的图层渲染。
-
- 交互效果:
- Core Animation框架支持用户交互效果,如手势识别、点击事件等。
- 可以通过手势识别和事件处理来实现交互式的动画效果。
2. 核心类介绍
Core Animation框架中的核心类主要包括以下几个:
-
- CALayer:
- CALayer是Core Animation框架中的核心类,用于管理视图的显示和动画效果。
- CALayer负责视图的渲染、布局、动画等操作,是视图的可视化表示。
- CALayer提供了丰富的属性和方法,用于控制视图的外观、位置、大小等。
-
- CAAnimation:
- CAAnimation是Core Animation框架中用于实现动画效果的基类。
- CAAnimation包括基础动画(CABasicAnimation)、关键帧动画(CAKeyframeAnimation)、过渡动画(CATransition)等类型。
- 开发者可以通过CAAnimation来创建各种动画效果,如平移、旋转、缩放等。
-
- CATransform3D:
- CATransform3D是Core Animation框架中用于实现3D变换的类。
- CATransform3D可以实现视图的平移、旋转、缩放等3D变换效果。
- 开发者可以通过CATransform3D来创建复杂的3D变换效果,实现炫酷的视觉效果。
-
- CAEmitterLayer:
- CAEmitterLayer是Core Animation框架中用于实现粒子效果的类。
- CAEmitterLayer可以创建和管理粒子系统,实现雪花、火焰、烟雾等效果。
- 开发者可以通过CAEmitterLayer来实现丰富的粒子效果,为应用程序增加动感和生动性。
-
- CAShapeLayer:
- CAShapeLayer是Core Animation框架中用于绘制形状的类。
- CAShapeLayer可以创建和管理各种形状,如矩形、圆角矩形、椭圆等。
- 开发者可以通过CAShapeLayer来实现复杂的形状绘制和路径动画效果。
-
Core Animation
是常用的框架之一。它比 UIKit 和 AppKit 更底层。正如我们所知,UIView底下封装了一层CALayer树,Core Animation 层是真正的渲染层,我们之所以能在屏幕上看到内容,真正的渲染工作是在 Core Animation 层进行的。 - Core Animation 是一套Objective-C API,实现了一个高性能的复合引擎,并提供一个简单易用的编程接口,给用户UI添加平滑运动和动态反馈能力。
- Core Animation 是 UIKit 实现动画和变换的基础,也负责视图的复合功能。使用Core Animation可以实现定制动画和细粒度的动画控制,创建复杂的、支持动画和变换的layered 2D视图
- OpenGL ES的内容也可以与Core Animation内容进行集成。
- 为了使用Core Animation实现动画,可以修改 层的属性值 来触发一个action对象的执行,不同的action对象实现不同的动画。Core Animation 提供了一组基类及子类,提供对不同动画类型的支持:
- CAAnimation 是一个抽象公共基类,CAAnimation采用CAMediaTiming 和CAAction协议为动画提供时间(如周期、速度、重复次数等)和action行为(启动、停止等)。
- CAPropertyAnimation 是 CAAnimation的抽象子类,为动画提供一个由一个key路径规定的层属性的支持;
- CABasicAnimation 是CAPropertyAnimation的具体子类,为一个层属性提供简单插入能力。
- CAKeyframeAnimation 也是CAPropertyAnimation的具体子类,提供key帧动画支持。
四、 Core Graphics & Quartz 2D
1. 主要特点和功能
Core Graphics(Quartz 2D)是iOS和macOS平台上的2D绘图引擎,它是一套C-based API,用于实现图形绘制
、图像处理
和文本渲染
等功能。
以下是Core Graphics & Quartz 2D的详细介绍:
-
- 绘图功能:
- 绘图上下文:
- Core Graphics使用绘图上下文(Graphics Context)来进行绘制操作,可以是基于位图的上下文、PDF上下文等。
- Core Graphics提供了丰富的绘图功能,包括:
- 绘制形状(直线、矩形、椭圆等)
- 绘制路径(自定义路径、曲线路径等)
- 绘制图像(位图图像、矢量图像等)
- ...
- 开发者可以在不同类型的上下文中进行绘制操作,创建自定义的绘图效果,实现各种绘图需求
-
- 图像处理:
- Core Graphics支持图像的
加载
、绘制
、裁剪
、变换
、合成
、渲染
等操作 实现图像的特效和优化
-
- 文本渲染:
- Core Graphics提供了文本渲染功能,可以渲染
文本内容
,可以控制文本样式
、排版布局
等实现文本的自定义渲染。
-
- 图形上下文:
- Core Graphics使用图形上下文(Graphics Context)来管理绘图环境的状态和属性。
- 通过设置图形上下文的属性来控制绘图效果,如颜色、线宽、填充模式等。
-
- 颜色和渲染:
- Core Graphics支持颜色管理和渲染操作,可以设置
填充颜色
、描边颜色
、渐变色
等。
-
- 图形变换:
- Core Graphics提供了图形变换的功能,可以实现
平移
、旋转
、缩放
等变换操作。
-
- 坐标系:
- Core Graphics使用笛卡尔坐标系,原点在左下角,x轴向右延伸,y轴向上延伸。
- 开发者可以通过坐标变换来实现坐标系的转换和调整,实现复杂的绘图效果和布局。
-
- 性能优化:
- Core Graphics利用GPU硬件加速来实现图形渲染和处理,提高绘图效率和性能。
- 开发者可以通过合理使用Core Graphics的API和功能来优化绘图性能,实现流畅的图形绘制和处理
-
- 其它
-
Core Graphics
(使用Quartz 2D引擎)- 当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,
运行时实时计算
、绘制一系列图像帧
来实现动画。 - 与之相对的是运行前创建图像(例如从磁盘中或内存中已经创建好的UIImage图像)
- 当开发者需要在运行时创建图像时,可以使用 Core Graphics 去绘制,
-
Quartz 2D
- Quartz 2D是Core Graphics中的2D 绘制呈现引擎。
- Quartz 2D能够与所有的图形和动画技术(如
Core Animation
,OpenGL ES
, 和UIKit
等)一起使用。Quartz 2D采用paint模式进行绘制。
2. 核心类介绍
Core Graphics(Quartz 2D)框架中的核心类主要包括以下几个:
-
- CGContext:
- CGContext是Core Graphics中的
绘图上下文
,用于执行绘图操作和渲染图形。 - CGContext提供了绘制路径、图像、文本等的功能,是实现图形绘制的核心类。
-
- CGPath:
- CGPath是Core Graphics中
表示路径
的类,用于创建和管理路径对象。 - CGPath可以包含
直线
、曲线
、矩形
、椭圆
等形状,用于定义绘制的轮廓和形状。 - CGPath能够被填充和stroke
-
- CGImage:
- CGImage是Core Graphics中
表示图像
的类,用于加载、创建和处理位图图像。 - CGImage可以从文件、数据或其他来源创建,用于图像的绘制和处理。
-
- CGColor:
- CGColor是Core Graphics中
表示颜色
的类,用于定义绘制和填充的颜色。 - CGColor可以表示RGB、RGBA、灰度等颜色空间,用于设置绘制和填充的颜色值。
-
CGColor
和CGColorSpace
;用来进行颜色和颜色空间管理;
-
- CGGradient:
- CGGradient是Core Graphics中
表示渐变
的类,用于创建和管理颜色渐变效果。 - CGGradient可以
定义线性渐变
、径向渐变
等效果,用于实现丰富的颜色渐变效果。 -
CGShading
和CGGradient
:用于绘制剃度;
-
- CTFont:
- CTFont是Core Text框架中
表示字体
的类,用于处理文本的字体和排版。 - CTFont可以设置字体的
样式
、大小
、粗细
等属性,用于文本的渲染和显示。
- 其他常用类:
-
CGLayer
:用来表示一个能够用于重复绘制和offscreen绘制的绘制层; -
CGPattern
:用来表示Pattern,用于重复绘制; -
CGPDFContentStream
、CGPDFScanner
、CGPDFPage
、CGPDFObject
,CGPDFStream
,CGPDFString
等用来进行pdf文件的创建、解析和显示
-
五、Core Image
Core Image是苹果提供的图像处理框架,主要用于实现图像处理
、滤镜应用
和图像分析
等功能。以下是Core Image的核心要点:
1. 主要特点和功能
-
- 滤镜(Filter):
- Core Image提供了丰富的滤镜效果,如
模糊
、锐化
、色彩调整
、边缘检测
等。 - 开发者可以通过Core Image的滤镜功能对图像进行处理和增强。
- iOS 8 之后 引入 CIFilter,Core Image从此支持自定义滤镜的创建和应用,实现个性化的图像处理效果。
-
- 图像处理链(Image Processing Pipeline):
- Core Image使用图像处理链来处理图像,包括
输入图像
、滤镜效果
、和输出图像
。 - 开发者可以构建自定义的图像处理链,实现复杂的图像处理流程。
-
- 图像分析(Image Analysis):
- Core Image支持图像分析功能,如
人脸检测
、特征识别
、颜色识别
、物体识别
等。 - 开发者可以利用Core Image进行图像分析,
提取图像中的信息和特征
。
-
- Metal性能优化:
- Core Image可以与Metal框架结合,利用GPU硬件加速来提高图像处理的性能。
- 开发者可以通过Metal框架优化Core Image的性能,实现高效的图像处理和滤镜效果。
-
- 图像格式转换(Image Format Conversion):
- Core Image支持图像格式的转换和处理,如
颜色空间转换
、像素格式转换
等。 - 开发者可以使用Core Image进行图像格式的转换和处理,满足不同的图像处理需求。
-
- 实时预览(Real-time Preview):
- Core Image提供实时预览功能,可以在应用程序中实时显示滤镜效果的预览。
- 开发者可以通过Core Image实现实时的滤镜预览,方便用户调整和查看效果。
-
- 其它
-
Core Image
与 Core Graphics 恰恰相反- Core Graphics 用于在运行时创建图像
- 而 Core Image 是用来处理已经创建的图像的。
-
Core Image
是 iOS5 新加入到 iOS 平台的一个图像处理框架,提供了强大高效的图像处理功能, 用来对基于像素的图像进行操作与分析, 内置了很多强大的滤镜(Filter) (目前数量超过了180种), 这些Filter 提供了各种各样的效果, 并且还可以通过 滤镜链 将各种效果的 Filter叠加 起来形成强大的自定义效果。- iOS8 之后更是支持自定义 CIFilter,可以定制满足业务需求的复杂效果。
-
Core Image
的优点在于十分高效。大部分情况下,它会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。如果设备支持 Metal,那么会使用 Metal 处理。这些操作会在底层完成,Apple 的工程师们已经帮助开发者们完成这些操作了。- 例如他可以根据需求选择 CPU 或者 GPU 来处理。
// 创建基于 CPU 的 CIContext 对象 (默认是基于 GPU,CPU 需要额外设置参数) context = [CIContext contextWithOptions: [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:kCIContextUseSoftwareRenderer]]; // 创建基于 GPU 的 CIContext 对象 context = [CIContext contextWithOptions: nil]; // 创建基于 GPU 的 CIContext 对象 EAGLContext *eaglctx = [[EAGLContext alloc] initWithAPI:kEAGLRenderingAPIOpenGLES2]; context = [CIContext contextWithEAGLContext:eaglctx]; ```
2. 核心类介绍
-
- CIImage:
- CIImage是Core Image框架中表示图像数据的类,可以用来表示图像数据源
- CIImage可以从各种来源创建,如UIImage、CGImage、NSData、图像文件或者像素数据等,用于输入到Core Image的滤镜中进行处理。
-
- CIFilter:
- CIFilter是Core Image框架中的滤镜类,用于实现各种图像处理和滤镜效果。
- CIFilter包含了各种内置的滤镜效果,也可以自定义滤镜来实现特定的图像处理需求。
- 这个框架中对图片属性进行细节处理的类。它对所有的像素进行操作,用一些键-值设置来决定具体操作的程度。
-
- CIContext:
- CIContext是Core Image框架中的上下文类,用于管理图像处理的环境和输出。
- CIContext可以指定渲染目标(如屏幕、图像文件)、渲染选项(如颜色空间、缩放比例)等参数。
-
- CIFeature:
- CIFeature是Core Image框架中的特征类,用于检测图像中的特征和对象。
- CIFeature可以用于人脸检测、文本识别、条形码识别等应用场景。
-
- CIColor:
- CIColor是Core Image框架中的颜色类,用于表示颜色信息。
- CIColor可以用来创建颜色对象,设置滤镜效果中的颜色参数。
-
- CIVector:
- CIVector是Core Image框架中的向量类,用于表示多维向量数据。
- CIVector可以用来设置滤镜效果中的向量参数,如位置、大小等。
-
Core Image 的 API 主要就是三类:
- CIContext 表示上下文,如 Core Graphics 以及 Core Data 中的上下文用于处理绘制渲染以及处理托管对象一样,Core Image 的上下文也是实现对图像处理的具体对象。可以从其中取得图片的信息。
六、Metal
1. 主要特点和功能
Metal是苹果公司推出的图形和计算框架,主要用于实现高性能的图形渲染和通用计算任务。以下是Metal框架的主要特点和功能:
-
- 低级别接口:
- Metal提供了
直接访问GPU的低级别接口
,相比OpenGL和OpenCL等传统图形和计算框架更加高效。 - 开发者可以更加精细地控制图形渲染和计算流程,实现更高性能的图形处理和计算任务。
-
- 高性能图形渲染:
- Metal通过利用GPU的并行处理能力,实现高性能的图形渲染和效果处理。
- Metal支持现代图形渲染技术,如
着色器编程
、纹理映射
、光照效果
、渲染三维场景
等,可以实现复杂的图形渲染效果。
-
- 通用计算:
- Metal
不仅用于图形渲染
,还可以用于通用计算任务,如机器学习
、图像处理
、物理模拟
等。 - Metal提供了
计算着色器和计算管线
,支持GPU加速的通用计算,提高计算任务的效率和速度。
-
- 着色器编程:
- Metal使用着色器(Shader)来控制图形渲染和计算过程。
- 开发者可以使用Metal Shading Language(MSL)
编写着色器代码
,实现定制化的图形效果
和计算逻辑
。
-
- 命令缓冲区:
- Metal使用命令缓冲区(Command Buffer)来管理GPU命令的提交和执行。
- 开发者可以创建和提交命令缓冲区,控制GPU的操作流程,实现高效的图形渲染和计算任务。
-
- 资源管理:
- Metal提供了资源管理功能,用于管理GPU资源如缓冲区、纹理等。
- 开发者可以创建和管理各种GPU资源,优化资源的使用和共享,提高图形渲染和计算的效率。
-
- 多线程并发:
- Metal框架支持多线程并发,可以在多个线程上同时执行图形渲染和计算任务。
- 开发者可以利用Metal的多线程特性实现并行处理,提高应用程序的性能和响应速度。
-
- 内存管理:
- Metal提供了高效的内存管理机制,可以管理GPU资源和缓冲区的分配和释放。
- Metal的内存管理功能可以帮助开发者优化内存使用,避免内存泄漏和性能下降。
-
- 跨平台支持:
- Metal框架不仅支持iOS和macOS平台,还可以在tvOS上使用,实现跨平台的图形渲染和计算功能。
- Metal提供了一致的API和功能,方便开发者在不同平台上实现统一的图形处理和计算任务
2. 核心类介绍
以下是Metal框架中一些核心类的介绍:
-
- MTLDevice:
- MTLDevice是Metal框架中表示GPU设备的类,用于创建Metal对象和管理GPU资源。
- 开发者可以通过MTLDevice创建其他Metal对象,如缓冲区、纹理等。
-
- MTLCommandQueue:
- MTLCommandQueue是Metal框架中的命令队列类,用于提交和执行GPU命令。
- 开发者可以将命令添加到MTLCommandQueue中,然后由GPU执行这些命令。
-
- MTLRenderPipelineState:
- MTLRenderPipelineState是Metal框架中的渲染管线状态类,用于配置图形渲染管线。
- 开发者可以通过MTLRenderPipelineState设置渲染管线的各个阶段,如顶点着色器、片元着色器等。
-
- MTLBuffer:
- MTLBuffer是Metal框架中表示缓冲区的类,用于存储数据和传递给GPU。
- 开发者可以创建MTLBuffer对象来传递顶点数据、纹理数据等到GPU进行处理。
-
- MTLTexture:
- MTLTexture是Metal框架中表示纹理的类,用于存储和处理图像数据。
- 开发者可以创建MTLTexture对象来加载纹理数据,用于图像处理和渲染。
-
- MTLComputePipelineState:
- MTLComputePipelineState是Metal框架中的计算管线状态类,用于配置计算任务。
- 开发者可以通过MTLComputePipelineState设置计算任务的各个阶段,如计算内核函数等。
- Metal框架提供了直接访问GPU的能力,可以实现高性能的图形渲染和计算任务。通过这些核心类,开发者可以利用Metal框架实现复杂的图形渲染、计算任务和图形效果,为应用程序提供更加流畅和高性能的图形处理能力。
七、GPUImage
1. 主要特点和功能
GPUImage是一个基于OpenGL ES 2.0的开源图像处理框架,支持GPU加速,用于实现图像和视频处理的功能 以下是GPUImage框架的主要特点和功能:
-
- 滤镜(Filters):
- GPUImage提供了大量的滤镜效果,如
色彩调整
、模糊
、锐化
、边缘检测
等。 - 开发者可以通过GPUImage提供的滤镜效果对
图像
和视频
进行实时处理
。 - 定制滤镜(Custom Filters):开发者可以通过编写自定义滤镜来扩展GPUImage的功能和效果,通过组合各种图像处理操作来创建个性化的滤镜效果,满足特定的处理需求。
-
- 处理链(Processing Chain):
- GPUImage支持链式处理图像和视频数据,可以将多个滤镜效果串联起来,实现复杂的处理流程,同时保持高效的处理速度
- 每个处理步骤都可以添加不同的滤镜效果。
- 开发者可以自定义处理链,实现复杂的图像处理流程。
-
- 实时相机(Camera):
- GPUImage提供了相机类,可以直接从摄像头捕获实时图像数据、视频流。
- 开发者可以利用GPUImage的相机类实现实时滤镜效果和实时图像处理。
-
- 纹理(Textures):
- GPUImage使用OpenGL纹理来处理图像数据,提高处理效率和性能。
- 开发者可以利用GPUImage的纹理处理功能实现高效的图像处理和渲染。
-
- 实时处理(Real-time Processing):
- GPUImage支持实时处理图像和视频流,可以在
实时视频流中应用滤镜效果
,实现实时的图像处理和增强
。 - 开发者可以利用GPUImage实现实时的图像处理效果,为应用程序增加更多的视觉吸引力
-
- 跨平台支持:
- GPUImage不仅支持iOS平台,还支持Android平台和桌面平台,可以实现跨平台的图像处理应用开发。
-
- 易用性:
- GPUImage提供了简洁易用的API和文档,使开发者能够快速上手并实现各种图像处理需求,降低开发成本和复杂度。
-
- GPU加速:
- GPUImage利用GPU的并行计算能力来加速图像处理和滤镜效果的计算,提高处理速度和效率。
2. 核心类和协议介绍
核心类:
-
- GPUImageOutput:
- GPUImageOutput是所有输出类的基类,用于处理图像数据的输出。
- 其子类包括GPUImageFilter、GPUImagePicture等,用于实现不同的图像处理功能。
-
- GPUImageFilter:
- GPUImageFilter是图像滤镜的基类,用于实现各种图像处理滤镜效果。
- 开发者可以通过GPUImageFilter及其子类来应用各种滤镜效果,如模糊、锐化、色彩调整等。
-
- GPUImagePicture:
- GPUImagePicture用于将静态图像加载到GPU中进行处理。
- 可以通过GPUImagePicture加载UIImage或CGImage等静态图像数据,并传递给滤镜进行处理。
-
- GPUImageFramebuffer:
- GPUImageFramebuffer用于在GPU中存储图像数据的帧缓冲区。
- GPUImage处理图像数据时会使用帧缓冲区来存储中间结果,提高处理效率。
-
- GPUImageMovieWriter:
- GPUImageMovieWriter是用于将视频数据写入文件的类。
- GPUImageMovieWriter实现了GPUImageInput和GPUImageTextureOutput协议,可以接收图像数据并将处理后的视频数据写入文件。
协议:
-
- GPUImageInput:
- GPUImageInput是输入类的协议,定义了接收图像数据的方法。
- 所有需要接收图像数据的类都需要实现GPUImageInput协议,以便接收和处理图像数据。
-
- GPUImageTextureOutput:
- GPUImageTextureOutput是输出纹理的协议,用于输出处理后的纹理数据。
- 实现了GPUImageTextureOutput协议的类可以将处理后的纹理数据输出到其他地方,如屏幕、纹理等。
八、OpenGL ES
1. 主要特点和功能
OpenGL ES(OpenGL for Embedded Systems)是一种用于嵌入式系统的轻量级版本的OpenGL图形库,常用于移动设备和嵌入式系统中的图形渲染。以下是OpenGL ES的核心要点:
-
- 跨平台性:
- OpenGL ES是跨平台的图形API,可以在多种移动设备和嵌入式系统上使用,如iOS、Android、嵌入式Linux等。
-
- 硬件加速:
- OpenGL ES利用GPU硬件加速进行图形渲染,提供高性能的图形处理能力,适合移动设备和嵌入式系统的资源限制环境。
-
- 上下文管理:
- OpenGL ES使用上下文(Context)来管理图形状态和资源,包括
着色器程序
、纹理
、缓冲区
等。 - 开发者需要创建和管理OpenGL ES上下文,以便进行图形渲染和操作。
-
- 图形渲染:
- OpenGL ES支持各种图形渲染功能,包括
顶点处理
、片元处理
、纹理映射
、着色器编程
等,可以实现各种复杂的图形效果。 - 着色器编程:
- OpenGL ES使用着色器(Shader)来定义图形渲染的计算过程,包括
顶点着色器
和片元着色器
。 - 开发者可以编写
自定义的着色器程序
,用于实现各种图形效果和渲染算法。
- OpenGL ES使用着色器(Shader)来定义图形渲染的计算过程,包括
- 纹理映射:
- OpenGL ES支持纹理映射(Texture Mapping),用于将纹理映射到几何图形表面,实现丰富的纹理效果和贴图功能。
- 开发者可以加载纹理数据并将其映射到几何图形上,实现丰富的纹理效果。
-
- 缓冲区对象:
- OpenGL ES使用缓冲区对象(Buffer Object),包括
顶点缓冲区
、索引缓冲区
等来存储顶点数据
、索引数据
等。 - 开发者可以创建和管理缓冲区对象,
存储和传递
图形数据,提供给GPU进行图形渲染
-
- 深度测试和混合:
- OpenGL ES支持深度测试和混合功能,可以实现
透明效果
、深度排序
等,提高图形渲染的真实感和逼真度。 - 多重采样抗锯齿:
- OpenGL ES支持多重采样抗锯齿(MSAA),可以减少图形渲染中的锯齿现象,提高图形质量。
-
- 光照和材质:
- OpenGL ES支持光照和材质(Lighting and Material),用于
模拟光照
效果和物体表面的材质特性
。 - 开发者可以通过设置光照参数和材质属性来实现逼真的光照效果。
-
- 渲染管线:
- OpenGL ES使用渲染管线(Rendering Pipeline)来处理图形渲染过程,包括顶点处理、几何处理、光栅化等阶段。
- 开发者可以配置渲染管线的各个阶段,以实现定制的图形渲染效果。
2. 核心类介绍
-
- GLKView:
- GLKView是OpenGL ES框架中用于显示OpenGL内容的视图类。
- GLKView提供了一个方便的方式来在iOS应用中显示OpenGL渲染的内容。
-
- GLKViewController:
- GLKViewController是OpenGL ES框架中的视图控制器类,用于管理OpenGL渲染和视图的交互。
- GLKViewController可以处理渲染循环、用户交互等任务,简化OpenGL ES应用程序的开发。
-
- GLKBaseEffect:
- GLKBaseEffect是OpenGL ES框架中的基本效果类,用于实现简单的渲染效果。
- GLKBaseEffect提供了一些基本的渲染效果,如颜色、光照、纹理等。
-
- GLKTextureLoader:
- GLKTextureLoader是OpenGL ES框架中的纹理加载器类,用于加载和管理纹理数据。
- GLKTextureLoader可以从文件、内存等来源加载纹理数据,方便在OpenGL ES应用中使用纹理。
-
- GLKMath:
- GLKMath是OpenGL ES框架中的数学库,用于处理矩阵、向量等数学运算。
- GLKMath提供了一些常用的数学函数和数据结构,方便在OpenGL ES应用中进行数学计算。
九、ARKit和Metal框架
1. ARKit
1.1 主要特点和功能
ARKit是苹果公司提供的增强现实(AR)框架,用于在iOS设备上实现增强现实体验。以下是ARKit的主要特点和功能:
-
- 环境感知:
- ARKit可以通过
摄像头
和传感器
感知设备周围的环境,包括平面
、垂直面
、光照
、特征点
等。 - ARKit提供了环境感知的功能,用于识别和追踪现实世界中的物体和场景。
-
- 跟踪技术:
- ARKit支持视觉惯性里程计(Visual-Inertial Odometry)和基于特征点的跟踪技术。
- ARKit可以实时追踪设备的
位置
和方向
,实现精确的虚拟物体叠加在现实世界中。
-
- 虚拟内容叠加:
- ARKit可以将虚拟内容叠加到设备摄像头捕捉的现实世界中,实现虚拟物体的
显示
和交互
-
- 场景理解:
- ARKit可以理解设备所处的场景,包括平面检测、光照估计等。
- ARKit可以检测水平和垂直平面,识别光照条件,为虚拟物体的渲染提供更真实的效果。
-
- 用户交互:
- ARKit支持用户交互功能,可以通过
手势
、触摸
、点击事件
等方式与虚拟内容进行交互,如移动
、旋转
、缩放
等操作。 - 用户可以通过手势操作和触摸事件与增强现实场景进行交互,实现更加沉浸式的体验。
-
- 平面检测:
- ARKit支持平面检测功能,可以识别
水平面
和垂直面
,用于在现实世界中放置虚拟物体。
-
- 光照估计:
- ARKit提供光照估计功能,可以根据环境光照条件调整虚拟内容的
亮度
和阴影
,使其与现实世界更加融合。
-
- 持久性体验:
- ARKit支持持久性体验,可以
保存
和恢复
虚拟内容在现实世界中的位置。 - 用户可以在不同时间点和场景中保持一致的增强现实体验,提升用户体验的连贯性。
-
- 面部追踪:
- ARKit提供面部追踪功能,可以识别和追踪用户的面部表情,用于实现
面部滤镜
、AR表情
等效果。
-
- 多设备协同:
- ARKit支持多设备协同功能,可以实现多个设备之间的AR体验同步,如
共享虚拟场景
、多人协作
等。
1.2 核心类介绍
ARKit框架中一些核心类的介绍:
-
- ARSession:
- ARSession是ARKit框架中的会话类,用于管理AR体验的整个过程。
- ARSession负责追踪设备的位置和方向,识别环境中的特征点,以及处理AR场景的渲染和更新。
-
- ARConfiguration:
- ARConfiguration是ARKit框架中的配置类,用于配置AR会话的参数和设置。
- ARConfiguration包括不同类型的配置,如ARWorldTrackingConfiguration、ARFaceTrackingConfiguration等,用于不同类型的AR体验。
-
- ARAnchor:
- ARAnchor是ARKit框架中的锚点类,用于表示在AR场景中的位置和方向。
- ARAnchor可以用于标记特定的位置或对象,以便在AR场景中进行定位和交互。
-
- ARSCNView:
- ARSCNView是ARKit框架中的场景视图类,用于显示AR场景和渲染3D内容。
- ARSCNView结合了SceneKit框架,可以方便地在AR场景中添加3D模型、动画等内容。
-
- ARPlaneAnchor:
- ARPlaneAnchor是ARKit框架中的平面锚点类,用于表示检测到的水平平面。
- ARPlaneAnchor可以用于在AR场景中识别和跟踪水平表面,如地面、桌面等。
-
- ARHitTestResult:
- ARHitTestResult是ARKit框架中的命中测试结果类,用于检测AR场景中的交互命中结果。
- ARHitTestResult可以用于检测用户在AR场景中的点击、触摸等交互操作。
通过这些核心类,开发者可以利用ARKit框架实现各种增强现实体验,包括追踪设备位置
、识别环境特征
、渲染3D内容
等功能,为用户提供沉浸式的增强现实体验。
2. VR = ARKit+Metal 核心要点:
结合ARKit和Metal实现VR(Virtual Reality)体验的核心要点包括以下几个方面:
-
- ARKit中的空间追踪:
- 使用ARKit进行空间追踪,可以实现设备在现实世界中的定位和追踪,为VR体验提供基础支持。
- ARKit可以识别环境中的特征点、平面和物体,用于定位虚拟内容的位置和方向。
-
- Metal中的图形渲染:
- 利用Metal进行高性能的图形渲染,可以实现流畅的虚拟现实场景呈现。
- Metal提供了直接访问GPU的能力,可以实现复杂的图形渲染和计算任务,为VR体验提供高性能的图形处理支持。
-
- ARKit和Metal的集成:
- 将ARKit和Metal结合起来,可以实现在AR场景中渲染虚拟内容,与现实世界进行交互。
- 通过ARKit提供的空间追踪功能,将虚拟内容与现实世界对齐,再利用Metal进行图形渲染,实现沉浸式的VR体验。
-
- 交互和控制:
- 在ARKit+Metal的VR体验中,可以通过手势识别、头部追踪等技术实现用户的交互和控制。
- 用户可以通过手势、头部运动等方式与虚拟内容进行交互,增强VR体验的沉浸感和互动性。
-
- 性能优化:
- 在实现ARKit+Metal的VR体验时,需要考虑性能优化,包括减少渲染延迟、优化图形质量、提高帧率等方面。
- 通过合理的性能优化措施,可以确保VR体验的流畅性和稳定性。
3. 其它
本文主要针对官方自带的图形界面处理框架和比较流行的GPUImage框架进行简单介绍。暂不对AR、VR等相关技术展开更多讨论
十、Open CV、SpriteKit、Sence Kit
在iOS开发中,使用SpriteKit框架、Sence Kit框架 可以进行 2D、3D游戏开发。使用OpenCV可以进行计算机视觉处理开发。
1. Open CV
1.1 主要特点和功能
OpenCV(Open Source Computer Vision Library)是一个开源的计算机视觉库,提供了丰富的图像处理和计算机视觉算法。通过OpenCV提供的丰富功能和算法,开发者可以实现各种计算机视觉任务,包括图像处理
、特征检测
、目标检测
、机器学习
、特征提取
、视频处理
等,为图像处理和计算机视觉应用提供强大支持。
以下是OpenCV的核心要点:
-
- 图像处理:
- OpenCV提供了各种图像处理功能,包括
图像滤波
、边缘检测
、图像变换
、颜色空间转换
等。 - 开发者可以利用OpenCV对图像进行处理和增强,实现各种图像处理需求。
-
- 特征检测和描述:
- OpenCV包括
多种特征检测和描述算法
,如SIFT
、SURF
、ORB
等。 - 这些算法可以用于检测图像中的
关键点
、描述特征
、匹配特征点
等任务。
-
- 目标检测和跟踪:
- OpenCV提供了
目标检测
和跟踪
的功能,如Haar级联检测器
、HOG检测器
等。 - 这些功能可以用于在图像或视频中检测和跟踪目标对象。
-
- 机器学习:
- OpenCV集成了机器学习算法库,包括支持
向量机(SVM)
、K均值聚类
、决策树
等。 - 开发者可以利用OpenCV进行
机器学习任务
,如分类
、聚类
、回归
等。
-
- 摄像头标定和几何变换:
- OpenCV提供了
摄像头标定
、透视变换
、仿射变换
等几何变换功能。 - 这些功能可以用于校正
图像畸变
、实现图像配准
、透视变换
等任务。
-
- 图像分割和轮廓检测:
- OpenCV包括图像分割和轮廓检测算法,如
分水岭算法
、边缘检测
等。 - 这些算法可以用于
图像分割
、对象检测
、轮廓提取
等应用。
1.2 核心类介绍
-
- Mat:
- Mat是OpenCV中表示图像和矩阵数据的类,是最常用的数据结构之一。
- Mat类可以存储图像数据、矩阵数据等,支持各种图像处理和计算操作。
-
- CascadeClassifier:
- CascadeClassifier是OpenCV中的级联分类器类,用于对象检测和识别。
- CascadeClassifier可以加载Haar级联分类器模型,用于人脸检测、目标检测等任务。
-
- VideoCapture:
- VideoCapture是OpenCV中的视频捕获类,用于从摄像头或视频文件中捕获图像帧。
- VideoCapture类可以实时捕获视频流,用于实时图像处理和分析。
-
- FeatureDetector:
- FeatureDetector是OpenCV中的特征检测器类,用于检测图像中的特征点。
- FeatureDetector可以检测关键点、角点、边缘等特征点,用于图像匹配和特征提取。
-
- DescriptorExtractor:
- DescriptorExtractor是OpenCV中的描述子提取器类,用于提取特征点的描述子。
- DescriptorExtractor可以计算特征点的描述子向量,用于特征匹配和识别。
-
- Imgproc:
- Imgproc是OpenCV中的图像处理模块,提供了丰富的图像处理函数和算法。
- Imgproc包括图像滤波、边缘检测、形态学操作、颜色空间转换等功能。
-
- HighGUI:
- HighGUI是OpenCV中的图形用户界面模块,用于显示图像、视频和交互操作。
- HighGUI提供了图像显示、窗口管理、鼠标事件处理等功能,方便图像处理的可视化展示。
1.3 其他
OpenCV 的 API 是 C++ 的。它由不同的模块组成,这些模块中包含范围极为广泛的各种方法,从底层的图像颜色空间转换到高层的机器学习工具。
这里提供一个入门PDF文档 下载入口。
- 使用 C++ API 并不是绝大多数 iOS 开发者每天都做的事,你需要使用 Objective-C++ 文件来调用 OpenCV 的函数。
- 也就是说,你不能在 Swift 或者 Objective-C 语言内调用 OpenCV 的函数。 这篇 OpenCV 的 iOS 教程告诉你只要把所有用到 OpenCV 的类的文件后缀名改为
.mm
就行了,包括视图控制器类也是如此。 - 这么干或许能行得通,却不是什么好主意。正确的方式是给所有你要在 app 中使用到的 OpenCV 功能写一层 Objective-C++ 封装。
- 这些 Objective-C++ 封装把 OpenCV 的 C++ API 转化为安全的 Objective-C API,以方便地在所有 Objective-C 类中使用。
- 走封装的路子,你的工程中就可以只在这些封装中调用 C++ 代码,从而避免掉很多让人头痛的问题,比如直接改文件后缀名会因为在错误的文件中引用了一个 C++ 头文件而产生难以追踪的编译错误。
OpenCV 声明了命名空间 cv
,因此 OpenCV 的类的前面会有个 cv::
前缀,就像 cv::Mat
、 cv::Algorithm
等等。
- 你也可以在
.mm
文件中使用using namespace cv
来避免在一堆类名前使用cv::
前缀。 - 但是,在某些类名前你必须使用命名空间前缀,比如
cv::Rect
和cv::Point
,因为它们会跟定义在MacTypes.h
中的Rect
和Point
相冲突。尽管这只是个人偏好问题,我还是偏向在任何地方都使用cv::
以保持一致性。
一般讲的OpenCV是基于CPU的,相关资料和支持也是最完善的。当然,也有基于GPU模块,但提供的接口非常坑爹:
-
相当一部分不支持浮点类型(像histogram、integral这类常用的都不支持)
-
又如,遇到阈值判断的地方,就必须传回cpu处理,因为gpu函数都是并行处理的,每改写完一个算法模块,就测试一下运行效率,有的时候是振奋人心,有的时候则是当头棒喝——比CPU还慢。详情可参阅 这里。
2. SpriteKit、Sence Kit
对于寻找游戏引擎的开发者来说,Metal 不是最佳选择。苹果官方的是更好的选择:
- Scene Kit (3D)
- Sprite Kit (2D)
- 这些 API 提供了包括物理模拟在内的更高级别的游戏引擎。
另外还有功能更全面的 3D 引擎,例如 :
- Epic 的 Unreal Engine
- 或 Unity
- 二者都是跨平台的。使用这些引擎,你无需直接使用 Metal 的 API,就可以从 Metal 中获益。
2.1 2D渲染 -- SpriteKit
SpriteKit 让开发者可以开发高性能、省电节能的 2D 游戏。在 iOS 8 中,我们新添了多项增强功能,这将使 2D 游戏体验更加精彩。这些新技术有助于使游戏角色的动作更加自然,并让开发者可以更轻松地在游戏中加入力场、检测碰撞和生成新的灯光效果。
2.2 3D渲染 -- SceneKit
SceneKit 专为休闲 3D 游戏而设计,可让开发者渲染 3D 游戏场景。SceneKit 内置了物理引擎、粒子发生器和各种易用工具,可以轻松快捷地为 3D 物体编写动作。不仅如此,它还与 SpriteKit 完全集成,所以开发者可以直接在 3D 游戏中加入 SpriteKit 的素材。
3. 其它
本文主要针对官方自带的图形界面处理框架和比较流行的GPUImage框架进行简单介绍。暂不对游戏、计算机视觉等相关技术展开更多讨论
总结
本文主要针对官方自带的图形界面处理框架和比较流行视频图层处理框架进行简单介绍:
- 系统自带框架
- UIKit
- Core Graphics
- Core Image
- Core Animation
- Metal
- 流行的第三方框架
- GPUImage
- OpenGL ES
后面会有几篇文章针对这几大块,进行更详细的介绍
本文尚未讨论
- ARKit和Metal框架进行 XR开发(AR、VR)
- Open CV 计算机视觉开发
- SpriteKit、Sence Kit 游戏开发 若后面有时间,会进行简单的了解,给自己一个知识储备
系列文章
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】
Swiftlint 介绍
安装Fastlane
使用 Fastlane 上传 App 到蒲公英
Fastlane 是一款为 iOS 和 Android 开发者提供的自动化构建工具,它可以帮助开发者将 App 打包、签名、测试、发布、信息整理、提交 App Store 等工作完整的连接起来,实现完全自动化的工作流,如果使用得当,可以显著的提高开发者的开发效率。
为了让使用 Fastlane 的用户,可以将 Fastlane 的自动化工作流与蒲公英结合起来,我们为大家提供了 Fastlane 的蒲公英插件。该插件使用起来非常简单,安装该插件后,由 Fastlane 打包生成的 App 可以自动上传到蒲公英。
前置条件
- 开发者使用的是 mac 或 linux 系统
- 已经安装好 ruby、rubygems、bundler
- 已经安装了 Fastlane。如果没有安装,请参考:官方安装指南
- 开发者了解基本的 App 开发流程、终端的使用方法
- 本文使用环境为:macOS 10.13, Xcode 9.2, Fastlane 2.78.0, fastlane-plugin-pgyer 0.2.1
配置SwiftLint的lane
设置工程的Fastlane配置
给项目添加SwiftLint
- 安装Fastlane
- 配置Fastlane的Fastfile
- 添加一个lane
# This file contains the fastlane.tools configuration # You can find the documentation at https://docs.fastlane.tools # # For a list of all available actions, check out # # https://docs.fastlane.tools/actions # # For a list of all available plugins, check out # # https://docs.fastlane.tools/plugins/available-plugins # # Uncomment the line if you want fastlane to automatically update itself # update_fastlane default_platform(:ios) platform :ios do desc "Description of what the lane does" lane :lint do swiftlint( mode: 'lint', # 运行 SwiftLint 的模式 config_file: 'swiftlint.yml', # SwiftLint 配置文件的路径(可选) ignore_exit_status: true # 忽略 SwiftLint 的退出状态,以允许 Fastlane 继续执行 ) end end
- 在工程主目录添加swiftlint文件:(swiftlint.yml)
- 可以在github上搜索、参考:一些大公司的Swiftlint规则设置,进行编写
- 也可以直接使用以下的配置:
# Enabled Rules opt_in_rules: - anyobject_protocol - array_init - attributes - block_based_kvo - capture_variable - class_delegate_protocol - closing_brace - closure_body_length - closure_end_indentation - closure_parameter_position - closure_spacing - collection_alignment - colon - comma - compiler_protocol_init - contains_over_filter_count - contains_over_filter_is_empty - contains_over_first_not_nil - contains_over_range_nil_comparison - control_statement - custom_rules - deployment_target - discarded_notification_center_observer - discouraged_direct_init - discouraged_none_name - discouraged_object_literal - duplicate_enum_cases - duplicate_imports - duplicated_key_in_dictionary_literal - dynamic_inline - empty_collection_literal - empty_count - empty_enum_arguments - empty_parameters - empty_parentheses_with_trailing_closure - empty_string - empty_xctest_method - enum_case_associated_values_count - expiring_todo - explicit_init - fallthrough - fatal_error_message - file_header - file_length - first_where - flatmap_over_map_reduce - for_where - force_cast - force_try - function_body_length - function_parameter_count - generic_type_name - ibinspectable_in_extension - identical_operands - identifier_name - implicit_getter - implicit_return - implicitly_unwrapped_optional - indentation_width - inert_defer - is_disjoint - joined_default_parameter - large_tuple - last_where - leading_whitespace - legacy_cggeometry_functions - legacy_constant - legacy_constructor - legacy_hashing - legacy_multiple - legacy_nsgeometry_functions - legacy_random - let_var_whitespace - line_length - literal_expression_end_indentation - mark - missing_docs - modifier_order - multiline_arguments - multiline_function_chains - multiline_literal_brackets - multiline_parameters - multiple_closures_with_trailing_closure - no_extension_access_modifier - no_fallthrough_only - no_space_in_method_call - notification_center_detachment - nslocalizedstring_require_bundle - nsobject_prefer_isequal - number_separator - operator_usage_whitespace - operator_whitespace - optional_enum_case_matching - orphaned_doc_comment - overridden_super_call - override_in_extension - prefer_self_type_over_type_of_self - prefer_zero_over_explicit_init - private_over_fileprivate - private_subject - private_unit_test - prohibited_interface_builder - prohibited_super_call - protocol_property_accessors_order - reduce_boolean - reduce_into - redundant_discardable_let - redundant_nil_coalescing - redundant_objc_attribute - redundant_optional_initialization - redundant_set_access_control - redundant_string_enum_value - redundant_void_return - return_arrow_whitespace - shorthand_operator - sorted_first_last - sorted_imports - statement_position - superfluous_disable_command - switch_case_alignment - syntactic_sugar - toggle_bool - trailing_comma - trailing_newline - trailing_semicolon - trailing_whitespace - type_body_length - type_contents_order - type_name - unavailable_function - unneeded_break_in_switch - unneeded_parentheses_in_closure_argument - unowned_variable_capture - untyped_error_in_catch - unused_capture_list - unused_closure_parameter - unused_control_flow_label - unused_declaration - unused_enumerated - unused_import - unused_optional_binding - unused_setter_value - valid_ibinspectable - vertical_parameter_alignment - vertical_parameter_alignment_on_call - vertical_whitespace - vertical_whitespace_between_cases - vertical_whitespace_closing_braces - void_return - weak_delegate - xctfail_message - yoda_condition # Disabled Rules disabled_rules: - balanced_xctest_lifecycle # don't require balanced test setup and teardown - conditional_returns_on_newline # principles encourage one-line if and guard statements where applicable - cyclomatic_complexity # we have many complex switches that have over 100 cases - discouraged_assert # we don't currently prefer assertionFailure() and preconditionFailure() over assert(false) - discouraged_optional_boolean # disabled b/c nil, false, and true are all valid cases - discouraged_optional_collection # disabled b/c it looks at function signatures as well - explicit_acl # implicit internal ACL is a nice convenience - explicit_enum_raw_value # implicit raw values is a nice convenience - explicit_self # principles encourage you to drop self when it is not needed - explicit_top_level_acl # implicit internal ACL is a nice convenience - explicit_type_interface # implied Int and String types is a nice convenience - extension_access_modifier # extension declarations should not have an ACL modifier - file_types_order # type ordering cannot be consistent in all cases - force_unwrapping # disabled b/c of guards and ternary use cases - function_default_parameter_at_end # closures are often after parameters with default values - lower_acl_than_parent # only makes sense to enable if `no_extension_access_modifier` is disabled - multiline_arguments_brackets # doesn't match coding standards for collection parameters without external name - multiline_parameters_brackets # doesn't match coding standards for multi-line methods and functions - nesting # principles encourage namespacing through nested types - nimble_operator # not using nimble - no_grouping_extension # principles encourage grouping type functionality into extensions - nslocalizedstring_key # don't use genstrings due to loctool - object_literal # don't use color or image literals - opening_brace # doesn't support our multi-line function formatting - pattern_matching_keywords # principles encourage consistency and ease of reading - prefixed_toplevel_constant # not a good Swift pattern to prefix with `k` - private_action # don't use storyboards or nibs - private_outlet # don't use storyboards or nibs - quick_discouraged_call # not using quick - quick_discouraged_focused_test # not using quick - quick_discouraged_pending_test # not using quick - raw_value_for_camel_cased_codable_enum # don't restrict codable usage for webservice payloads - required_deinit # useful for debugging, but too extreme to require for all codebases - required_enum_case # this rule makes no sense...10 gold stars to anyone that can explain it - single_test_class # principles encourage multiple testcase classes in same file - static_operator # structs and classes should use static functions in extensions, not protocols - strict_fileprivate # still valid use cases for fileprivate - strong_iboutlet # don't use storyboards or nibs - switch_case_on_newline # principles encourage single line cases where applicable - trailing_closure # enforces trailing closure use even when function is called on multiple lines - vertical_whitespace_opening_braces # doesn't match coding standards for MARKs in structs and classes - xct_specific_matcher # valid cases where equal is more readable with optional bool tests # Excluded Directories excluded: - Pods - Submodules reporter: "xcode" # Configurable Rules closure_body_length: warning: 100 error: 140 deployment_target: iOS_deployment_target: 10.0 macOS_deployment_target: 10.12 tvOS_deployment_target: 10.0 watchOS_deployment_target: 3.0 expiring_todo: approaching_expiry_threshold: 15 date_format: "MM/dd/yyyy" date_delimiters: opening: "[" closing: "]" date_separator: "/" file_header: required_pattern: | \/\/ \/\/ .*?\.swift \/\/ [\w ]* \/\/ \/\/ Created by ([a-zA-Z-]+ ){2,4}on \d{1,2}\/\d{1,2}\/\d{2}\. \/\/ Copyright © \d{4} Nike\. All rights reserved\. \/\/ file_length: warning: 1000 error: 1200 ignore_comment_only_lines: true function_body_length: warning: 200 error: 240 function_parameter_count: warning: 8 error: 12 generic_type_name: min_length: 1 max_length: 40 identifier_name: min_length: 1 max_length: 50 excluded: ["id"] allowed_symbols: ["_"] # for gray_8D and _somePrivateVariable validates_start_with_lowercase: true large_tuple: warning: 5 error: 7 line_length: warning: 140 error: 160 ignores_urls: false ignores_function_declarations: false ignores_comments: false modifier_order: preferred_modifier_order: [ "acl", "setterACL", "override" ] number_separator: minimum_length: 6 minimum_fraction_length: 100 # to simply disable it type_body_length: warning: 600 error: 800 type_contents_order: order: [ ["case"], ["type_alias", "associated_type", "subtype", "type_property", "instance_property"], ["ib_outlet"], ["ib_inspectable"], ["initializer", "deinitializer"], ["type_method"], ["subscript"], ["view_life_cycle_method"], ["ib_action"], ["other_method"] ] type_name: min_length: 3 max_length: 50 allowed_symbols: ["_"] # Custom Rules custom_rules: fixme: include: ".*swift" name: "FIXME" regex: "((?i)(FIXME))" match_kinds: - comment message: "Unfinished Code" severity: warning hanging_else_keyword: included: ".*\\.swift" name: "There should not be a newline character after the `else` keyword." regex: "\\else\\s*\\n+" severity: warning todo_format: include: ".*swift" name: "TODO" regex: "((?i)(TODO: ))(?![A-Z]+-)" match_kinds: - comment message: "TODO should include Jira ticket e.g. '// TODO: TEAM-xxxx:'" severity: warning no_newlines_after_indent_changes_before_comments: included: ".*\\.swift" name: "No Newlines After Indent Changes Before Comments" regex: "\\{[\\w ]*?\\n[ ]*?\\n[ ]*\\/{2,3}[ ](?!MARK)" severity: warning blank_line_after_closing_brace_or_parenthesis: included: ".*\\.swift" name: "A Closing Brace/Parenthesis On Its Own Line Must Have a Newline Between It and Any Code" regex: "\\n[ ]*?[\\}\\)]\\n[ ]*?(?!set|case|else)[\\w]" severity: warning triple_quotation_marks_on_declaring_line: included: ".*\\.swift" name: "First Triple Quotation Marks Must Be On Declaring Line" regex: "=\\n[ ]*\"\"\"" severity: warning no_extra_newline_after_open_brace: included: ".*\\.swift" name: "No Extra Newline After An Open Brace" regex: "\\{\\n *\\n(?! *\\/\\/ MARK)" severity: warning uppercase_id_suffix: included: ".*\\.swift" name: "ID suffix should always be all uppercase" regex: "[\\w]*Id\\b" match_kinds: - argument - comment - identifier - parameter severity: warning uppercase_url_suffix: included: ".*\\.swift" name: "URL suffix should always be all uppercase" regex: "[\\w]*Url\\b" match_kinds: - argument - comment - identifier - parameter severity: warning uppercase_json_suffix: included: ".*\\.swift" name: "JSON suffix should always be all uppercase" regex: "[\\w]*Json\\b" match_kinds: - argument - comment - identifier - parameter severity: warning test_case_suffix: included: ".*\\.swift" name: "Test case suffix should always be 'TestCase'" regex: "[\\w]*Tests:" match_kinds: - identifier severity: warning
- 配置工程,添加启动脚本
# Type a script or drag a script file from your workspace to insert its path. if which swiftlint >/dev/null; then swiftlint else echo "warning: SwiftLint not installed, download from https://github.com/realm/SwiftLint" fi
- 在终端切换到该工程目录下,执行Fastlane命令:
bundle exec fastlane lint
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”中找到
Main
storyboard的设置地方。 - 默认情况下,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 可以选择:
UIWebView
WKWebView
这两个 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 Setting
value对应字段的缩写,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显示。
- 其中第一步校验数据,视情况而定,也有不需要的情况。