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。
因此,查询的过程是:
- 使用HashKey查询到满足条件的FrameBuffer个数:
NSString *lookupHash = [self hashForSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
NSNumber *numberOfMatchingTexturesInCache = [framebufferTypeCounts objectForKey:lookupHash];
NSInteger numberOfMatchingTextures = [numberOfMatchingTexturesInCache integerValue];
- 如果个数为零,则生成一个新的FrameBuffer并且返回:
framebufferFromCache = [[GPUImageFramebuffer alloc] initWithSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
- 如果有满足条件的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];
}
- 在返回FrameBuffer之前,需要将FrameBuffer进行一次lock,增加引用计数。
- 当一个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);