什么情况下会导致opengl渲染管线的图像不能播出

opengl 渲染不同 纹理图 比如说想创建两个精灵(形状不规则)?_百度知道
opengl 渲染不同 纹理图 比如说想创建两个精灵(形状不规则)?
之后所有渲染 的都会是同一个纹理图,无论怎么创建
怎么解决?
提问者采纳
你要理解opengl是一个“状态机”这个概念,当你绑定了一个纹理对象后,那么opengl认为当前只要是贴图我就用这个纹理了,除非你删除这个纹理对象获绑定了其他对象,想用别的图贴,你只要再glbind一下别的纹理对象就好了。
如果我场景中想创建多个精灵的话。。。岂不是很费事。。。。
但是想用多个贴图的话,就得创建多个纹理对象存起来,每次都bind下,据我的了解。
提问者评价
不管怎么样 还是谢了。。。。知道怎么做了。。。
其他类似问题
为您推荐:
opengl的相关知识
等待您来回答
下载知道APP
随时随地咨询
出门在外也不愁  最近由于手机项目中需要用到OpenGL ES的知识,所以这段时间正在研究OpenGL的相关知识。因为OpenGL ES是OpenGL的剪裁版本,所以我直接从OpenGL入手,然后再去看OpenGL ES就很容易上手。从此篇开始,我将发表一系列文章来逐步深入介绍OpenGL的相关知识,开发我们可以使用VC6.0或.NET。
  那么今天我要介绍的是OpenGL开发之旅基础知识介绍,这很重要,会让我们从整体上熟悉OpenGL的工作原理及过程。
  1. 保持模式与立即模式:  通常情况下我们编写3D图形时可使用两种不同的方法:  一种方法我们称之为保持模式。在保持模式中,我们可以向编写的API或是工具箱提供物体及场景的描述,然后图形包就会在屏幕上创建这个图像,我们需要做的就是提供命令去改变照相机或场景中其他物体的位置和观察方向。对于我们开发者而言,我们创建的能够对物体及场景的描述称为场景图,场景图是什么呢?大家可能对这个名称比较熟悉但却不能说出它的准确含义。在通常情况下,场景图是个有向无环图的数据结构,它包含了场景中的所有物体以及这些物体之间相互的关系,实现了对物体及场景的描述。据了解,现在许多的游戏引擎及高层工具箱都使用了这种方法。这使得我们开发人员不需要对其渲染过程进行特别精细的控制,它只需要向图形函数库提供一个模型或是场景,图形函数库就会负责进行渲染,大大减轻了我们开发人员的工作量。
  另一种方法我们称之为立即模式。在立即模式中,我们不需要像保持模式一样去提供一个模型或是场景,而是向图形处理器发送命令,图形处理器就会根据它的状态及发送的命令产生立即的效果。查询图形学书籍得知,大多数保持模式中的API或场景图在其内部使用一个立即模式的API执行实际的渲染任务。
  2. OpenGL是什么?
  OpenGL是一套应用程序编程接口(API),借助这个API我们开发人员就可以开发出对图形硬件具有访问的能力的程序。我们可以使用OpenGL开发出运行效率较高的图形程序或游戏,因为OpenGL非常接近底层硬件并且OpenGL使得我们不必去关注图形硬件的细节。既然我们开发人员不必关注图形硬件的细节,那么我们需要关注什么呢?我们需要关注OpenGL如何绘制,按照专业术语就是根据物体的规格参数及相关属性,借助虚拟照相机和光照生成一幅该物体的图像。OpenGL程序与平台是无关的,所以OpenGL API中不包含任何输入函数或窗口函数,原因是因为这两种函数都要依赖于特定的平台,例如Windows,Linux或是其他系统。
  OpenGL API是过程性的,不是描述性的,即OpenGL不是面向对象的,所以OpenGL无法利用面向对象的特性,例如重载,继承等,但是我们可以使用面向对象的程序与OpenGL的实现进行链接就可以了。作为开发人员来说,我们不需要去描述场景的性质和外观,而是去确定一些操作步骤,为些操作步骤是为实现一定图形或图像所服务的。我们在实现这些步骤时可以调用OpenGL中的一些命令,可以利用这些命令绘制点、直线、多边形或是其它图形,还可以调用这些命令实现光照、着色,动画等各种效果。
  OpenGL的实现可以是软件实现,也可以是硬件实现。软件实现是对OpengGL函数调用时作出的响应并创建二维或三维图像的函数库,那么硬件实现则是通过设置能够绘制图形或图像的图形卡驱动程序。一般来说,硬件实现要比软件实现快得多。我们都应该熟悉,在Windows上,是由图形设备接口将图形或图像显示在屏幕上或是其他显示设备上的。OpenGL的实现就软件实现来说,在Windows上会根据程序命令的要求,生成相应的图形或图像,然后会将这个图形或图像移交给图形设备接口,由图形设备接口将图形或是图像显示在我们的屏幕上或是其他显示设备。这样一说,我们可能会明白一点OpengGL原来是在应用程序和图形设备接口之间运作,但我感觉还不能准确地这样说。大家看下下面的图对OpenGL的工作原理可能会理解得更明白一点: 
  上图是OpenGL的软件实现的工作原理。需要注意的是上图中的构造图形是通过软件进行构造的。
  OpenGL的硬件实现与软件实现稍微有些不同,硬件实现是将OpenGL的调用传递给硬件驱动程序,而硬件驱动程序不会将生成的图形或图像传递给图形设备接口,而是直接与显示设备通信,直接将图形或图像结果传递给显示器或其他显示设备。如下图所示:
  OpenGL在绘制图形时是基于一个被称为流水线模型的模式。也就是说其中的几何图形在程序中通过描述空间位置或顶点来指定其形状并由程序生成,这些顶点在流经一系列模块时,每个模块在图形的基本组成部分(在这里称为图元)经过时对其实施一种或多种操作。模块负责对流经的图元实施一种或多种操作变换,例如:旋转、平移、缩放及对摄像机进行定位等。
  3. OpenGL的组成
  OpenGL中包含许多对图形图像处理的函数,主要包括以下几种:
  图元函数:指定要生成图形或图像的图元。主要有两种类型,一种是绘制二维或三维的几何图元,如点,线,多边形等;另一种是离散型的实体,例如:位图。
  属性函数:属性函数主要是控制图元的外观及样式,例如:对图元的颜色、线型、光照及纹理等效果处理。
  观察函数:观察函数主要是对摄像机属性的操作。我们可以操作摄像机显示图形或图像近距或是远距效果。
  控制函数:能够让我们启用或是彬各种OpenGL的特性。
  查询函数:可以让我们查询OpenGL状态变量的值。
  输入与窗口控制函数:这个本身不属于OpenGL,但是由于我们会经常在程序中输入输出或是窗口控制操作,所以,这些函数还是比较重要的。
  OpenGL函数库一般包含在两个库中,分别称为GL或GLU。GL是OpenGL的核心库,包含必需的OpenGL函数。GLU是OpenGL的实用库,包含许多的新函数。下面的代码显示了许多的Windows程序包含的典型头文件:
#include &GL/gl.h&
#include &GL/glu.h&
#include &windows.h&
  但是为了实现和窗口系统的交互,一般使用如下代码引用头文件:
#include &GL/glut.h&
  GLUT表示OpenGL工具箱,体现了现代窗口系统所共有的功能函数库。GLUT的目的就是隐藏平台的细节,glut.h已经包含了gl.h和glu.h。使用GLUT是因为OpenGL没有包含输入和窗口命令,而输入和窗口命令是由平台所决定的,与平台的相关性较大。但是前面说过,OpenGL是与平台无关的,也就是说OpenGL是跨平台的。这样设计人员就需要专门设计一个需要和窗口系统进行交互的函数库。
  为了能使OpenGL代码更易于从一个平台移植到另一个平台,OpenGL定义了它自己的数据类型,这些数据类型都可以映射到相应的C语言数据类型中。下图显示其映射关系:
  4. 开发语言与编程约定
  我们开发OpenGL目前最流行的做法是OpenGL的C语言绑定,当然也可以使用其它平台或语言,例如.NET、JAVA、Python、Perl等。我会在下一篇文章中介绍如何配置相应的环境。
  我们以后会见到OpenGL的函数多是以gl开头,因为OpenGL的函数遵循一定的命名约定,它可以告诉我们这个函数来自哪个函数库,并且还可以告诉我们这个函数的参数个数和类型。
  OpenGL的函数是采用以下的书写格式:
  &函数库前缀&&根命令&&可选的参数数量&&可选的参数类型&
  以下是一个函数标注图:
  5. 坐标系与变换
  在开发OpenGL程序时,需要用到两个坐标系。一个称为对象坐标系,另一个称为世界坐标系。
  第一个坐标系是我们在开发中使用的坐标系;第二个坐标系又称为窗口坐标系或屏幕坐标系,在这个坐标系中的单位是像素。
  在绘制的过程中,OpenGL会自动实现从对象到窗口坐标系的转换,所需要的信息是屏幕中显示窗口的尺寸和用户希望显示对象空间的大小。OpenGL中所需要的坐标系变换由两个矩阵决定,即模型视图矩阵和投影矩阵,这些矩阵是OpenGL的状态的一部分。设置这两种矩阵的典型步骤包括以下三个步骤:
  (1) 指定我们希望修改的矩阵。
  (2) 将矩阵设为单位矩阵。
  (3) 修改当前矩阵为用户期望的矩阵。
  以上三个步骤分别对应以下代码:
glMatrixMode(GL_PROJECTION)
glLoadIdentity();
gluortho2D(-1.0,1.0,-1.0,1.0)
  6. 图元及属性
  图元是图形系统中常用的基本实体,主要是指:点,线,直线,多边形,位图和像素,我们的2D或3D图形都可由这几个基本的图元来绘制。前面四个称为几何图元,主要是构建几何图形;后面二个称为非几何图元。OpenGL在处理几何图元和非几何图元的方式差别比较大。
  每个图元都有自己的属性,属性决定了由OpenGL显示的方式。比如多边形的颜色,形状,线条的粗细等。
  我们在绘制基本的图形时,总是以glBegin()函数开始,而以glEnd()函数结束,针对不同的图形,glBegin()函数中的参数不一样。如下所示是绘制两条直线段:
glBegin(GL_LINES);
glVertex2f(-0.5, -0.5);
glVertex2f(-0.5, 0.5);
glVertex2f(0.5, 0.5);
glVertex2f(0.5, -0.5);
  OpenGL在绘制图形时有很多功能,比如:光照、消隐、纹理映射等,每一种功能都将影响绘制处理的速度,在我们的程序中可单独的启用或是禁用某些功能,在不使用时要将其禁用掉以使我们的程序更加高效。以下是启用或是禁用某项功能的代码:
void glEnable(GLenum feature)
void glDisable(GLenum feature)
//启用点划模式
glEnable(GL_LINE-STIPPLE)
  7. 状态的保存
  OpenGL在内部就是一个状态机,函数调用会修改其内部的状态,OpenGL的状态决定了图元的行为和绘制方式。我们对图元的属性和其他状态变量所进行的全部修改,例如模型视图矩阵和投影矩阵,都会改变当前的状态。在OpenGL中提供了两种类型的堆栈,可将当前状态保存在堆栈中,以便以后使用。
  矩阵堆栈可用于保存投影矩阵和模型视图矩阵。每种类型的堆栈只能用来容纳相应类型的矩阵。所使用的矩阵由当前矩阵模式(GL_MODELVIEW或GL_PROJECTION)所决定的。可用函数glPushMatrix()和glPopMatrix()使矩阵入栈或出栈。
  矩阵堆栈的主要作用:一是在构建层次模型时,使用堆栈来遍历这些层次模型的树型数据结构;二是在进行绘制时可以回到先前的视图,而不需要我们重新计算绘制。我们会在开发过程中常看到以下代码:
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glPopMatrix();
  需要注意的是,入栈操作和出栈操作必须成对使用;一次出栈必须与一次入栈对应。在层次系统中,如果这对操作没有正确的成对出现的话,将使堆栈处于一种不可预知的状态。
  8. 单缓冲与双缓冲
  我们在构建图形窗口时经常也会经常看到以下的一行代码:
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);
  这行代码告诉我们在创建时使用何种类型的显示模式。GLUT_SINGLE表示使用一个单缓冲的窗口;GLUT_RGBA表示使用RGBA颜色模式。
  单缓冲窗口意味着所有的绘图命令都是在被显示的窗口上执行的。另一种显示模式是双缓冲窗口,绘图命令实际上是在一个屏幕之外的缓冲区中执行的,然后快速反应的交换到窗口的视图上进行显示。我们经常用双缓冲模式开发具有动画效果的程序,这样会提高我们程序的执行效率。
  9. 一个简单的示例
  好了,上面说了那么多,有的人可能想知道OpenGL的程序到底是什么样的,主框架是什么?大家如果有C语言编程经验的一眼就能看出来它的结构。
  接下来让我们看一个运行VC++环境中的OpenGL示例,代码如下:
#include "StdAfx.h"
#ifdef __APPLE__
#include &GLUT/glut.h&
#include &GL/glut.h&
/* glut.h 包含 gl.h和glu.h*/
void display(void)
// 清除颜色缓冲区
glClear(GL_COLOR_BUFFER_BIT);
// 绘制矩形
glBegin(GL_POLYGON);
glVertex2f(-0.5, -0.5);
glVertex2f(-0.5, 0.5);
glVertex2f(0.5, 0.5);
glVertex2f(0.5, -0.5);
// 执行缓冲区
glFlush();
int main(int argc, char** argv)
// 初始化窗口
glutInit(&argc,argv);
glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB);
glutInitWindowSize(500,500);
glutInitWindowPosition(0,0);
glutCreateWindow("绘制矩形");
glutDisplayFunc(display);
glutMainLoop();
  这是运行在VC6.0中的一个示例,在创建项目时需要注意创建的是控制台项目。
  怎么样?main函数中的代码你能看懂它的意思吗?我相信对大家不是难事。下面看来简单的解释。
  (1)&glutCreateWindow("绘制矩形")是创建一个标题是"绘制矩形"的窗口。
  (2)&glutInitDisplayMode (GLUT_SINGLE | GLUT_RGB)函数在上面已经说过了。
  (3)&glutDisplayFunc(display);&这行代码是将前面定义的display()函数确定为显示回调函数。也就是说在窗口需要被绘制时,GLUT将会调用这个函数。比如说,当窗口第一次显示或是窗口大小改变的时候,或是窗口从被覆盖的状态中恢复时,就会发生这个调用。这也是我们放置OpenGL渲染函数调用的地方。
  (4)&glutInitWindowSize(500,500)和glutInitWindowPosition(0,0)则是分别设置窗口的大小和位置。
  (5)&glutMainLoop() 这个函数很重要。这个函数启动了GLUT框架的运行。该函数一经调用便不再返回,直到程序的结束。所以,该函数在我们的程序中只调用一次,它能处理操作系统中特定的消息及击键等事件操作,直到我们的程序结束。
  以上示例的效果是在窗体中绘制一个白色的矩形,效果图如下所示:
  该程序演示了在OpenGL中使用GLUT创建窗口的基本原理。
  以上便是OpenGL开发中的基本原理和基础知识。
  最后,希望转载的朋友能够尊重作者的劳动成果,加上转载地址:&谢谢。
  示例下载:
  完毕。^_^
阅读(...) 评论()收藏,12.2k 浏览
译注:这篇文章虽然比较长,但是里面的内容还是很有价值的。
像素是如何绘制到屏幕上面的?把数据输出到屏幕的方法有很多,通过调用很多不同的framework和不同的函数。这里我们讲一下这个过程背后的东西。希望能够帮助大家了解什么时候该使用什么API,特别是当遇到性能问题需要调试的时候。当然,我们这里主要讲iOS,但是事实上,很多东西也是可以应用到OSX上面的。
Graphics Stack
绘制屏幕的过程中又很多都是不被人了解的。但是一旦像素被绘制到屏幕上面,那么像素就是有3种颜色组成:红绿蓝。这3个颜色单元通过特定的强弱组合形成一个特定的颜色。对于iPhone5
的分辨率是1,136×640 = 727,040个像素,也就是有2,181,120个颜色单元。对于一个15寸高清屏幕的MacBook Pro来说,这个数字差不多是1500万。Graphics Stack 就是确保每一个单元的强弱都正确。当滑动整个屏幕的时候,上百万的颜色单元需要在每秒60次的更新。
The Software Components
下面是一个简单的例子,整个软件看起来是这个样子:
显示器上面的就是GPU,图像处理单元。GPU是一个高度并发计算的硬件单元,特别是处理图形图像的并行计算。这就是为什么可以这么快的更新像素并输出到屏幕的原因。并行计算的设计让GPU可以高效的混合图像纹理。我们会在后面详细解释混合图像纹理这个过程。现在需要知道的就是GPU是被高度优化设计的,因此非常适合计算图像这种类型的工作。他比CPU计算的更快,更节约能耗。因为CPU是为了更一般的计算设计的硬件。CPU虽然可以做很多事情,但是在图像这方面还是远远慢于GPU。
GPU驱动是一些直接操作GPU的代码,由于各个GPU是不同的,驱动在他们之上创建一个层,这个层通常是OpenGL/OpenGL ES。
OpenGL()是用来做2D和3G图形图像渲染的API。由于GPU是一个非常定制化的硬件,OpenGL和GPU紧密合作充分发挥GPU的能力来实现图形图像渲染硬件加速。对大多数情况,OpenGL太底层了。但是当1992年第一个版本发布后(20多年前),它就成为主流的操作GPU的方式,并且前进了一大步。因为程序员再也不用为了每一个GPU编写不同的应用程序。
在OpenGL上面,分开了几个。iOS设备几乎所有的东西变成了Core Animation,但是在OSX,绕过Core Animation而使用Core Graphic 并不是不常见。有一些特别的应用程序,特别是游戏,可能直接使用OpenGL/OpenGL ES. 然后事情变得让人疑惑起来,因为有些渲染Core Animation 使用 Core Graphic。类似AVFoundation, Core Image 这样的框架,或是其他的一些混合的方式。
这里提醒一件事情, GPU是一个强有力的图形图像硬件,在显示像素方面起着核心作用。它也连接着CPU。从硬件方面讲就是有一些总线把他们连接了起来。也有一些框架比如 OpenGL, Core Animation。Core Graphic控制GPU和CPU之间的数据传输。为了让像素能够显示到屏幕上面,有一些工作是需要CPU的。然后数据会被传给GPU,然后数据再被处理,最后显示到屏幕上面。
每一个过程中都有自己的挑战,在这个过程中也存在很多权衡。
这是一个很简单的图表用来描述一个挑战。GPU有纹理(位图)合成为一帧(比如1秒60帧)每一个纹理占用VRAM(显卡)因此GPU一次处理的纹理有大小限制。GPU处理合成方面非常高效,但是有一些合成任务比其他要复杂,所以GPU对处理能力有一个不能超过16.7ms的限制(1秒60帧)。
另一个挑战是把数据传给GPU。为了让GPU能够访问数据,我们需要把数据从内存复制到显存。这个过程叫做上传到GPU。这个可能看上去不重要,但是对于一个大的纹理来说,会非常耗时。
最后CPU运行程序。你可能告诉CPU从资源文件夹中加载一个PNG图片,并解压。这些过程都发生在CPU。当需要显示这些解压的图片时,就需要上传数据到GPU。一些事情看似非常简单,比如显示一段文字,对CPU来说是一个非常复杂的任务。需要调用Core Text 和 Core Graphic框架去根据文字生成一个位图。完成后,以纹理的方式上传到GPU,然后准备显示。当你滑动或是移动一段屏幕上面的文字时,同样的纹理会被重用,CPU会简单的告诉GPU只是需要一个新的位置,所以GPU可以重新利用现有的纹理。CPU不需要重新绘制文字,位图也不需要重新上传到GPU。
上面的有一点复杂,在有一个整体概念之后,我们会开始解释里面的技术细节。
图像合成的字面意思就是把不同的位图放到一起创建成最后的图像然后显示到屏幕上面。在很多方面来看,这个过程都是显而易见的,所以很容易忽视其中的复杂性和运算量。
让我们忽视一些特殊情况,假设屏幕上面都是纹理。纹理就是一个RGBA值的矩形区域。每一个像素包括红,绿,蓝,透明度。在Core Animation世界里面,基本上相当于CALayer。
在这个简单的假设中,每一个层是一个纹理,所有的纹理通过栈的方式排列起来。屏幕上的每一个像素,CPU都需要明白应该如何混合这些纹理,从而得到相对应的RGB值。这就是合成的过程。
如果我们只有一个纹理,而且这个纹理和屏幕大小一致。每一个像素就和纹理中得一个像素对应起来。也就是说这个纹理的像素就是最后屏幕显示的样子。
如果我们有另一个纹理,这个纹理覆盖在之前的纹理上面。GPU需要首先把第二个纹理和第一个纹理合成。这里面有不同的覆盖模式,但是如果我们假设所有的纹理都是像素对齐且我们使用普通的覆盖模式。那么最后的颜色就是通过下面的公式计算出来的。
R = S + D * (1 - Sa)
最后的结果是通过源的颜色(最上面的纹理)加目标颜色(下面的纹理) 乘以(1 – 源颜色的透明度)公式里面所有的颜色就假定已经预先乘以了他们的透明度。
很显然,这里面很麻烦。让我们再假设所有的颜色都是不透明的,也就是alpha = 1. 如果目标纹理(下面的纹理)是蓝色的(RGB = 0,0,1)源纹理(上面的纹理)是红色(RGB = 1,0,0)。因为Sa = 1, 那么这个公式就简化为
结果就是源的红色,这个和你预期一致。
如果源(上面的)层50%透明,比如 alpha = 0,5. 那么 S 的RGB值需要乘以alpha会变成 (0.5,0,0)。这个公式会变成这个样子
R = S + D * (1 - Sa) = 0
+ 0 * (1 - 0.5) = 0
我们最后得到的RGB颜色是紫色(0.5, 0, 0.5) 。这个和我们的直觉预期一致。透明和红色和蓝色背景混合后成为紫色。
要记住,这个只是把一个纹理中的一个像素和另一个纹理中的一个像素合成起来。GPU需要把2个纹理之间覆盖的部分中的像素都合成起来。大家都知道,大多数的app都有多层,因此很多纹理需要被合成起来。这个对GPU的开销很大,即便GPU已经是被高度硬件优化的设备。
不透明 VS 透明
当源纹理是完全不透明,最终的颜色和源纹理一样。这就可以节省GPU的很多工作,因为GPU可以简单的复制源纹理而不用合成所有像素值。但是GPU没有办法区别纹理中的像素是不透明的还是透明。只有程序员才能知道CALayer里面的到底是什么。这也就是CAlayer有opaque属性的原因。如果opaque = YES, 那么GPU将不会做任何合成计算,而是直接直接简单的复制颜色,不管下面还有什么东西。GPU可以减少大量的工作。这就是Instruments(Xcode 的性能测试工具)中 color blended layers 选项做的事情。(这个选项也在模拟器菜单里面)。它可以让你了解哪一个层(纹理)被标记成透明,也就是说,GPU需要做合成工作。合成不透明层要比透明的层工作量少很多,因为没有那么多的数学运算在里面。
如果你知道哪一个层是不透明的,那么一定确保opaque = YES。如果你载入一个没有alpha通道的image,而且在UIImageView显示,那么UIImageView会自动帮你设置opaque = YES。但是需要注意一个没有alpha通道的图片和每个地方的alpha都是100%的图片区别很大。后面的情况,Core Animation 需要假定所有像素的alpha都不是100%。在Finder中,你可以使用Get Info并且检查More Info部分。它将告诉你这张图片是否拥有alpha通道。
像素对齐和不对齐
到目前为止,我们考虑的层都是完美的像素对齐的。当所有的像素都对齐时,我们有一个相对简单的公式。当GPU判断屏幕上面的一个像素应该是什么时,只需要看一下覆盖在屏幕上面的所有层中的单个像素,然后把这些像素合成起来,或者如果最上面的纹理是不透明的,GPU只需要简单的复制最上面的像素就好了。
当一个层上面的所有像素和屏幕上面的像素完美对应,我们就说这个层是像素对齐的。主要有2个原因导致可能不对齐。第一个是放大缩小;当放大或是缩小是,纹理的像素和屏幕像素不对齐。另一个原因是当纹理的起点不在一个像素边界上。
这2种情况,GPU不得不做额外的计算。这个需要从源纹理中混合很多像素来创建一个像素用来合成。当所有像素对齐时,GPU就可以少做很多工作。
注意,Core Animation Instrument和模拟器都有color misaligned images 选项,当CALayer中存在像素不对齐的时候,把问题显示出来。
遮罩(mask)
一个层可以有一个和它相关联的遮罩。遮罩是一个有alpha值的位图,而且在合成像素之前需要被应用到层的contents属性上。当你这顶一个层为圆角时,一就在设置一个遮罩在这个层上面。然而,我们也可以指定一个任意的遮罩。比如我们有一个形状像字母A的遮罩。只有CALayer的contents中的和字母A重合的一部分被会被绘制到屏幕。
离屏渲染(Offscreen rendering)
离屏渲染可以被Core Animation 自动触发或是应用程序手动触发。离屏渲染绘制layer tree中的一部分到一个新的缓存里面(这个缓存不是屏幕,是另一个地方),然后再把这个缓存渲染到屏幕上面。
你可能希望强制离屏渲染,特别是计算很复杂的时候。这是一种缓存合成好的纹理或是层的方式。如果你的呈现树(render tree)是复杂的。那么就希望强制离屏渲染到缓存这些层,然后再使用缓存合成到屏幕。
如果你的APP有很多层,而且希望增加动画。GPU一般来说不得不重新合成所有的层在1秒60帧的速度下。当使用离屏渲染时,GPU需要合成这些层到一个新的位图纹理缓存里面,然后再用这个纹理绘制到屏幕上面。当这些层一起移动时,GPU可以重复利用这个位图缓存,这样就可以提高效率。当然,如果这些层没有修改的化,才能有效。如果这些层被修改了,GPU就不得不重新创建这个位图缓存。你可以触发这个行为,通过设置shouldRasterize = YES
这是一个权衡,如果只是绘制一次,那么这样做反而会更慢。创建一个额外的缓存对GPU来说是一个额外的工作,特别是如果这个位图永远没有被复用。这个实在是太浪费了。然而,如果这个位图缓存可以被重用,GPU也可能把缓存删掉了。所以你需要计算GPU的利用率和帧的速率来判断这个位图是否有用
离屏渲染也可以在一些其他场景发生。如果你直接或是间接的给一个层增加了遮罩。Core Animation 会为了实现遮罩强制做离屏渲染。这个增加了GPU的负担,因为一般上来,这些都是直接在屏幕上面渲染的。
Instrument的Core Animation 有一个叫做Color Offscreen-Rendered Yellow的选项。它会将已经被渲染到屏幕外缓冲区的区域标注为黄色(这个选项在模拟器中也可以用)。同时确保勾选Color Hits Green and Misses Red选项。绿色代表无论何时一个屏幕外缓冲区被复用,而红色代表当缓冲区被重新创建。
一般来说,你需要避免离屏渲染。因为这个开销很大。在屏幕上面直接合成层要比先创建一个离屏缓存然后在缓存上面绘制,最后再绘制缓存到屏幕上面快很多。这里面有2个上下文环境的切换(切换到屏幕外缓存环境,和屏幕环境)。
所以当你打开Color Offscreen-Rendered Yellow后看到黄色,这便是一个警告,但这不一定是不好的。如果Core Animation能够复用屏幕外渲染的结果,这便能够提升性能,当绘制到缓存上面的层没有被修改的时候,就可以被复用了。
注意,缓存位图的尺寸大小是有限制的。Apple 提示大约是2倍屏幕的大小。
如果你使用的层引发了离屏渲染,那么你最好避免这种方式。增加遮罩,设置圆角,设置阴影都造成离屏渲染。
对于遮罩来说,圆角只是一个特殊的遮罩。clipsToBounds 和 masksToBounds 2个属性而已。你可以简单的创建一个已经设置好遮罩的层创建内容。比如,使用已经设置了遮罩的图片。当然,这个也是一种权衡。如果你希望在层的contents属性这只一个矩形的遮罩,那你更应该使用contentsRect而不是使用遮罩。
如果你最后这是shouldRasterize = YES,记住还要设置rasterizationScale = contentsScale
更多的关于合成
通常,维基百科上面有许多关于图像合成的。我们这里简单的拓展一下像素中的红、绿、蓝以及alpha是如何呈现在内存中的。
如果你在OSX上面工作,你会发现大部分的这些调试选项在一个独立的叫做“Quartz Debug”的程序里面。而并不在 Instruments 中。Quartz Debug是Graphics Tools中的一部分,这可以在苹果的developer portal中下载到。
Core Animation & OpenGL ES
就像名字所建议的那样,Core Animation 让我们可以创建屏幕动画。我们将跳过大部分的动画,关注于绘制部分。重要的是,Core Animation允许你坐高效的渲染。这就是为什么你可以通过Core Animation 实现每秒60帧的动画。
Core Animation 的核心就是基于OpenGL ES的抽象。简单说,它让你使用OpenGL ES的强大能力而不需要知道OpenGL ES的复杂性。当我讨论像素合成的时候,我们提到的层(layer)和 纹理(texture)是等价的。他们准确来说不是一个东西,但是缺非常类似。
Core Animation的层可以有多个子层。所以最后形成了一个layer tree。Core Animation做的最复杂的事情就是判断出那些层需要被绘制或重新绘制,那些层需要OpenGL ES 去合成到屏幕上面。
例如,当你这是一个layer的contents属性是一个CGImageRef时,Core Animation创建一个OpenGL 纹理,然后确保这个图片中的位图上传到指定的纹理中。或者,你重写了-drawInContext方法,Core Animation 会分配一个纹理,确保你的Core Graphics的调用将会被作用到这个纹理中。层的 性质和CALayer的子类会影响OpenGL渲染方式的效率。很多底层的OpenGL ES行为被简单的封装到容易理解的CALayer的概念中去。
Core Animation通过Core Graphics和OpenGL ES,精心策划基于CPU的位图绘制。因为Core Animation在渲染过程中处于非常重要的地位,所以如何使用Core Animation,将会对性能产生极大影响。
CPU限制 vs GPU限制(CPU bound vs. GPU bound)
当在屏幕上面显示的时候,有很多组件都参与其中。这里面有2个主要的硬件分别是CPU和GPU。P和U的意思就是处理单元。当东西被显示到屏幕上面是,CPU和GPU都需要处理计算。他们也都受到限制。
为了能够达到每秒60帧的效果,你需要确保CPU和GPU都不能过载。也就是说,即使你当前能达到60fps,你还是要尽可能多的绘制工作交给GPU做。CPU需要做其他的应用程序代码,而不是渲染。通常,GPU的渲染性能要比CPU高效很多,同时对系统的负载和消耗也更低一些。
因为绘制的性能是基于GPU和CPU的。你需要去分辨哪一个是你绘制的瓶颈。如果你用尽的GPU的资源,GPU是性能的瓶颈,也就是绘制是GPU的瓶颈,反之就是CPU的瓶颈。
如果你是GPU的瓶颈,你需要为GPU减负(比如把一些工作交给CPU),反之亦然。
如果是GPU瓶颈,可以使用OpenGL ES Driver instrument,然后点击 i 按钮。配置一下,同时注意查看Device Utilization % 是否被选中。然后运行app。你会看到GPU的负荷。如果这个数字接近100%,那么你交给GPU的工作太多了。
CPU瓶颈是更加通常的问题。可以通过Time Profiler instrument,找到问题所在。
Core Graphics / Quartz 2D
通过Core Graphics这个框架名字,Quartz 2D更被人所知。
Quartz 2D 有很多小功能,我们不会在这里提及。我们不会讲有关PDF创建,绘制,解析或打印。只需要了解答应PDF和创建PDF和在屏幕上面绘制位图原理几乎一致,因为他们都是基于Quartz 2D。
让我们简单了解一下Quartz 2D的概念。更多细节可以参考 Apple 的 。
Quartz 2D是一个处理2D绘制的非常强大的工具。有基于路径的绘制,反锯齿渲染,透明图层,分辨率,并且设备独立等很多特性。因为是更为底层的基于C的API,所以看上去会有一点让人恐惧。
主要概念是非常简单的。UIKit和AppKit都封装了Quartz 2D的一些简单API,一旦你熟练了,一些简单C的API也是很容易理解的。最后你可以做一个引擎,它的功能和Photoshop一样。Apple提到的一个 ,就是一个很好的Quartz 2D例子。
当你的程序进行位图绘制时,不管使用哪种方式,都是基于Quartz 2D的。也就是说,CPU通过Quartz 2D绘制。尽管Quartz可以做其他事情,但是我们这里还是集中于位图绘制,比如在缓存(一块内存)绘制位图会包括RGBA数据。
比方说,我们要画一个八角形,我们通过UIKit能做到这一点
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];
Core Graphics 的代码差不多:
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);
问题就是,绘制到哪里呢? 这就是CGContext做的事情。我们传递的ctx这个参数。这个context定义了我们绘制的地方。如果我们实现了CALayer的-drawInContext:方法。我们传递了一个参数context。在context上面绘制,最后会在layer的一个缓存里面。我们也可以创建我们自己的context,比如 CGBitmapContextCreate()。这个函数返回一个context,然后我们可以传递这个context,然后在刚刚创建的这个context上面绘制。
这里我们发现,UIKit的代码并没有传递context。这是因为UIKit或AppKit的context是隐形的。UIKit和UIKit维护着一个context栈。这些UIKit的方法始终在最上面的context绘制。你可以使用UIGraphicsPushContext()和 UIGraphicsPopContext()来push和pop对应的context。
UIKit有一个简单的方式,通过 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext()来创建一个位图context,和 CGBitmapContextCreate()一样。混合UIKit和 Core Graphics调用很简单。
UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextStrokePath(ctx);
UIGraphicsEndImageContext();
或其他方式
CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo);
CGContextScaleCTM(ctx, 0.5, 0.5);
UIGraphicsPushContext(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path stroke];
UIGraphicsPopContext(ctx);
CGContextRelease(ctx);
通过 Core Graphics可以做很多有趣的事情。苹果的文档有很多例子,我们这里就不太细说他们了。但是Core Graphics有一个非常接近Adobe Illustrator和Adobe Photoshop如何工作的绘图模型,并且大多数工具的理念翻译成Core Graphics了。毕竟这就是NextStep一开始做的。
一件非常值得提起的事,便是CGLayer。它经常被忽视,并且它的名字有时会造成困惑。他不是Photoshop中的图层的意思,也不是Core Animation中的层的意思。
把CGLayer想象成一个子context。它共用父context的所有特性。你可以独立于父context,在它自己的缓存中绘制。并且因为它跟context紧密的联系在一起,CGLayer可以被高效的绘制到context中。
什么时候这将变得有用呢?如果你用Core Graphics来绘制一些相当复杂的,并且部分内容需要被重新绘制的,你只需将那部分内容绘制到CGLayer一次,然后便可绘制这个CGLayer到父context中。这是一个非常优雅的性能窍门。这和我们前面提到的离屏绘制概念有点类似。你需要做出权衡,是否需要为CGLayer的缓存申请额外的内存,确定这是否对你有所帮助。
像素(Pixels)
屏幕上面的像素是通过3个颜色组成的:红,绿,蓝。因此位图数据有时候也被成为RGB数据。你可能想知道这个数据在内存中是什么样子。但是实际上,有非常非常多的方式。
后面我们会提到压缩,这个和下面讲得完全不一样。现在我们看一下RGB位图数据。RGB位图数据的每一个值有3个组成部分,红,绿,蓝。更多的时候,我们有4个组成部分,红,绿,蓝,alpha。这里我们讲4个组成部分的情况。
默认的像素布局
iOS和OS X上面的最通常的文件格式是32 bits-per-pixel (bpp),8 bits-per-component (bpc),alpha会被预先计算进去。在内存里面像这个样子
这个格式经常被叫做ARGB。每一个像素使用4个字节,每一个颜色组件1个字节。每一个像素有一个alpha值在R,G,B前面。最后RGB分别预先乘以alpha。如果我们有一个橘黄的颜色。那么看上去就是 240,99,24. ARGB就是 255,240,99,24 如果我们有一个同样的颜色,但是alpha是0.33,那么最后的就是 ARGB就是 84,80,33,8
另一个常见的格式是32bpp,8bpc,alpha被跳过了:
这个也被称为xRGB。像素并没有alpha(也就是100%不透明),但是内存结构是相同的。你可能奇怪为什么这个格式很流行。因为如果我们把这个没用的字节从像素中去掉,我们可以节省25%的空间。实际上,这个格式更适合现代的CPU和图像算法。因为每一个独立的像素和32字节对齐。现代CPU不喜欢读取不对其的数据。算法会处理大量的位移,特别是这个格式和ARGB混合在一起的时候。
当处理xRGB时,Core Graphic也需要支持把alpha放到最后的格式,比如RGBA,RGBx(RGB已经预先乘以alpha的格式)
深奥的布局
大多数时候,当我们处理位图数据时,我们就在使用Core Graphic 或是 Quartz 2D。有一个列表包括了所有的支持的文件格式。让我们先看一下剩余的RGB格式。
有16bpp,5bpc,不包括alpha。这个格式比之前节省50%空间(2个字节一个像素)。但是如果解压成RGB数据在内存里面或是磁盘上面就有用了。但是,因为只有5个字节一个像素,图像特别是一些平滑的渐变,可能就混合到一起了。(图像质量下降)。
还有一个是64bpp,16bpc,最终为128bpp,32bpc,浮点数组件(有或没有alpha值)。它们分别使用8字节和16字节,并且允许更高的精度。当然,这会造成更多的内存和更复杂的计算。
最后,Core Graphics 也支持一些其他格式,比如CMYK,还有一些只有alpha的格式,比如之前提到的遮罩。
平面数据 (Planar Data color plane)
大多数的框架(包括 Core Graphics)使用的像素格式是混合起来的。这就是所谓的 planar components, or component planes。每一个颜色组件都在内存中的一个区域。比如,对于RGB数据。我们有3个独立的内存空间,分别保存红色,绿色,和蓝色的数值。
在某些情况下,一些视频框架会使用 Planar Data。
YCbCr 是一个常见的视频格式。同样有3个部分组成(Y,Cb,Cr)。但是它更倾向于人眼识别的颜色。人眼是很难精确识别出来Cb和Cr的色彩度。但是却能很容易识别出来Y的亮度。在相同的质量下,Cb和Cr要比Y压缩的更多。
JPEG有时候把RGB格式转换为YCbCr格式。JPEG单独压缩每一个color plane。当压缩YCbCr格式时,Cb和Cr比Y压缩得更好。
iOS和OSX上面的大多数图片都是JPEG和PNG格式。下面我们再了解一下。
每个人都知道JPEG,他来自相机。他代表了图片是图和存储在电脑里,即时是你的妈妈也听过JPEG。
大家都认为JPEG就是一个像素格式。就像我们之前提到的RGB格式一样,但是实际上并不是这样。
真正的JPEG数据变成像素是一个非常复杂的过程。一个星期都没有办法讲清楚,或是更久。对于一个color plane, JPEG使用一种离散余弦变换的算法。讲空间信息转换为频率(convert spatial information into the frequency domain)。然后通过哈夫曼编码的变种来压缩。一开始会把RGB转换成YCbCr,解压缩的时候,再反过来。
这就是为什么从一个JPEG文件创建一个UIImage然后会知道屏幕上面会有一点点延迟的原因。因为CPU正在忙于解压图片。如果每个TableViewCell都需要解压图片的话,那么你的滚动效果就不会平滑。
那么,为什么使用JPEG文件呢?因为JPEG可以把图片压缩的非常非常好。一个没有压缩过的IPhone5拍照的图片差不多24MB。使用默认的压缩设置,这个只有2-3MB。JPEG压缩效果非常好,因为几乎没有损失。他把那些人眼不能识别的部分去掉了。这样做可以远远的超过gzip这样的压缩算法。但是,这个仅仅在图片上面有效。因为,JPEG依赖于丢掉那些人眼无法识别的数据。如果你从一个基本是文本的网页截取一张图片,JPEG就不会那么高效,压缩效率会变得低下。你甚至都可以看出图片已经变形了。
PNG读作“ping”,和JPEG相反,他是无损压缩的。当你保存图片成PNG时,然后再打开。所有的像素数据和之前的完全一样。因为有这个限制,所有PNG压缩图片的效果没有JPEG那么好。但是对于app中的设计来说,比如按钮,icon,PNG就非常适合。而且PNG的解码工作要比JPEG简单很多。
在真实的世界里面,事情没有这么简单。有很多不同的PNG格式。维基百科上面有很多细节。但是简单说,PNG支持压缩有alpha或是没有alpha通道的RGB像素,这也就是为什么他适合app上面的原因。
当在app中使用颜色是,你需要使用者2种格式中得一个,PNG和JPEG。他们的解码和压缩算法都是被高度硬件优化的。有些情况甚至支持并行计算。同时Apple也在不断地提高解码的能力在未来的操作系统版本中。如果使用其他格式,这可能会对你的程序性能产生影响,而且可能会产生漏洞,因为图像解码的算法是黑客们最喜欢攻击的目标。
已经讲了好多有关的优化了,你可以在互联网上面自己查找。这里需要注意一点,Xcode的压缩算法和大部分的压缩引擎不一样。
当Xcode压缩png时,技术上来说,已经不是一个有效的PNG文件了。但是iOS系统可以读取这个文件,然后比通常的PNG图片处理速度更快。Xcode这样做,是为了更好地利用解码算法,而这些解码算法不能在一般的PNG文件上面适用。就像上面提到的,有非常多的方法去表示RGB数据。而且如果这个格式不是iOS图形图像系统需要的,那么就需要增加额外的计算。这样就不会有性能上的提高了。
再抢到一次,如果你可以,你需要设置 resizable images。你的文件会变得更小,因此,这样就会有更小的文件需要从文件系统里面读取,然后在解码。
UIKit and Pixels
UIKit中得每一个view都有自己的CALayer,一般都有一个缓存,也就是位图,有一点类似图片。这个缓存最后会被绘制到屏幕上面。
With -drawRect:
如果你的自定义view的类实现了-drawRest:,那么就是这样子工作的:
当你调用-setNeedsDisplay时,UIKit会调用这个view的层的 -setNeedsDisplay方法。这个设置一个标记,表明这个层已经脏了(dirty,被修改了)。实际上,并没有做任何事情,所以,调用多次-setNeedsDisplay 没有任何问题。
当渲染系统准备好后,会调用层的-display方法。这时,层会设置缓存。然后设置缓存的Core Graphics的上下文环境(CGContextRef)。后面的绘制会通过这个CGContextRef绘制到缓存中。
当你调用UIKit中的函数,比如UIRectFill() 或者 -[UIBezierPath fill]时,会通过这个CGContextRef调用你的drawRect方法。他们是通过把上面的CGContextRef push 到 图形图像堆栈中,也就是设置成当前的上下文环境。UIGraphicsGetCurrent()会返回刚才push的那个context。由于UIKit绘制方法使用UIGraphicsGetCurrent(),所以这些绘制会被绘制到缓存中。如果你希望直接使用 Core Graphics 方法,那么你需要调用UIGraphicsGetCurrent()方法,然后自己手动传递context参数到Core Graphics的绘制函数中去。
那么,一个个层的缓存都会被绘制到屏幕上面,知道下一次设置-setNeedsDisplay,然后再重新更新缓存,再重复上面的过程。
不使用 drawRect
当你使用UIImageView的时候,有一点点的不同。这个view依然包含一个CALayer,但是这个层并不会分配一个缓存空间。而是使用CGImageRef作为CALayer的contents属性,渲染系统会把这个图片绘制到帧的缓存,比如屏幕。
这个情况下,就没有继续绘制的过程了。我们就是简单的通过传递位图这种方式把图片传递给UIImageView,然后传递给Core Animation,然后传递给渲染系统。
使用drawRect 还是不使用drawRect
听上去不怎么样,但是,最快速的方法,就是不使用。
大多数情况,你可以通过自定义view或是组合其他层来实现。可以看一下Chris的文章,有关。这个方法是推荐的,因为UIKit非常高效。
当你需要自定义绘制的时候 是一个非常好的例子 。
另一个地方需要自定义绘制的是iOS的股票软件。这个股票图是通过Core Graphics实现的。注意,这个只是你需要自定义绘制,并不是一定要实现drawRect函数,有时候通过UIGraphicsBeginImageContextWithOptions()或是 CGBitmapContextCreate()创建一个额外的位图,然后再上面绘制图片,然后传递给CALayer的contents会更容易。下面有一个测试
这是一个简单的例子
// Don't do this
- (void)drawRect:(CGRect)rect
[[UIColor redColor] setFill];
UIRectFill([self bounds]);
我们知道为什么这样做很烂,我们让Core Animation创建了一个额外的缓存,然后我们让Core Graphics 在缓存上面填充了一个颜色。然后上传给了GPU。
我们可以不实现-drawRect:函数来省去这些步骤。只是简单的设置view的backgroundColor就好了。如果这个view有CAGradientLayer,那么同样的方法也可以设置成渐变的颜色。
可变大小的图片(resizable image)
你可以简单的通过可变大小的图片减少图形系统的工作压力。如果你原图上面的按钮大小是300*50。那么就有 600 * 100 = 60k 像素 * 4 = 240KB的内存数据需要传递给GPU。传递给显存。如果我们使用resizable image。我们可以使用一个 52 * 12 大小的图片,这样可以节省10kb的内存。这样会更快。
Core Animation 通过 contentsCenter 来resize图片,但是,更简单的是通过 -[UIImage resizableImageWithCapInsets:resizingMode:]。
而且,在第一次绘制的时候,我们并不需要从文件系统读取60K像素的PNG文件,然后解码。越小的图片解码越快。这样,我们的app就可以启动的更快。
上一个我们讲到了并发。UIKit的线程模型非常简单,你只能在主线程使用UIKit。所以,这里面还能有并发的概念?
如果你不得不实现-drawRect:,并且你必须绘制大量的东西,而这个会花费不少时间。而且你希望动画变得更平滑,除了在主线程中,你还希望在其他线程中做一些工作。并发的绘图是复杂的,但是除了几个警告,并发的绘图还是比较容易实现的。
你不能在CAlayer的缓存里面做任何事情出了主线程,否则不好的事情会发生。但是你可以在一个独立的位图上面绘制。
所有的Core Graphics的绘制方法需要一个context参数,指定这个绘制到那里去。UIKit有一个概念是绘制到当前的context上。而这个当前的context是线程独立的。
为了实现异步绘制,我们做下面的事情。我们在其他队列(queue,GCD中的概念)中创建一个图片,然后我们切换到主队列中把结果传递给UIImageView。这个技术被 中提到
- (UIImage *)renderInImageOfSize:(CGSize)
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
// do drawing here
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
这个函数通过UIGraphicsBeginImageContextWithOptions 创建一个新的CGContextRef。这个函数也修改了当前的UIKit的context。然后可以铜鼓UIKit的方法绘制,然后通过UIGraphicsGetImageFromCurrentImageContext()根据位图数据生成一个UIImage。然后关闭掉创建的这个context。
保证线程安全是非常重要的,比如你访问UIKit的属性,必须线程安全。如果你在其他队列调用这个方法,而这个方法在你的view类里面,这个事情就可能古怪了。更简单的方法是创建一个独立的渲染类,然后当触发绘制这个图片的时候才设置这些必须得属性。
但是UIKit的绘制函数是可以在其他队列中调用的,只需要保证这些操作在 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext ()之前就好。
你可以通过下面的方法触发绘制
UIImageView * // assume we have this
NSOperationQueue *renderQ // assume we have this
CGSize size = view.bounds.
[renderQueue addOperationWithBlock:^(){
UIImage *image = [renderer renderInImageOfSize:size];
[[NSOperationQueue mainQueue] addOperationWithBlock:^(){
view.image =
注意view.image = 必须在主队列调用。这是非常重要的细节。你不能在其他队列中调用。
通常来说,异步绘制会带来很多复杂度。你需要实现取消绘制的过程。你还需要限制异步操作的最大数目。
所以,最简单的就是通过NSOperation的子类来实现renderInImageOfSize方法。
最后,有一点非常重要的就是异步设置UITableViewCell 的content有时候很诡异。因为当异步绘制结束的时候,这个Cell很可能已经被重用到其他地方了。
CALayer的奇怪和最后
现在你是到了CALayer某种程度上很像GPU中的纹理。层有自己的缓存,缓存就是一个会被绘制到屏幕上的位图。 大多数情况,当你使用CALayer时,你会设置contents属性给一个图片。这个意思就是告诉 Core Animation,使用这个图片的位图数据作为纹理。 如果这个图片是PNG或JPEG,Core Animation 会解码,然后上传到GPU。
当然,还有其他种类的层,如果你使用CALayer,不设置contents,而是这事background color, Core Animation不会上传任何数据给GPU,当然这些工作还是要被GPU运算的,只是不需要具体的像素数据,同理,渐变也是一个道理,不需要把像素上传给GPU。
图层和自定义绘制
如果CALayer或是子类实现了 -drawInContext 或是-drawLayer:inContext delegate。Core Animation会为这个layer创建一个缓存,用来保存这些函数中绘制的结果。这些代码是在CPU上面运行的,结果会被传递给GPU。
形状和文本层(Shape and Text Layers)
形状和文本层会有一点不同。首先,Core Animation 会为每一个层生成一个位图文件用来保存这些数据。然后Core Animation 会绘制到layer的缓存上面。如果你实现了-drawInContext方法,结果和上面提到的一样。最后性能会受到很大影响。
当你修改形状层或是文本层导致需要更新layer的缓存时,Core Animation会重新渲染缓存,比如。当实现shape layer的大小动画时,Core Animation会在动画的每一帧中重新绘制形状。
CALayer 有一个属性是 drawsAsynchronously。这个似乎看上去很不错,可以解决所有问题。实际上虽然可能会提高效率,但是可能会让事情更慢。
当你设置 drawsAsynchronously = YES 后,-drawRect: 和 -drawInContext: 函数依然实在主线程调用的。但是所有的Core Graphics函数(包括UIKit的绘制API,最后其实还是Core Graphics的调用)不会做任何事情,而是所有的绘制命令会被在后台线程处理。
这种方式就是先记录绘制命令,然后在后台线程执行。为了实现这个过程,更多的事情不得不做,更多的内存开销。最后只是把一些工作从主线程移动出来。这个过程是需要权衡,测试的。
这个可能是代价最昂贵的的提高绘制性能的方法,也不会节省很多资源。
转载自: -
你可能感兴趣的文章
2 收藏,111 浏览
6 收藏,1k 浏览
21 收藏,1.8k 浏览
本文隶属于专栏
新闻资讯一手掌握
分享到微博?
与我们一起探索更多的未知
专业的开发者技术社区,为用户提供多样化的线上知识交流,丰富的线下活动及给力的工作机会
加入只需一步
举报理由:
推广(招聘、广告、SEO 等)方面的内容
带有人身攻击、辱骂、仇恨等违反条款的内容
与已有问题重复(请编辑该提问指向已有相同问题)
不友善内容
答非所问,不符合答题要求
其他原因(请补充说明)
补充说明:
扫扫下载 App
SegmentFault
一起探索更多未知

我要回帖

更多关于 opengl 渲染到纹理 的文章

 

随机推荐