一:图像成像过程
①、将需要显示的图像,由CPU和GPU通过总线连接起来,在CPU中输出的位图经总线在合适的时机上传给GPU ,GPU拿到位图做相应位图的图层渲染、纹理合成。
②、将渲染后的结果,存储到帧缓存区,帧缓存区中存储的格式是位图。
③、由视屏控制器根据Vsync(垂直同步信号)在指定时间之前去提取对应帧缓冲区当中的屏幕内容,交由显示器,从左上角逐行扫描进行显示。
如图所示:
二:图片纹理映射
我们在获取到图片的纹理数据后,要将纹理显示到屏幕上,先要做两件事:
①、将图片的纹理坐标通过 attribute方式,经顶点着色器桥接给片元着色器
②、将图片纹理数据通过Uniform传递给片元着色器,由片元着色器进行图片颜色的填充
在图片进行纹理颜色填充时,需要按照坐标进行一一对应,纹理坐标默认左下角为(0,0),右上角为(0,1)。
纹理的坐标与图形的坐标一一对应,最终会将图片正确的显示出来。如果纹理坐标映射的不正确则可能出现图片翻转、倒置等情况,甚至图片信息错乱。
三:图片解压
在解释图片解压之前我们先了解几个概念:
①、位图:
又叫像素图或栅格图,它记录了图片每一个像素的颜色、深度、透明度等信息。这一系列像素按照一定的规则排列起来,就形成了我们看到的图片。位图的优点是能够完整记录图片信息,无论图片怎样拉伸都不会失真,缺点是图片文件太大,因此一般将位图压缩为jpg、png等格式。
②、有损压缩:
不会完全真实的记录图片信息,会根据人眼观察世界的特性,忽略掉部分会被人眼忽略的颜色信息,代之以邻近的颜色。因此图片虽然大部分可以还原,但某些情况下还是会失真,常见的有损压缩格式有JPG等。
③、无损压缩:
无损压缩会完整记录图片颜色信息,但是相同颜色的区域,会被压缩记录,因此无损压缩也可以比较完整的还原图片。不过由于能够保存的颜色值有限,所以依然有可能会出现失真,常见的格式有PNG等。
④、解压流程
在我们的开发过程中,我们使用比较多的都是 JPG 或者 PNG 等格式图片,但是在图片真正显示之前,都会被先解压成位图,再重新渲染到屏幕上。所以图片解压的流程是:
1、解压JPG/PNG图片,获取图片信息
2、根据获取到的图片信息重新绘制位图,即纹理数据
3、将纹理数据载入,传入到片元着色器,经过渲染后显示
⑤、解压方法(Core Graphic)
UImage *image = [UImage imageNamed:@"fly"];
CGImageRef cgImageRef = [image CGImage]; // 将UImage转换为CGImageRef
// 获取图片宽高
GLuint width = (GLuint)CGImageGetWidth(cgImageRef);
GLuint height = (GLuint)CGImageGetHeight(cgImageRef);
//获取图片的rect
CGRect rect = CGRectMake(0, 0, width, height);
//获取图片的颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// 为图片开辟一片内存区域
// 一个像素点的颜色值包含 RGBA 各8位,共4个字节
void *imageData = malloc(width * height * 4);
// 创建上下文
/**
CGBitmapContextCreate(void * __nullable data,
size_t width, size_t height, size_t bitsPerComponent, size_t bytesPerRow,
CGColorSpaceRef cg_nullable space, uint32_t bitmapInfo)
data:如果不为 NULL ,那么它应该指向一块大小至少为 bytesPerRow * height 字节的内存;如果 为 NULL ,那么系统就会为我们自动分配和释放所需的内存,所以一般指定 NULL 即可;
width: 图片宽度
height:图片高度
bitsPerComponent:每个颜色分量所占bit数,此处传8位
bytesPerRow:位图的每一行使用的字节数,大小至少为 width * bytes per pixel 字节。当我们指定 0/NULL 时,系统不仅会为我们自动计算,而且还会进行cache line alignment 的优化
space:颜色空间
bitmapInfo:位图的信息,此处采用RGBA,即kCGImageAlphaPremultipliedLast
*/
CGContextRef context = CGBitmapContextCreate(imageData, width, height, 8, width * 4, colorSpace, kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
// 将图片翻转过来(图片默认是倒置的)
// 图片的坐标系左上角为(0,0),纹理坐标左下角为(0,0),因此需要翻转
CGContextTranslateCTM(context, 0, height);
CGContextScaleCTM(context, 1.0f, -1.0f);
// 绘制前先清除颜色空间和绘图区域,防止残留数据
CGColorSpaceRelease(colorSpace);
CGContextClearRect(context, rect);
// 对图片进行重新绘制,得到一张新的解压缩后的位图
CGContextDrawImage(context, rect, cgImageRef);
// 设置图片纹理属性
// 获取纹理ID
GLuint textureID;
glGenTextures(1, &textureID); // 获取一个纹理句柄
glBindTexture(GL_TEXTURE_2D, textureID); // 将句柄绑定到纹理目标上,GL_TEXTURE_2D等
// 设置纹理属性
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); // 将纹理目标重新绑定为0
CGContextRelease(context); // 释放context
free(imageData); // 释放图片数据区域
⑥、总结
1、图片文件只有在确认要显示时,CPU才会对齐进行解压缩.因为解压是非常消耗性能的事情.解压过的图片就不会重复解压,会缓存起来.
2、图片渲染到屏幕的过程: 读取文件->计算Frame->图片解码->解码后纹理图片位图数据通过数据总线交给GPU->GPU获取图片Frame->顶点变换计算->光栅化->根据纹理坐标获取每个像素点的颜色值(如果出现透明值需要将每个像素点的颜色*透明度值)->渲染到帧缓存区->渲染到屏幕
四、图片渲染流程
1、App通过CoreGraphics、CoreAnimation、CoreImage等框架的接口调用来触发图形渲染操作
2、CoreGraphics、CoreAnimation、CoreImage等框架将渲染交由OpenGL ES,由OpenGL ES来驱动GPU做渲染,最后显示到屏幕上
3、由于OpenGL ES 是跨平台的,所以在他的实现中,没有任何窗口相关的代码,而是让各自的平台为OpenGL ES提供载体。在iOS中,如果需要使用OpenGL ES,就是通过CoreAnimation提供窗口,让App可以去调用。
五、屏幕卡顿
屏幕卡顿是指图形图像的在显示时出现了撕裂(即图片错位显示)、掉帧(重复显示同一帧数据)等问题,导致用户能直观的从屏幕上看到的一种异常现象。
①、屏幕卡顿原因
1、由图像的显示原理,我们知道一帧的显示是由CPU和GPU共同决定的。一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,从缓存区获取位图显示时,下一帧数据还没准备好,获取的仍是上一帧的数据,产生掉帧现象,掉帧就会导致屏幕卡顿。
2、苹果官方为解决屏幕撕裂问题,目前使用的方案是垂直同步+双缓存区,可以从根本上防止和解决屏幕撕裂,但是同时也导致了新的问题掉帧。虽然我们采用了双缓存区,但是我们并不能解决CPU和GPU处理图形图像的速度问题,导致屏幕在接收到垂直信号时,数据尚未准备好,缓存区仍是上一帧的数据,因此导致掉帧,如图5-1-2所示。
3、在垂直同步+双缓存区方案的基础上,提出将双缓存区,改为三缓存区的优化,但是这样并不能从根本解决掉帧问题,只是比双缓存区掉帧的概率小很多,用户可能无感知。
图5-1-2
②、垂直同步与双缓存区
1、VSync(垂直同步信号):是指给帧缓冲加锁,当电子光束扫描的过程中,只有扫描完成了才会读取下一帧的数据,而不是只读取一部分
2、双缓存区:采用两个帧缓冲区用来存储GPU处理的结果,当屏幕显示其中一个缓存区内容时,另一个缓冲区继续等待下一个缓冲结果,两个缓冲区依次进行交替