Offscreen Rendering
如何檢測你的項目中是否 觸發了離屏渲染問題
?
那么為何有一些會觸發離屏渲染,而有一些卻不會觸發呢?下面我們開始深入的探索。
離屏渲染的具體過程
我們知道通常的渲染流程是這樣的:
App通過CPU和GPU的合作,不停的將內容渲染完成放入FrameBuffer幀緩存區,而屏幕顯示不斷從FrameBuffer中獲取內容,顯示實時的內容。
但是離屏渲染的流程是這樣的:
在普通的情況下,GPU直接將渲染好的內容放入FrameBuffer中,但是在離屏渲染時不同,需要先額外創建離屏渲染緩存區OffscreenBuffer。將提前渲染好的內容放入其中,等到合適的時機再將OffSreeBuffer中的內容進一步疊加、渲染。完成后將結果切換到FrameBuffer中。
離屏渲染的效率問題
從上面的流程來看,離屏渲染時,由于App需要提前對部分內容進行額外的渲染并保存到OffScreenBuffer,以及需要在必要時對OffScreenBuffer和FrameBuffer進行內容切換,所以會需要更長的處理時間。(實際上這兩步切換的代價是非常大的)。
OffScreenBuffer本身就需要額外的空間,大量的離屏渲染可能造成內存過大的壓力。與此同時,OffScreenBuffer的總大小也是有限的:不能超過屏幕總像素的2.5倍。
可見離屏渲染的開銷非常大,一旦需要離屏渲染的內容過多,就容易造成掉幀問題,所以大部分情況下,我們要避免出現離屏渲染。
為什么要用離屏渲染
既然離屏渲染會造成性能問題,那么為什么還要使用離屏渲染?
其實主要是以下兩種原因:
一些特殊的效果需要使用額外的OffScreenBuffer來保存渲染中間的狀態,所以不得不使用離屏渲染
出于效率的目的,可以將內容提前渲染并保存到OffScreenBuffer中,從而達到復用的目的
例如,第一種原因,也就是不得不使用離屏渲染的情況。一般都是系統自動觸發。如:陰影、圓角等。比如我們使用的蒙版(mask)功能,因為最終的結果是有超過一層的渲染結果進行疊加,所以必須要利用額外的內存空間對中間的渲染結果進行保存,因此系統會默認觸發離屏渲染.
比如,iOS8開始提供模糊特效UIBlurEffectView:
先渲染需要模糊的內容本身;
對內容進行縮放;
對上一步結果進行垂直模糊;
對上一步結果進行橫向模糊;
最后一步,將模糊后的結果進行疊加合成,實現最終完整的模糊效果。
在這樣的5次過程,系統也會自動觸發離屏渲染,用來保存復雜的特效下,利用額外的內存空間對中間的結果進行保存,以便最后進行效果的合成。
離屏渲染的第二種原因:shouldRasterize 光柵化
開啟光柵化后,就會主動觸發離屏渲染。Render Server會強制將CALayer渲染位圖結果bitmap保存下來,這樣下次渲染可以直接復用,提高效率;而保存下來的bitmap就已經包含了layer和sublayer、圓角、陰影、透明度等。
如果layer的構成包含了以上幾種元素,結構非常復雜且還需要重復利用,可以考慮開啟光柵化;因為layer 上的圓角、陰影、透明度等會由系統自動觸發離屏渲染,那么打開光柵化就可以節約第二次以及以后的渲染時間。
而多層的subLayer的情況由于不會自動觸發離屏渲染,所以相比之下會花費第一次離屏渲染的時間,但是可以節約后續重復的渲染開銷。
使用光柵化的注意點:
如果layer并不能被復用,則沒必要開啟;
如果layer不是靜態的,需要 被頻繁修改,比如處于動畫之中,那么開啟離屏渲染反而影響效率了;
離屏渲染緩存內容有時間限制,緩存內容如果100ms沒被復用,那么就會被丟棄,無法進行復用;
離屏渲染緩存空間有限,超過2.5倍屏幕像素大小的話,也會失效,且無法復用
圓角的離屏渲染探索
通常來講,設置了Layer的圓角效果后,會自動觸發離屏渲染,但是具體什么情況下設置圓角才會觸發離屏渲染?
如上圖所示,Layer由3層組成,我們設置圓角通常是用下面的代碼:
view.layer.cornerRadius?=?2;
CornerRadius - Apple 官方介紹
根據蘋果的描述,上面這句代碼,只會默認設置backgroundColor和border的圓角,而不會設置content的圓角,除非設置了layer.maskToBounds為true(對應view的clickToBounds屬性)。
如果只設置了CornerRadius而沒有設置masksToBounds,由于不需要疊加裁剪,此時是不會觸發離屏渲染的。而當設置了裁剪屬性時,由于maskToBounds會對layer以及所有 的subLayer的content都進行裁剪,所以不得不觸發離屏渲染。
view.layer.masksToBounds?=?true?//觸發離屏渲染的原因
離屏渲染的邏輯
剛才說圓角加上masksToBounds時,因為masksToBounds會對layer上的所有內容進行裁剪,從而誘發了離屏渲染,那么這個過程具體是怎么回事呢?我們來仔細研究一下:
在普通的layer繪制中,上層的subLayer會覆蓋下層的subLayer,下層的subLayer在繪制完成后就可以拋棄了,從而節約空間提高效率。
所有subLayer一次繪制完畢后,整個繪制過程完成,就可以進行后續的呈現了。假設我們需要繪制一個三層的subLayer,并不設置裁剪和圓角,那么整個繪制過程就如下圖所示:
繪制完進行display
設置了CornerRadius以及masksToBounds
當我們設置了CornerRadius以及masksToBounds進行圓角加裁剪時,masksToBounds裁剪屬性會應用到所有的subLayer上,也就意味著所有的subLayer都要進行圓角+裁剪,意味著所有的subLayer在第一次繪制后,并不能立刻丟棄,而必須保存在OffScreenBuffer中等待下一輪的圓角加裁剪操作,這樣便引發了離屏渲染。
實際上,并不單只有圓角加裁剪會觸發離屏渲染。如果設置了透明度和組透明(layer.allowsGroupOpacity+layer.opacity),陰影屬性(shadowOffset)等,都會產生這樣的離屏渲染,因為這些都不是對單一的layer進行處理,而是對layer及其所有 的subLayer進行處理,從而引發離屏渲染。
避免圓角離屏渲染的手段:
除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染?
由于剛才提到,圓角引起離屏渲染的本質是裁剪的疊加,導致了masksToBounds對layer及其所有的subLayer進行了二次處理,那么我們只要避免使用masksToBounds進行二次處理,而是對所有的subLayer進行預處理,就可以只進行“畫家算法(先繪制離屏幕較遠的圖層,然后繪制距離屏幕較近的圖層,根據深度值,確定繪制順序)”,用一次疊加就完成繪制。
有哪些可行方案
1.換資源
直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這周方法需要依賴具體情況,并不通用 。
2.mask
再增加一個和背景色相同的遮罩mask覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色為圖片 或漸變色的情況。注意這里的mask并不是指的layer上的mask,而是用兩個view的疊加
3.UIBezierPath
用貝塞爾曲線繪制閉合帶圓角的矩形,在上下文設置只有內部可見,再將不帶圓角的layer渲染成圖片,添加到貝塞爾矩形中。這種方法效率較高,但是layer的布局一旦改變,貝塞爾曲線都需要手動進行重新繪制,所以需要對frame、color等進行手動監聽并重繪。
4.CoreGraphics
重寫 drawRect:,用coreGraphics相關方法,在需要應用圓角時進行手動繪制。不過CoreGraphics效率也有限,如果多次調用也會有效率問題。
觸發離屏渲染的幾種情況
使用了mask的layer(layer.mask)
需要進行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
設置了組透明度YES,并且透明度不為1的layer (layer.allowsGroupOpacity/layer.opacity)
添加了投影的layer(layer.shadow)
采用了光柵化的layer(layer.shouldRasterize)
繪制了文字的layer (UILabel,CATextLayer,CoreText等)
舉幾個例子,加深下理解:
//按鈕存在背景圖片??因為?會對button的layer和其imageView的layer進行圓角加裁剪?所以會觸發離屏渲染 UIButton?*btn1?=?[UIButton?buttonWithType:UIButtonTypeCustom]; btn1.frame?=?CGRectMake(100,?30,?100,?100); [btn1?setImage:[UIImage?imageNamed:@"btn.png"]?forState:UIControlStateNormal]; [self.view?addSubview:btn1]; btn1.layer.cornerRadius?=?50; btn1.clipsToBounds?=?YES;
//按鈕存在背景圖片??只設置了clipsToBounds,裁剪是針對于imageview的,所以不會觸發離屏渲染(not?offscreen?rendering) UIButton?*btn1?=?[UIButton?buttonWithType:UIButtonTypeCustom]; btn1.frame?=?CGRectMake(100,?30,?100,?100); [btn1?setImage:[UIImage?imageNamed:@"btn.png"]?forState:UIControlStateNormal]; [self.view?addSubview:btn1]; btn1.imageView.layer.cornerRadius?=?50; btn1.clipsToBounds?=?YES;
//按鈕不存在背景圖片?雖然設置了圓角+裁剪,但是只有一層layer??所以不會觸發離屏渲染 UIButton?*btn2?=?[UIButton?buttonWithType:UIButtonTypeCustom]; btn2.frame?=?CGRectMake(100,?180,?100,?100); btn2.backgroundColor?=?[UIColor?blueColor]; [self.view?addSubview:btn2]; btn2.layer.cornerRadius?=?50; btn2.clipsToBounds?=?YES;
//UIImageView?設置了圖片+背景色;?背景色和圖片兩層layer都需要進行圓角+裁剪??所以會觸發離屏渲染 UIImageView?*img1?=?[[UIImageView?alloc]init]; img1.frame?=?CGRectMake(100,?320,?100,?100); img1.backgroundColor?=?[UIColor?blueColor]; img1.image?=?[UIImage?imageNamed:@"btn.png"]; [self.view?addSubview:img1]; img1.layer.cornerRadius?=?50; img1.layer.masksToBounds?=?YES;
//UIImageView?只設置了圖片,無背景色;?只有一層?layer?需要圓角+裁剪?所以不會觸發離屏渲染 UIImageView?*img2?=?[[UIImageView?alloc]init]; img2.frame?=?CGRectMake(100,?480,?100,?100); img2.image?=?[UIImage?imageNamed:@"btn.png"]; [self.view?addSubview:img2]; img2.layer.cornerRadius?=?50; img2.layer.masksToBounds?=?YES;
編輯:黃飛
?
評論
查看更多