GPUImage源码解读之GPUImageFramebufferCache

简介

由于GPUImage添加滤镜可以形成一个FilterChain,因此,在渲染的过程中,可能会需要很多个FrameBuffer,但是正如上文所说,每生成一个FrameBuffer都需要占用一定的内存或者显存。因此,必须保证尽可能少创建FrameBuffer。而GPUImageFrameBufferCache就是用来管理所有的FrameBuffer的。

根据上面对GPUImageFrameBuffer的介绍,每个FrameBuffer其实就是一块内存或者缓存,因此只要它们的size和textureOption是一样的,那么这个FrameBuffer就是完全可以重用的。

一般来说,GPUImageFrameBufferCache可以创建多个,一般每一个GPUImageContext中会有一个公用的GPUImageFrameBufferCache。通过这个Cache可以获得对应的GPUImageContext中得到对应的FrameBuffer对象。

重用过程如下:

  • 首先就是要使用size和textureOptions生成一个Key:
- (NSString *)hashForSize:(CGSize)size textureOptions:(GPUTextureOptions)textureOptions onlyTexture:(BOOL)onlyTexture;
{
    if (onlyTexture)
    {
        return [NSString stringWithFormat:@"%.1fx%.1f-%d:%d:%d:%d:%d:%d:%d-NOFB", size.width, size.height, textureOptions.minFilter, textureOptions.magFilter, textureOptions.wrapS, textureOptions.wrapT, textureOptions.internalFormat, textureOptions.format, textureOptions.type];
    }
    else
    {
        return [NSString stringWithFormat:@"%.1fx%.1f-%d:%d:%d:%d:%d:%d:%d", size.width, size.height, textureOptions.minFilter, textureOptions.magFilter, textureOptions.wrapS, textureOptions.wrapT, textureOptions.internalFormat, textureOptions.format, textureOptions.type];
    }
}
  • 第二步是根据这个生成的key,查询在cache里面有多少个满足这个条件的FrameBuffer可用。在GPUImageFrameBufferCache中,包含了两个Dictionary:
NSMutableDictionary *framebufferCache;
NSMutableDictionary *framebufferTypeCounts;

其中framebufferTypeCounts是保存了满足当前size和textureOptions生成的key的FrameBuffer个数,key就是上面生成的hashKey;而framebufferCache则是保存的每个Texture对象,key是上面生成的hashKey+“-i”;比如满足当前size和textureOptions的FrameBuffer有5个,则在framebufferCache里面会有haskey-0~hashkey-4这些key和对应的FrameBuffer。

因此,查询的过程是:

  1. 使用HashKey查询到满足条件的FrameBuffer个数:
NSString *lookupHash = [self hashForSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
NSNumber *numberOfMatchingTexturesInCache = [framebufferTypeCounts objectForKey:lookupHash];
NSInteger numberOfMatchingTextures = [numberOfMatchingTexturesInCache integerValue];
  1. 如果个数为零,则生成一个新的FrameBuffer并且返回:
framebufferFromCache = [[GPUImageFramebuffer alloc] initWithSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
  1. 如果有满足条件的FrameBuffer,则获取index最大的一个Key对应的FrameBuffer,并且分别更新两个FrameBuffer对应的Key和Value
  NSInteger currentTextureID = (numberOfMatchingTextures - 1)
  while ((framebufferFromCache == nil) && (currentTextureID >= 0))
{
     NSString *textureHash = [NSString stringWithFormat:@"%@-%ld", lookupHash, (long)currentTextureID];
     framebufferFromCache = [framebufferCache objectForKey:textureHash];
     if (framebufferFromCache != nil)  {
          [framebufferCache removeObjectForKey:textureHash];
    }
     currentTextureID--;
}
 currentTextureID++;
 [framebufferTypeCounts setObject:[NSNumber numberWithInteger:currentTextureID] forKey:lookupHash];
            
  if (framebufferFromCache == nil) {
     framebufferFromCache = [[GPUImageFramebuffer alloc] initWithSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
 }
  1. 在返回FrameBuffer之前,需要将FrameBuffer进行一次lock,增加引用计数。
  2. 当一个FrameBuffer的引用计数为0的时候,我们就会将这个FrameBuffer重新放置到Cache中以便重用。

思考

我们为什么要用cache里的framebuffer呢?自己创建一个,使用完后再释放行不行呢?

答案显示是NO。

我们来看一下GPUImageFramebuffer类的代码,在dealloc中,调用了destroyFramebuffer方法,这个方法的实现如下。

- (void)destroyFramebuffer;
{
    runSynchronouslyOnVideoProcessingQueue(^{
        [GPUImageContext useImageProcessingContext];
        
        if (framebuffer)
        {
            glDeleteFramebuffers(1, &framebuffer);
            framebuffer = 0;
        }
 
        
        if ([GPUImageContext supportsFastTextureUpload] && (!_missingFramebuffer))
        {
#if TARGET_IPHONE_SIMULATOR || TARGET_OS_IPHONE
            if (renderTarget)
            {
                CFRelease(renderTarget);
                renderTarget = NULL;
            }
            
            if (renderTexture)
            {
                CFRelease(renderTexture);
                renderTexture = NULL;
            }
#endif
        }
        else
        {
            glDeleteTextures(1, &_texture);
        }
 
    });
}

问题就出在其中的renderTarget上,当创建GPUImageFramebuffer时给onlyTexture参数填NO(一般就是填NO的)时,会创建一个CVPixelBufferRef类型的变量renderTarget,当用CFRelease去释放这个变量时,它占用的内存并不会立即释放,而是要调用

CVOpenGLESTextureCacheFlush([[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache], 0);

之后,才会真正释放内存。这个现象的原因可以在GPUImageFrameBuffer的init函数中找到。

CVOpenGLESTextureCacheRef coreVideoTextureCache = [[GPUImageContext sharedImageProcessingContext] coreVideoTextureCache];
            // Code originally sourced from http://allmybrain.com/2011/12/08/rendering-to-a-texture-with-ios-5-texture-cache-api/
            
            CFDictionaryRef empty; // empty value for attr value.
            CFMutableDictionaryRef attrs;
            empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // our empty IOSurface properties dictionary
            attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
            CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty);
            
            CVReturn err = CVPixelBufferCreate(kCFAllocatorDefault, (int)_size.width, (int)_size.height, kCVPixelFormatType_32BGRA, attrs, &renderTarget);
            if (err)
            {
                NSLog(@"FBO size: %f, %f", _size.width, _size.height);
                NSAssert(NO, @"Error at CVPixelBufferCreate %d", err);
            }
            
            err = CVOpenGLESTextureCacheCreateTextureFromImage (kCFAllocatorDefault, coreVideoTextureCache, renderTarget,
                                                                NULL, // texture attributes
                                                                GL_TEXTURE_2D,
                                                                _textureOptions.internalFormat, // opengl format
                                                                (int)_size.width,
                                                                (int)_size.height,
                                                                _textureOptions.format, // native iOS format
                                                                _textureOptions.type,
                                                                0,
                                                                &renderTexture);

其中coreVideoTextureCache是CVOpenGLESTextureCacheRef类型的属性,也就是说,renderTarget的内存,并不是自己创建的,而是来自OpenGLESTextureCache,在调用CFRelease时也不会自行释放。如果不知道其中的原理,自行创建GPUImageFramebuffer,dealloc时并没有真正释放内存,会造成内存泄漏,而且每次都是一帧视频或者一幅图像的大小,相当可观。

而在GPUImageFramebufferCache的purgeAllUnassignedFramebuffers方法中,会帮我们清空OpenGLESTextureCache,真正释放GPUImageFramebuffer占用内存。purgeAllUnassignedFramebuffers方法会在收到memory warning时触发释放内存,一般情况下无需自行调用。

所以,GPUImage给我们实现了一套完善的framebuffer的cache机制,如果不用它而是自行创建和管理framebuffer去处理视频和大量图片时,稍有不慎就会出现crash的情况。在这种情况下出现的crash并不会抛出异常,在xcode提供的内存检测工具中也不能观测到内存增长,会让不明就里的人难以定位crash的原因。

关于CVOpenGLESTextureCache

对于 iOS 5.0+ 的设备,Core Video 允许 OpenGL ES 的 texture 和一个 image buffer 绑定,从而省略掉创建 texture 的步骤,也方便对 image buffer 操作,例如以多种格式读取其中的数据而不是用 glReadPixels 这样比较费时的方法。Core Video 中的 OpenGL ES texture 类型为 CVOpenGLESTextureRef,定义为

A texture-based image buffer that supplies source image data to OpenGL ES.

image buffer 类型为 CVImageBufferRef,在文档中可以看到两个类型其实是一回事:

typedef CVImageBufferRef CVOpenGLESTextureRef;

这些 texture 是由 CVOpenGLESTextureCache 缓存、管理的。可以用 CVOpenGLESTextureCacheCreateTextureFromImage 来从 image buffer 得到 texture 并将两者绑定,该 texture 可能是新建的或缓存的但未使用的。用 CVOpenGLESTextureCacheFlush 来清理未使用的缓存。

以上的 image buffer 需要满足一定条件:

To create a CVOpenGLESTexture object successfully, the pixel buffer passed to CVOpenGLESTextureCacheCreateTextureFromImage() must be backed by an IOSurface.

camera API 得到的 image buffer(CVPixelBufferRef)已经满足条件,在 Apple 的官方 sample code 中有从视频文件的一帧 image buffer 映射到相应 texture 并在 shader 中使用的示例。

但如果要自己创建空的 image buffer 并和 texture 绑定用来 render,那么创建时需要为 dictionary 指定一个特殊的 key:kCVPixelBufferIOSurfacePropertiesKey。代码示例:

CFDictionaryRef empty; // empty value for attr value.
CFMutableDictionaryRef attrs;
empty = CFDictionaryCreate(kCFAllocatorDefault, NULL, NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); // our empty IOSurface properties dictionary
attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 1, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
CFDictionarySetValue(attrs, kCVPixelBufferIOSurfacePropertiesKey, empty);
  
CVPixelBufferRef renderTarget;
CVReturn err = CVPixelBufferCreate(kCFAllocatorDefault, (int)_size.width, (int)_size.height, kCVPixelFormatType_32BGRA, attrs, &renderTarget);
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342

推荐阅读更多精彩内容