GPUImage源碼解讀之GPUImageFramebufferCache

簡介

由于GPUImage添加濾鏡可以形成一個(gè)FilterChain,因此,在渲染的過程中,可能會需要很多個(gè)FrameBuffer,但是正如上文所說,每生成一個(gè)FrameBuffer都需要占用一定的內(nèi)存或者顯存。因此,必須保證盡可能少創(chuàng)建FrameBuffer。而GPUImageFrameBufferCache就是用來管理所有的FrameBuffer的。

根據(jù)上面對GPUImageFrameBuffer的介紹,每個(gè)FrameBuffer其實(shí)就是一塊內(nèi)存或者緩存,因此只要它們的size和textureOption是一樣的,那么這個(gè)FrameBuffer就是完全可以重用的。

一般來說,GPUImageFrameBufferCache可以創(chuàng)建多個(gè),一般每一個(gè)GPUImageContext中會有一個(gè)公用的GPUImageFrameBufferCache。通過這個(gè)Cache可以獲得對應(yīng)的GPUImageContext中得到對應(yīng)的FrameBuffer對象。

重用過程如下:

  • 首先就是要使用size和textureOptions生成一個(gè)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];
    }
}
  • 第二步是根據(jù)這個(gè)生成的key,查詢在cache里面有多少個(gè)滿足這個(gè)條件的FrameBuffer可用。在GPUImageFrameBufferCache中,包含了兩個(gè)Dictionary:
NSMutableDictionary *framebufferCache;
NSMutableDictionary *framebufferTypeCounts;

其中framebufferTypeCounts是保存了滿足當(dāng)前size和textureOptions生成的key的FrameBuffer個(gè)數(shù),key就是上面生成的hashKey;而framebufferCache則是保存的每個(gè)Texture對象,key是上面生成的hashKey+“-i”;比如滿足當(dāng)前size和textureOptions的FrameBuffer有5個(gè),則在framebufferCache里面會有haskey-0~hashkey-4這些key和對應(yīng)的FrameBuffer。

因此,查詢的過程是:

  1. 使用HashKey查詢到滿足條件的FrameBuffer個(gè)數(shù):
NSString *lookupHash = [self hashForSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
NSNumber *numberOfMatchingTexturesInCache = [framebufferTypeCounts objectForKey:lookupHash];
NSInteger numberOfMatchingTextures = [numberOfMatchingTexturesInCache integerValue];
  1. 如果個(gè)數(shù)為零,則生成一個(gè)新的FrameBuffer并且返回:
framebufferFromCache = [[GPUImageFramebuffer alloc] initWithSize:framebufferSize textureOptions:textureOptions onlyTexture:onlyTexture];
  1. 如果有滿足條件的FrameBuffer,則獲取index最大的一個(gè)Key對應(yīng)的FrameBuffer,并且分別更新兩個(gè)FrameBuffer對應(yīng)的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進(jìn)行一次lock,增加引用計(jì)數(shù)。
  2. 當(dāng)一個(gè)FrameBuffer的引用計(jì)數(shù)為0的時(shí)候,我們就會將這個(gè)FrameBuffer重新放置到Cache中以便重用。

思考

我們?yōu)槭裁匆胏ache里的framebuffer呢?自己創(chuàng)建一個(gè),使用完后再釋放行不行呢?

答案顯示是NO。

我們來看一下GPUImageFramebuffer類的代碼,在dealloc中,調(diào)用了destroyFramebuffer方法,這個(gè)方法的實(shí)現(xiàn)如下。

- (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上,當(dāng)創(chuàng)建GPUImageFramebuffer時(shí)給onlyTexture參數(shù)填NO(一般就是填NO的)時(shí),會創(chuàng)建一個(gè)CVPixelBufferRef類型的變量renderTarget,當(dāng)用CFRelease去釋放這個(gè)變量時(shí),它占用的內(nèi)存并不會立即釋放,而是要調(diào)用

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

之后,才會真正釋放內(nèi)存。這個(gè)現(xiàn)象的原因可以在GPUImageFrameBuffer的init函數(shù)中找到。

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的內(nèi)存,并不是自己創(chuàng)建的,而是來自O(shè)penGLESTextureCache,在調(diào)用CFRelease時(shí)也不會自行釋放。如果不知道其中的原理,自行創(chuàng)建GPUImageFramebuffer,dealloc時(shí)并沒有真正釋放內(nèi)存,會造成內(nèi)存泄漏,而且每次都是一幀視頻或者一幅圖像的大小,相當(dāng)可觀。

而在GPUImageFramebufferCache的purgeAllUnassignedFramebuffers方法中,會幫我們清空OpenGLESTextureCache,真正釋放GPUImageFramebuffer占用內(nèi)存。purgeAllUnassignedFramebuffers方法會在收到memory warning時(shí)觸發(fā)釋放內(nèi)存,一般情況下無需自行調(diào)用。

所以,GPUImage給我們實(shí)現(xiàn)了一套完善的framebuffer的cache機(jī)制,如果不用它而是自行創(chuàng)建和管理framebuffer去處理視頻和大量圖片時(shí),稍有不慎就會出現(xiàn)crash的情況。在這種情況下出現(xiàn)的crash并不會拋出異常,在xcode提供的內(nèi)存檢測工具中也不能觀測到內(nèi)存增長,會讓不明就里的人難以定位crash的原因。

關(guān)于CVOpenGLESTextureCache

對于 iOS 5.0+ 的設(shè)備,Core Video 允許 OpenGL ES 的 texture 和一個(gè) image buffer 綁定,從而省略掉創(chuàng)建 texture 的步驟,也方便對 image buffer 操作,例如以多種格式讀取其中的數(shù)據(jù)而不是用 glReadPixels 這樣比較費(fèi)時(shí)的方法。Core Video 中的 OpenGL ES texture 類型為 CVOpenGLESTextureRef,定義為

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

image buffer 類型為 CVImageBufferRef,在文檔中可以看到兩個(gè)類型其實(shí)是一回事:

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)已經(jīng)滿足條件,在 Apple 的官方 sample code 中有從視頻文件的一幀 image buffer 映射到相應(yīng) texture 并在 shader 中使用的示例。

但如果要自己創(chuàng)建空的 image buffer 并和 texture 綁定用來 render,那么創(chuàng)建時(shí)需要為 dictionary 指定一個(gè)特殊的 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);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容