iOSiOS专题IT@程序员猿媛

iOS 图像渲染过程解析

2019-03-18  本文已影响13人  果哥爸

我们先假设这样一个场景:就是点击一个按键,然后实现一张图片的动画移动。

场景.gif

代码如下:

#import "FJFNineViewController.h"

@interface FJFNineViewController ()
// boxImageView
@property (nonatomic, strong) UIImageView *boxImageView;
@end

@implementation FJFNineViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *tmpButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    tmpButton.backgroundColor = [UIColor redColor];
    [tmpButton addTarget:self action:@selector(tmpButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:tmpButton];
    
    self.boxImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 400, 120, 120)];
    self.boxImageView.contentMode = UIViewContentModeScaleAspectFill;
    self.boxImageView.clipsToBounds = YES;
    self.boxImageView.image = [UIImage imageNamed:@"ic_red_box.png"];
    [self.view addSubview:self.boxImageView];
    
}


#pragma mark -------------------------- Response Event
- (void)tmpButtonClicked:(UIButton *)sender {
    [UIView animateWithDuration:0.5 animations:^{
        self.boxImageView.frame = CGRectMake(300, 80, 80, 80);
    }];
}

@end

一.图形渲染过程

1.视图渲染

image.png

下图是图形渲染的另一种表现形式:

图形渲染技术栈.png

如上图所示:在屏幕上显示视图,需要CPUGPU一起协作。一部数据通过CoreGraphicsCoreImage由CPU预处理。最终通过OpenGL ESMetal将数据传送到 GPU,最终显示到屏幕。

Core Graphics

Core Graphics 基于Quartz 高级绘图引擎,主要用于运行时绘制图像。开发者可以使用此框架来处理基于路径的绘图,转换,颜色管理,离屏渲染,图案,渐变和阴影,图像数据管理,图像创建和图像遮罩以及 PDF 文档创建,显示和分析。
当开发者需要在 运行时创建图像 时,可以使用 Core Graphics 去绘制。与之相对的是 运行前创建图像,例如用 Photoshop 提前做好图片素材直接导入应用。相比之下,我们更需要 Core Graphics 去在运行时实时计算、绘制一系列图像帧来实现动画。

Core Image
Core ImageCore Graphics 恰恰相反,Core Graphics 用于在 运行时创建图像,而 Core Image 是用来处理 运行前创建的图像 的。Core Image 框架拥有一系列现成的图像过滤器,能对已存在的图像进行高效的处理。
大部分情况下,Core Image 会在 GPU 中完成工作,但如果 GPU 忙,会使用 CPU 进行处理。
CoreImage支持CPU、GPU两种处理模式。

2.显示逻辑

image.png

二.提交流程(Commit Transaction)

Core Animation 流水线中,app 调用 Render Server 前的最后一步 Commit Transaction 其实可以细分为 4 个步骤:

Layout

Layout 阶段主要进行视图构建,包括:LayoutSubviews 方法的重载,addSubview: 方法填充子视图等。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

当程序运行main函数的时候,由于传入的principalClassNamenil,那么它的值将从Info.plist去获取,如果Info.plist没有,则默认为UIApplicationUIApplication设置AppDelegate为代理,然后通过代理方法:

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    
    self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
    self.window.rootViewController = [[ViewController alloc] init];
    [self.window makeKeyAndVisible];
    self.window.backgroundColor = [UIColor whiteColor];
    return YES;
}

生成windowwindow设置ViewController实例为rootViewController,因此生成如下所示的视图层级:

image.png

视图树如下所示:

image.png

如上图所示,我们可以看到视图树Xcode所展示的视图层级,多了UIApplication,这里的UIApplication是一个App进程的代表,作为视图树根节点,起到一种起始标志的作用,并不参与视图的处理过程,渲染服务进程是以UIApplication来识别渲染App进程的相关图层。

因为这里涉及到视图的生成,并将视图添加到对应的视图树中,因此会将这些视图标记为待处理,并提交到一个全局的容器中。

这里以tmpButtonboxImageView为例子说一下视图创建:

  UIButton *tmpButton = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    tmpButton.backgroundColor = [UIColor redColor];
    [tmpButton addTarget:self action:@selector(tmpButtonClicked:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:tmpButton];
    
    self.boxImageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 400, 120, 120)];
    self.boxImageView.contentMode = UIViewContentModeScaleAspectFill;
    self.boxImageView.clipsToBounds = YES;
    self.boxImageView.image = [UIImage imageNamed:@"ic_red_box.png"];
    [self.view addSubview:self.boxImageView];

当通过addSubview添加到视图树上面的self.View,这时对应会生成tmpButtonboxImageViewCALayer,同时CALayer对应会存储着
tmpButtonboxImageView设置的相关属性frame、backgroundColord等属性。

Display

Display 阶段主要进行视图绘制,这里仅仅是设置要成像的图元数据。重载视图的 drawRect: 方法可以自定义 UIView 的显示,其原理是在 drawRect: 方法内部绘制寄宿图,该过程使用 CPU内存

CALayer包含一个contents属性指向一块缓存区backing store,可以存放位图(Bitmap)

绘制完成的寄宿图就放在,缓存区的backing store

注意:绘制文本字符串比如说UILabel的文本,会默认调用drawRect来生成寄宿图

image.png

具体详见:iOS 图像渲染原理

由于我们代码并不涉及到调用drawRect来绘制寄宿图,因此不进行分析。

Prepare

Prepare 阶段属于附加步骤,一般处理图像的解码和转换等操作。

因为self.boxImageView.image = [UIImage imageNamed:@"ic_red_box.png"];进行了图片的赋值操作,所以在Prepare 阶段会进行图片数据的解码和图片格式转换操作。

Commit

Commit 阶段主要将图层进行打包(序列化),并将它们发送至 Render Server。该过程会递归执行,因为图层和视图都是以树形结构存在。

runloop即将进入休眠(BeforeWaiting)退出(Exit),会通知苹果之前注册Observer监听,回调去执行一个很长的函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会待处理的图层进行打包,并发送至 Render Server

这里的Core Animation会创建一个OpenGL ES纹理,并确保在boxImageView图层中的位图被上传到对应的纹理中。
同样当你重写-drawInContext方法时,Core Animation会请求分配一个纹理,同时确保Core Graphics会将你在-drawInContext中绘制的东西放入到纹理的位图数据中。

动画渲染原理

- (void)tmpButtonClicked:(UIButton *)sender {
    [UIView animateWithDuration:0.5 animations:^{
        self.boxImageView.frame = CGRectMake(300, 80, 80, 80);
    }];
}

iOS 动画的渲染也是基于上述 Core Animation 流水线完成的。这里我们重点关注appRender Server 的执行流程。

日常开发中,如果不是特别复杂的动画,一般使用 UIView Animation 实现,iOS将其处理过程分为如下三部阶段:

image.png

这里只进行一次将图层树CAAnimation对象提交到渲染服务进程,然后由渲染服务进程根据CAAnimation参数图层树信息,去渲染动画过程中的每一帧。

三.渲染服务(Render Server)

Render Server.png

渲染树就是指图层树对应每个图层的信息,比如顶点坐标、顶点颜色这些信息,抽离出来,形成的树结构,就叫渲染树了

四. 图形渲染管线(Graphics Rendering Pipeline)

OpenGL ES /Metal的作用是通过它提供给我们的API,最终在CPU上生成GPU可以理解的一系列指令(Commands),然后提交给GPU去执行它。

Graphics Rendering Pipeline,图形渲染管线,实际上指的是一堆原始图形数据途经一个输送管道,期间经过各种变化处理最终出现在屏幕的过程。通常情况下,渲染管线可以描述成 vertices(顶点)pixels(像素) 的过程

无论是OpenGL ES /Metal都有图形渲染管线这个概念,它是各类图形API通用的一个概念。

图形渲染管线的主要工作可以被划分为两个部分:

GPU 图形渲染管线的具体实现可分为六个阶段,如下图所示。

我们以最经典的三角形绘制为例,如下是一个典型的管线处理过程:

image.png

1.顶点着色器

顶点着色器对每个顶点执行一次运算,它可以使用顶点数据来计算该顶点的坐标,颜色,光照,纹理坐标等,在渲染管线每个顶点都是独立地被执行。

每个顶点都对应一组顶点数组,可以激活(启用)最多可达8个数组,每个数组用于存储不同类型的数据:顶点坐标、表面法线、RGBA颜色、辅助颜色、颜色索引、雾坐标、纹理坐标以及多边形的边界标志等。

如果设置了相关的属性就会激活相关的数组,比如tmpButton激活了顶点坐标、RGBA颜色boxImageView激活了顶点坐标、纹理坐标

接下来对顶点坐标进行变换,应用程序中设置的图元顶点坐标通常是针对本地坐标系的,本地坐标系简化了程序中的坐标计算,但是GPU 并不识别本地坐标系,所以在顶点着色器中要对本地坐标执行如下变换。

image.png

世界坐标系:

image.png image.png

透视投影:

image.png

W = 1:

image.png

视口变换是在投影变换之后,将空间中的物体变换到视口中:

image.png

引用自:OpenGl从零开始之坐标变换

2.形状(图元)装配

该阶段将顶点着色器输出的所有顶点作为输入,并将所有的点装配成指定图元的形状。图中则是一个三角形图元(Primitive) 用于表示如何渲染顶点数据,如:点、线、三角形

这里很关键的一点就是,在顶点着色器程序输出顶点坐标之后,各个顶点被按照绘制命令中的图元类型参数,以及顶点索引数组被组装成一个个图元。

Metal 中,支持的图元类型如下,也是由点,线,三角形组成。

typedef NS_ENUM(NSUInteger, MTLPrimitiveType) {
    MTLPrimitiveTypePoint = 0,
    MTLPrimitiveTypeLine = 1,
    MTLPrimitiveTypeLineStrip = 2,
    MTLPrimitiveTypeTriangle = 3,
    MTLPrimitiveTypeTriangleStrip = 4,
} NS_ENUM_AVAILABLE(10_11, 8_0);
image.png

同样的OpenGL支持图元类型如下:

image.png image.png

每个图元由一个或者多个顶点组成,每个顶点定义一个点,一条边的一端或者三角形的一个角。每个顶点关联一些数据,这些数据包括顶点坐标,颜色,法向量以及纹理坐标等。所有这些顶点相关的信息就构成顶点数据

这里由于顶点数据较多,因此性能更高的做法是,提前分配一块显存,将顶点数据预先传入到显存当中。这部分的显存,就被称为顶点缓冲区

另外,在绘制图像时,总是会有一些顶点被多个图元共享,而反复对这个顶点进行运算常常是没有必要的(也有某些特殊场景需要)。因此对通过索引数据,指示OpenGL绘制顶点的顺序,不但能防止顶点的重复运算,也能在不修改顶点数据的情况下,一定程度的重新组合图像。

顶点数据一样,索引数据也可以以索引数组的形式存储在内存当中,调用绘制函数时传入;或者提前分配一块显存,将索引数据存储在这块显存当中,这块显存就被称为索引缓冲区。同样的,使用缓冲区的方式,性能一般会比直接使用索引数组的方式更加高效。

举个例子:
比如生成一个正方形,会生成如下的顶点坐标数组和索引数组:

image.png

显存,也被叫做帧缓存,它的作用是用来存储显卡芯片处理过或者即将提取的渲染数据。如同计算机的内存一样,显存是用来存储要处理的图形信息的部件。

3. 几何着色器。

该阶段把图元形式的一系列顶点的集合作为输入,它可以通过产生新顶点构造出新的(或是其它的)图元来生成其他形状。例子中,它生成了另一个三角形。

通俗来讲:几何着色器就是提供图元相互之间的连接信息,将原本独立的图元连接起来。

如下图所示:

image.png

注意:几何着色器是一个可选的阶段,比如我们创建的tmpButton,在图元装配阶段,就可以根据顶点坐标、索引值、图元类型(GL_QUADS)就可以确定这是一个正方形,就无需再经过几何着色器这个阶段。

4.光栅化

光栅化阶段,基本图元被转换为供片段着色器使用的片段(Fragment)Fragment 表示可以被渲染到屏幕上的像素,它包含位置,颜色,纹理坐标等信息,这些值是由图元的顶点信息进行插值计算得到的。这些片元接着被送到片元着色器中处理。这是从顶点数据到可渲染在显示设备上的像素的质变过程

片段着色器运行之前会执行裁切(Clipping)裁切会丢弃超出你的视图以外的所有像素,用来提升执行效率。

image.png

红色区域即表示真正会进入片段着色器 (Fragment function) 中进行处理的片段。

5.片段着色器

片段着色器的主要作用是计算每一个片段最终的颜色值。

可编程的片段着色器是实现一些高级特效如纹理贴图,光照,环境光,阴影等功能的基础,这就是最精彩的部分。

在片段着色器之前的阶段,渲染管线都只是在和顶点,图元打交道。而在 3D 图形程序开发中,贴图是最重要的部分,我们的 Resources,可以包含纹理等数据,这些纹理可以被片段着色器使用。片段着色器可以根据顶点着色器输出的顶点纹理坐标对纹理进行采样,以计算该片段的颜色值。从而调整成各种各样不同的效果图。

另外,片段着色器也是执行光照等高级特效的地方,比如可以传给片段着色器一个光源位置光源颜色,可以根据一定的公式计算出一个新的颜色值,这样就可以实现光照特效

6. 测试与混合

测试:

在着色器程序完成之后,我们得到了像素数据。这些数据必须要通过测试才能最终绘制到画布,也就是帧缓冲上的颜色附着上。

测试主要可以分为像素所有者测试(PixelOwnershipTest)、裁剪测试(ScissorTest)、模板测试(StencilTest)和深度测试(DepthTest),执行的顺序也是按照这个顺序进行执行。

混合:

在 测试阶段 之后,如果像素依然没有被剔除,那么 像素的颜色 将会和 帧缓冲区 中颜色附着上的颜色进行混合, 混合的算法可以通过 OpenGL/ Metal的函数进行指定。但是OpenGL/ Metal提供的混合算法是有限的,如果需要更加复杂的混合算法,一般可以通过像素着色器进行实现,当然性能会比原生的混合算法差一些。

抖动:
混合阶段过后,根据OpenGL/Metal的状态设置,会决定是否有抖动这个阶段。

抖动是一种针对对于可用颜色较少的系统,可以以牺牲分辨率为代价,通过颜色值的抖动来增加可用颜色数量的技术。抖动操作是和硬件相关的,允许程序员所做的操作就只有打开或关闭抖动操作。实际上,若机器的分辨率已经相当高,激活抖动操作根本就没有任何意义。默认情况下,抖动是激活的。

这里由于demo中的UIWindow被设置为白色、UIViewControllerview没有设置背景色,tmpButton为红色、boxImageView加载图片,图片没有设置alpha,所以这里被视图都是不透明(UIWindow、tmpButton、boxImageView)或者完全透明(self.View)因此在渲染服务进程处理阶段,会根据图层树中图层顺序、图层位置、图层的RGBA值,进行过滤,因此最后留下的图层RGBA值,就是要显示屏幕的RGBA值。因此这里并不涉及到图层颜色的混合。

经历了测试和混合后,帧缓冲区绑定的颜色缓冲区就是最终要显示到屏幕上面的颜色值。

五.屏幕显示

image.png

通常来说,计算机系统中CPU、GPU、显示器是以上面这种方式协同工作的。

CPU 计算好显示内容提交到 GPUGPU 渲染完成后将渲染结果放入帧缓冲区

随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。

image.png

iOS 设备会始终使用双缓存,并开启垂直同步。
由于垂直同步的机制,如果在一个 VSync 时间内,CPU或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

双缓冲工作原理:GPU 会预先渲染一帧放入一个缓冲区中,用于视频控制器的读取。当下一帧渲染完毕后,GPU 会直接把视频控制器的指针指向第二个缓冲器

image.png

这里介绍屏幕图像显示的原理,需要先从 CRT 显示器原理说起,如下图所示。
CRT 的电子枪从上到下逐行扫描,扫描完成后显示器就呈现一帧画面。然后电子枪回到初始位置进行下一次扫描。为了同步显示器的显示过程和系统的视频控制器,显示器会用硬件时钟产生一系列的定时信号。当电子枪换行进行扫描时,显示器会发出一个水平同步信号(horizonal synchronization),简称 HSync;而当一帧画面绘制完成后,电子枪回复到原位,准备画下一帧前,显示器会发出一个垂直同步信号(vertical synchronization),简称 VSync。显示器通常以固定频率进行刷新,这个刷新率就是VSync 信号产生的频率。虽然现在的显示器基本都是液晶显示屏了,但其原理基本一致。

image.png

引用自:计算机那些事(7)——图形图像渲染原理

阅读延伸:

Metal入门教程总结
Metal【1】—— 概述
深入理解RunLoop
OpenGL全流程详细解读
iOS 图像渲染原理
iOS开发-视图渲染与性能优化
计算机那些事(7)——图形图像渲染原理

上一篇下一篇

猜你喜欢

热点阅读