簡介
由於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);