GPUImage(四):你们处理的都是GPUImageFramebuffer类型

GPUImage作为iOS相当老牌的图片处理三方库已经有些日子了(2013年发布第一个版本),至今甚至感觉要离我们慢慢远去(2015年更新了最后一个release)。可能现在分享这个稍微有点晚,再加上落影大神早已发布过此类文章,但是还是想从自己的角度来分享一下对其的理解和想法。

本文集所有内容皆为原创,严禁转载。


    在使用GPUImage处理图片或者视频时,并不能直接对iOS官方定义的的UIImage、CGImage、CIImage进行操作。那么如果要使用GPUImage的话,第一步操作就是把你想处理的数据类型用一个GPUImage定义的载体承载,才能在GPUImage的处理链中进行一步一步的操作,这个载体就是GPUImageFramebuffer。我们把一张图像的像素等携带的全部信息看成好几种散装液体颜料的集合,系统提供了UIImage(最常用的图像类型)相当于一个颜料盒子,想把图片显示在iPhone设备屏幕上的话首先就是要把散装颜料装到这个颜料盒子里。GPUImageFramebuffer就是GPUImage中的颜料盒子,所以要先把UIImage盒子中的颜料倒到GPUImageFramebuffer中,才可以用GPUImage对这些颜料进行再次调色。调色的过程之前的文章中比喻成了一个多维度的管道,因此,在处理过程中就像GPUImageFramebuffer带着颜料在管子里流动,经过一个节点处理完后会有一个新盒子把调好色的颜料接住再往下流动。

GPUImageFramebuffer

@property(readonly) CGSize size;  //颜料盒子的大小,盒子创建的时候你要知道需要用一个多大的盒子才能刚好容纳这些颜料

@property(readonly) GPUTextureOptions textureOptions;  //用于创建纹理时的相关设置

@property(readonly) GLuint texture; //纹理对象指针

@property(readonly) BOOL missingFramebuffer; //这个属性的设置就涉及到framebuffer和texture的关系,此处先不细说。GPUImage中将texture作为framebuffer对象的一个属性实现两者关系的绑定。若missingFramebuffer为YES,则对象只会生成texture,例如GPUImagePicture对象的图像数据就不需要用到framebuffer,只要texture即可;若为NO,则会先生成framebuffer对象,再生成texture对象,并进行绑定。

- (id)initWithSize:(CGSize)framebufferSize; //设置buffer大小初始化,texture设置为默认,又创建framebuffer又创建texture对象。

- (id)initWithSize:(CGSize)framebufferSize textureOptions:(GPUTextureOptions)fboTextureOptions onlyTexture:(BOOL)onlyGenerateTexture;

- (id)initWithSize:(CGSize)framebufferSize overriddenTexture:(GLuint)inputTexture;  //自己创建好texture替代GPUImageFramebuffer对象初始化时创建的texture

- (void)activateFramebuffer; //绑定frame buffer object才算是创建完成,也就是FBO在使用前,一定要调用此方法。

- (void)lock; 

- (void)unlock; 

- (void)clearAllLocks;

- (void)disableReferenceCounting;

- (void)enableReferenceCounting; //以上方法涉及framebuffer对象的内存管理,之后会具体说明。开发时基本不会手动调用以上方法。

- (CGImageRef)newCGImageFromFramebufferContents;  //从framebuffer中导出生成CGImage格式图片数据

- (void)restoreRenderTarget; 

- (void)lockForReading;

- (void)unlockAfterReading; //以上方法涉及到GPUImageFramebuffer对象管理自身生成的用于存储处理后的图像数据CVPixelBufferRef对象。

- (NSUInteger)bytesPerRow; //返回CVPixelBufferRef类型对象实际占用内存大小

- (GLubyte *)byteBuffer; //返回CVPixelBufferRef类型对象

从以上GPUImageFramebuffer.h的内容可看到,主要内容分为三大部分:1.framebuffer及texture的创建。2.GPUImage中GPUImageFramebuffer类型对象的内存管理。3.实际操作过程中数据存储的CVPixelBufferRef类型对象的具体操作。接下来就来看一下这三点中涉及到的具体类型到底是个啥以及在GPUImage框架中做了哪些事。

1.framebuffer及texture

·texture

百度百科中对纹理的解释:

一般说来,纹理是表示物体表面的一幅或几幅二维图形,也称纹理贴图(texture)。当把纹理按照特定的方式映射到物体表面上的时候,能使物体看上去更加真实。当前流行的图形系统中,纹理绘制已经成为一种必不可少的渲染方法。在理解纹理映射时,可以将纹理看做应用在物体表面的像素颜色。在真实世界中,纹理表示一个对象的颜色、图案以及触觉特征。纹理只表示对象表面的彩色图案,它不能改变对象的几何形式。更进一步的说,它只是一种高强度的计算行为。

在大学时期有一门计算机图形课,主要是在Windows上使用C进行OpenGL开发。当时我做了一架直升飞机,虽然具体如何开发OpenGL现在已经有点陌生,但是其中印象非常深刻的有两个地方:1.只能画三角形,正方形是通过两个三角形组成的,圆形是有非常多个三角形组成的,三角形越多,锯齿越不明显。2.画好各种形状组成三维图形后可以往图形上面贴图,就好像罐头的包装一样,刚做好的罐头其实只是一个铝制或者其他材料制成的没有任何图案的圆柱体,出产前最后一道工序就是给罐头贴上一圈纸或者喷上相应的图案。最后出厂运往各个卖场,我才能买到印有“某巢”以及各个信息在罐子上的奶粉。纹理就是贴在上面的纸或者印在上面的图案。

GPUImageFramebuffer中通过调用下面的方法创建texture,具体实现其实和OpenGL ES一模一样。

- (void)generateTexture;

{

glActiveTexture(GL_TEXTURE1);  //纹理单元相当于显卡中存放纹理的格子,格子有多少取决于显卡贵不贵。此方法并不是激活纹理单元,而是选择当前活跃的纹理单元。

glGenTextures(1, &_texture); //生成纹理,第二个参数为texture的地址,生成纹理后texture就指向纹理所在的内存区域。

glBindTexture(GL_TEXTURE_2D, _texture);  //将上方创建的纹理名称与活跃的纹理单元绑定,个人理解为暂时的纹理单元命名。

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _textureOptions.minFilter);//当所显示的纹理比加载进来的纹理小时,采用GL_LINEAR的方法来处理

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _textureOptions.magFilter);//当所显示的纹理比加载进来的纹理大时,采用GL_LINEAR的方法来处理

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _textureOptions.wrapS);

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _textureOptions.wrapT);

//以上配置texture的参数都存放在GPUTextureOptions结构体中,使用时候如果有特殊要求,也可以自己创建然后通过初始化方法传入。

}

注:以上源码中最后两行的注释

// This is necessary for non-power-of-two textures

Non-Power-of-Two Textures译为无二次幂限制的纹理。大概意思是纹理大小可以不等于某个数的二次幂。

纹理的WRAP设置解释如下(截图内容来自:http://blog.csdn.net/wangdingqiaoit/article/details/51457675)


·framebuffer

字面上的意思是帧缓存,在OpenGL中,帧缓存的实例被叫做FBO。个人理解:如果texture是罐头上的内容,那framebuffer就是那张裹在罐头上的纸,它负责对纹理内容进行缓存并渲染到屏幕上,这个过程就叫做render to texture。当然framebuffer中不仅可以缓存原始的纹理内容,还可以是经过OpenGL处理后的内容。比如照片中牛奶罐头上除了有本身的名字、图案和说明这些内容,我们看上去还有相应的质感和反光效果,这个就可以通过获取反光中的实际影像进行透明、拉伸、雾化等效果处理后得到与实际反光一模一样的图,最终渲染到牛奶罐上。

从这个角度理解的话,可以把texture看作是framebuffer的一部分,因此在GPUImage中把指向texture的地址作为GPUImageFramebuffer的一个属性。当然也有不需要创建frambuffer的情况,比如在初始化输入源时例如GPUImagePicture类型对象,载入图片资源时只需要创建texture即可,GPUImageFramebuffer的其中一个初始化方法的其中一个参数onlyGenerateTexture为YES时,就可以只创建texture,把图片信息转化成纹理,而没有进行framebuffer的创建。

glBindTexture(GL_TEXTURE_2D, _texture); //将一个命名的纹理绑定到一个纹理目标上,当把一张纹理绑定到一个目标上时,之前对这个目标的绑定就会失效。当一张纹理被第一次绑定时,它假定成为指定的目标类型。例如,一张纹理若第一次被绑定到GL_TEXTURE_1D上,就变成了一张一维纹理;若第一次被绑定到GL_TEXTURE_2D上,就变成了一张二维纹理。当使用glBindTexture绑定一张纹理后,它会一直保持活跃状态直到另一张纹理被绑定到同一个目标上,或者这个被绑定的纹理被删除了(使用glDeleteTextures)。

函数具体参数解释:http://www.dreamingwish.com/frontui/article/default/glbindtexture.html

glTexImage2D(GL_TEXTURE_2D, 0, _textureOptions.internalFormat, (int)_size.width, (int)_size.height, 0, _textureOptions.format, _textureOptions.type, 0); //用来指定二维纹理和立方体纹理。

函数具体参数的解释:http://blog.csdn.net/csxiaoshui/article/details/27543615

glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, _texture, 0); //参数GL_COLOR_ATTACHMENT0是告诉OpenGLES把纹理对像绑定到FBO的0号绑定点(一个FBO在同一个时间内可以绑定多个颜色缓冲区,每个对应FBO的一个绑定点),参数GL_TEXTURE_2D是指定纹理的格式为二维纹理,_texture保存的是纹理标识,指向一个之前就准备好了的纹理对像。纹理可以是多重映射的图像,最后一个参数指定级级为0,指的是使用原图像。

glBindTexture(GL_TEXTURE_2D, 0); //这是在创建framebuffer并绑定纹理的最后一步,个人理解是在绑定之后framebuffer已经获得texture,需要释放framebuffer对texture的引用

有创建就有销毁,GPUImageFramebuffer有一个私有方法- (void)destroyFramebuffer,在dealloc时调用。其中主要的操作是:

glDeleteFramebuffers(1, &framebuffer);

framebuffer = 0;


2.GPUImage对GPUImageFramebuffer类型对象的管理


相信很多使用过GPUImage都会有同样的经历,都会遇到这个断言。以下就拿具体案例来解释GPUImage对GPUImageFramebuffer到底做了哪些事情。场景是:创建一个GPUImagePicture对象pic,一个GPUImageHueFilter对象filter和一个GPUImageView对象imageView,实现效果是pic通过filter处理后的效果显示在imageView上。具体的处理过程就不过多说明,我们来主要看一下GPUImageFramebuffer在整个处理过程中发生的情况。

·GPUImagePicture对象pic中的framebuffer

在对初始化传入的UIImage对象进行一系列处理后,pic进行自身的framebuffer的获取:

可以看到,framebuffer对象并不是通过初始化GPUImageFramebuffer来创建的,而是通过调用单例[GPUImageContext sharedFramebufferCache]的其中一个方法得到的。sharedFramebufferCache其实是一个普通的NSObject子类对象,并不具有像NSCache对象对数据缓存的实际处理功能,不过GPUImage通过单例持有这个sharedFramebufferCache对象来对过程中产生的framebuffer进行统一管理。获取framebuffer的方法有两个入参:1.纹理大小,在pic中的这个大小正常情况下是传入图片的初始大小,当然还有不正常情况之后再说。2.是否返回只有texture的的framebuffer对象。

第一步:

传入的纹理大小,textureOptions(默认)、是否只要texture调用sharedFramebufferCache的- (NSString *)hashForSize:(CGSize)size textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture;方法得到一个用于查询的唯一值lookupHash。由此可见如果纹理大小一致的GPUImagePicture对象获取到的lookupHash是相同的。

第二步:

lookupHash作为key,在sharedFramebufferCache的一个字典类型的属性framebufferTypeCounts获取到numberOfMatchingTexturesInCache,如字面意思,在缓存中满足条件的GPUImageFramebuffer类型对象的个数。转换为整数类型为numberOfMatchingTextures

第三步:

如果numberOfMatchingTexturesInCache小于1也就是没找到的话,就调用GPUImageFramebuffer的初始化方法创建一个新的framebuffer对象。否则:将lookupHash和(numberOfMatchingTextures - 1)拼接成key从sharedFramebufferCache的另一个字典类型的属性framebufferCache中获取到framebuffer对象。再更新framebufferTypeCounts中numberOfMatchingTexturesInCache数值(减1)

第四步:

以防最后返回的framebuffer对象为nil,最后做了判断如果为nil的话就初始化创建一个。

第五步:

调用作为返回值的framebuffer对象的- (void)lock;方法。主要是对framebufferReferenceCount进行+1操作。framebufferReferenceCount为framebuffer的一个属性,初始化时候为0,作用也是字面意思:对象被引用的次数。此时需要进行+1操作是因为framebuffer对象即将被获取它的pic引用并将图片内容载入。

但是!与GPUImageFilter类型对象不同的是在pic获取到framebuffer后,进行了[outputFramebuffer disableReferenceCounting];。这个方法里将framebuffer的referenceCountingDisabled设置为YES。而这个属性的值在- (void)lock;方法中又会导致不一样的结果。如果referenceCountingDisabled为YES的话将不会对framebufferReferenceCount进行+1操作。

- (void)lock;

{

if (referenceCountingDisabled)

{

return;

}

framebufferReferenceCount++;

}

然而问题就出在这里,在pic获取到framebuffer之前,从sharedFramebufferCache找到framebuffer后就对它调用了- (void)lock;方法,此时pic获取到的framebuffer对象的framebufferReferenceCount已经被+1,而referenceCountingDisabled是在这之后才设置为YES,从而导致在pic对象dealloc时候自身的outputFramebuffer属性并未得到释放从而引起内存泄漏。为了解决这个问题我写了一个GPUImagePicture的caterogy,重写dealloc方法,将原本的[outputFramebuffer unlock];替换成[outputFramebuffer clearAllLocks];,保证outputFramebuffer的framebufferReferenceCount被重置为0,从而保证outputFramebuffer能顺利释放。

·GPUImageHueFilter对象filter中的framebuffer

我们以结构最简单的单输入源滤镜对象作为例子,通过GPUImageHueFilter对象filter。filter相比pic来说相同点是:自身都会通过sharedFramebufferCache获取到一个framebuffer用来存储经过自身处理后的数据并传递给下一个对象。不同点是:filter有一个firstInputFramebuffer变量,作用是引用上一个节点的outputFramebuffer。如果是继承自GPUImageTwoInputFilter的滤镜对象来说,它的成员变量将会多一个secondInputFramebuffer。若想进行到filter这,必须将filter作为pic的target,并调用pic的processImage方法。filter中方法调用顺序是:

1.对输入的framebuffer引用并调用lock方法。假设pic的framebuffer为初始化创建的,传入前framebufferReferenceCount为1,经过此方法后framebufferReferenceCount则为2

- (void)setInputFramebuffer:(GPUImageFramebuffer *)newInputFramebuffer atIndex:(NSInteger)textureIndex {

firstInputFramebuffer = newInputFramebuffer;

[firstInputFramebuffer lock];

2.filter的处理操作,也就是framebuffer的渲染纹理。这里就复杂了,那就不涉及到具体的渲染过程了。

- (void)renderToTextureWithVertices:(const GLfloat *)vertices textureCoordinates:(const GLfloat *)textureCoordinates

filter的outputFramebuffer与pic一样,都是通过shareFramebufferCache查找获得。因此,outputFramebuffer变量被赋值时framebuffer的framebufferReferenceCount已经为1。接下来有一个判断条件:usingNextFrameForImageCapture。在类中全局搜一下,发现在调用- (void)useNextFrameForImageCapture;方法时会将usingNextFrameForImageCapture设置为YES。那么什么时候会调用这个方法呢?有用GPUImage写过最简单的离屏渲染功能实现的都会觉得对这个方法有点眼熟,那就是在filter处理完后导出图片前必须要调用这个方法。为啥呢?原因就在这个判断条件这里,如果usingNextFrameForImageCapture为YES,那么outputFramebuffer要再lock一次,就是为了保证在处理完成后还需要引用outputFramebuffer,从而才可以从中生成图片对象,否则就被回收进shareFramebufferCache啦。

进行过一顿操作后,最后会调用输入源的unlock方法。这时候firstInputFramebuffer的framebufferReferenceCount按照正常情况的话将会为0,就会被添加到shareFramebufferCache中。

[firstInputFramebuffer unlock];

3.接下来执行的方法中会把自身的outputFramebuffer传递给链中的下一个节点,就像pic到filter的过程一样。

- (void)informTargetsAboutNewFrameAtTime:(CMTime)frameTime;

这里的[self framebufferForOutput]在正常的filter中会返回outputFramebuffer。如果usingNextFrameForImageCapture为YES的话,可以简单理解当前对象的outputFramebuffer传递给下一个target后还有其他用处的话就不置空outputFramebuffer变量。如果usingNextFrameForImageCapture为NO的话,当前outputFramebuffer被置为nil,但是原先outputFramebuffer指向的framebuffer并不会被回收到shareFramebufferCache。原因是:framebuffer已经传递给下个target,在相应赋值方法中对framebuffer调用了lock方法。周而复始,直到最后一个节点,要么生成图片,要么显示。

推荐阅读更多精彩内容