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方法。周而復始,直到最后一個節點,要么生成圖片,要么顯示。