GPU 渲染機制:CPU計算好顯示內容提交到GPU,GPU渲染完成後將渲染結果放入幀緩衝區frame buffer,隨後視頻控制器會按照VSync信號逐行讀取幀緩衝區的數據,經過可能的數模轉換傳遞給顯示器顯示。 GPU 屏幕渲染有以下兩種方式: ● 1)On-Screen Rendering,意為當 ...
GPU 渲染機制:CPU計算好顯示內容提交到GPU,GPU渲染完成後將渲染結果放入幀緩衝區frame buffer,隨後視頻控制器會按照VSync信號逐行讀取幀緩衝區的數據,經過可能的數模轉換傳遞給顯示器顯示。
GPU 屏幕渲染有以下兩種方式:
● 1)On-Screen Rendering,意為當前屏幕渲染,指的是GPU的渲染操作是在當前用於顯示的屏幕緩衝區中進行。
● 2)Off-Screen Rendering,意為離屏渲染,指的是GPU在當前屏幕緩衝區以外新開闢一個緩衝區進行渲染操作。
特殊的離屏渲染:如果將不在GPU的當前屏幕緩衝區中進行的渲染都稱為離屏渲染,那麼就還有另一種特殊的“離屏渲染”方式:CPU渲染。如果我們重寫了drawRect方法,並且使用任何Core Graphics的技術進行了繪製操作,就涉及到了CPU渲染。整個渲染過程由CPU在App內同步地完成,渲染得到的bitmap最後再交由GPU用於顯示。備註:Core Graphics通常是線程安全的,所以可以進行非同步繪製,顯示的時候再放回主線程,一個簡單的非同步繪製過程大致如下:
(void)display {
dispatch_async(backgroundQueue, ^{
CGContextRef ctx = CGBitmapContextCreate(...);
// draw in context...
CGImageRef img = CGBitmapContextCreateImage(ctx);
CFRelease(ctx);
dispatch_async(mainQueue, ^{
layer.contents = img;
});
});
}
離屏渲染的觸發方式:
1)shouldRasterize(光柵化),光柵化是比較特別的一種。光柵化概念:將圖轉化為一個個柵格組成的圖象。光柵化特點:每個元素對應幀緩衝區中的一像素。shouldRasterize = YES 在其他屬性觸發離屏渲染的同時,會將光柵化後的內容緩存起來,如果對應的 layer 及其 sublayers 沒有發生改變,在下一幀的時候可以直接復用。shouldRasterize = YES 這將隱式的創建一個點陣圖,各種陰影遮罩等效果也會保存到點陣圖中並緩存起來,從而減少渲染的頻度。相當於光柵化是把 GPU 的操作轉到 CPU 上了,生成點陣圖緩存,直接讀取復用。當你使用光柵化時,你可以開啟 Color Hits Green and Misses Red 來檢查該場景下光柵化操作是否是一個好的選擇。綠色表示緩存被覆用,紅色表示緩存在被重覆創建。如果光柵化的層變紅得太頻繁那麼光柵化對優化可能沒有多少用處。點陣圖緩存從記憶體中刪除又重新創建得太過頻繁,紅色表明緩存重建得太遲。可以針對性的選擇某個較小而較深的層結構進行光柵化,來嘗試減少渲染時間。對於經常變動的內容,這個時候不要開啟,否則會造成性能的浪費。例如經常打交道的 TableViewCell,因為 TableViewCell 的重繪是很頻繁的(因為 Cell 的復用),如果Cell的內容不斷變化,則Cell需要不斷重繪,如果此時設置了 cell.layer 可光柵化,則會造成大量的離屏渲染,降低圖形性能。
2)masks(遮罩)
3)shadows(陰影)
4)edge antialiasing(抗鋸齒)
5)group opacity(不透明)
6)複雜形狀設置圓角等
7)漸變
為什麼會使用離屏渲染:當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前(下一個 VSync 信號開始前)不能直接在屏幕中繪製,所以就需要屏幕外渲染被喚起。屏幕外渲染並不意味著軟體繪製,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論 CPU 還是 GPU)。所以當使用離屏渲染的時候會很容易造成性能消耗,因為離屏渲染會單獨在記憶體中創建一個屏幕外緩衝區併進行渲染,而屏幕外緩衝區跟當前屏幕緩衝區上下文切換是很耗性能的。由於垂直同步的機制,如果在一個 VSync 時間內,CPU或者GPU沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
Instruments 監測離屏渲染:
1)Color Offscreen-Rendered Yellow,開啟後會把那些需要離屏渲染的圖層高亮成黃色,這就意味著黃色圖層可能存在性能問題。
2)Color Hits Green and Misses Red,如果 shouldRasterize 被設置成YES,對應的渲染結果會被緩存,如果圖層是綠色,就表示這些緩存被覆用;如果是紅色就表示緩存會被重覆創建,這就表示該處存在性能問題了。
GPU離屏渲染的性能影響
GPU的操作是高度流水線化的。本來所有計算工作都在有條不紊地正在向frame buffer輸出,此時突然收到指令,需要輸出到另一塊記憶體,那麼流水線中正在進行的一切都不得不被丟棄,切換到只能服務於我們當前的“切圓角”操作。等到完成以後再次清空,再回到向frame buffer輸出的正常流程。
在tableView或者collectionView中,滾動的每一幀變化都會觸發每個cell的重新繪製,因此一旦存在離屏渲染,上面提到的上下文切換就會每秒發生60次,並且很可能每一幀有幾十張的圖片要求這麼做,對於GPU的性能衝擊可想而知(GPU非常擅長大規模並行計算,但是我想頻繁的上下文切換顯然不在其設計考量之中)
相比於當前屏幕渲染,離屏渲染的代價是很高的,主要體現在兩個方面:
(1)創建新緩衝區
要想進行離屏渲染,首先要創建一個新的緩衝區。
(2)上下文切換
離屏渲染的整個過程,需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen),等到離屏渲染結束以後,將離屏緩衝區的渲染結果顯示到屏幕上有需要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。
善用離屏渲染
儘管離屏渲染開銷很大,但是當我們無法避免它的時候,可以想辦法把性能影響降到最低。優化思路也很簡單:既然已經花了不少精力把圖片裁出了圓角,如果我能把結果緩存下來,那麼下一幀渲染就可以復用這個成果,不需要再重新畫一遍了。
CALayer為這個方案提供了對應的解法:shouldRasterize。一旦被設置為true,Render Server就會強制把layer的渲染結果(包括其子layer,以及圓角、陰影、group opacity等等)保存在一塊記憶體中,這樣一來在下一幀仍然可以被覆用,而不會再次觸發離屏渲染。有幾個需要註意的點:
● shouldRasterize的主旨在於降低性能損失,但總是至少會觸發一次離屏渲染。如果你的layer本來並不複雜,也沒有圓角陰影等等,打開這個開關反而會增加一次不必要的離屏渲染
● 離屏渲染緩存有空間上限,最多不超過屏幕總像素的2.5倍大小
● 一旦緩存超過100ms沒有被使用,會自動被丟棄
● layer的內容(包括子layer)必須是靜態的,因為一旦發生變化(如resize,動畫),之前辛苦處理得到的緩存就失效了。如果這件事頻繁發生,我們就又回到了“每一幀都需要離屏渲染”的情景,而這正是開發者需要極力避免的。針對這種情況,Xcode提供了“Color Hits Green and Misses Red”的選項,幫助我們查看緩存的使用是否符合預期
● 其實除瞭解決多次離屏渲染的開銷,shouldRasterize在另一個場景中也可以使用:如果layer的子結構非常複雜,渲染一次所需時間較長,同樣可以打開這個開關,把layer繪製到一塊緩存,然後在接下來複用這個結果,這樣就不需要每次都重新繪製整個layer樹了
什麼時候需要CPU渲染
渲染性能的調優,其實始終是在做一件事:平衡CPU和GPU的負載,讓他們儘量做各自最擅長的工作。
平衡CPU和GPU的負載
絕大多數情況下,得益於GPU針對圖形處理的優化,我們都會傾向於讓GPU來完成渲染任務,而給CPU留出足夠時間處理各種各樣複雜的App邏輯。為此Core Animation做了大量的工作,儘量把渲染工作轉換成適合GPU處理的形式(也就是所謂的硬體加速,如layer composition,設置backgroundColor等等)。
但是對於一些情況,如文字(CoreText使用CoreGraphics渲染)和圖片(ImageIO)渲染,由於GPU並不擅長做這些工作,不得不先由CPU來處理好以後,再把結果作為texture傳給GPU。除此以外,有時候也會遇到GPU實在忙不過來的情況,而CPU相對空閑(GPU瓶頸),這時可以讓CPU分擔一部分工作,提高整體效率。
來自WWDC18 session 221,可以看到Core Text基於Core Graphics
一個典型的例子是,我們經常會使用CoreGraphics給圖片加上圓角(將圖片中圓角以外的部分渲染成透明)。整個過程全部是由CPU完成的。這樣一來既然我們已經得到了想要的效果,就不需要再另外給圖片容器設置cornerRadius。另一個好處是,我們可以靈活地控製裁剪和緩存的時機,巧妙避開CPU和GPU最繁忙的時段,達到平滑性能波動的目的。
這裡有幾個需要註意的點:
● 渲染不是CPU的強項,調用CoreGraphics會消耗其相當一部分計算時間,並且我們也不願意因此阻塞用戶操作,因此一般來說CPU渲染都在後臺線程完成(這也是AsyncDisplayKit的主要思想),然後再回到主線程上,把渲染結果傳回CoreAnimation。這樣一來,多線程間數據同步會增加一定的複雜度
● 同樣因為CPU渲染速度不夠快,因此只適合渲染靜態的元素,如文字、圖片(想象一下沒有硬體加速的視頻解碼,性能慘不忍睹)
● 作為渲染結果的bitmap數據量較大(形式上一般為解碼後的UIImage),消耗記憶體較多,所以應該在使用完及時釋放,併在需要的時候重新生成,否則很容易導致OOM
● 如果你選擇使用CPU來做渲染,那麼就沒有理由再觸發GPU的離屏渲染了,否則會同時存在兩塊內容相同的記憶體,而且CPU和GPU都會比較辛苦
● 一定要使用Instruments的不同工具來測試性能,而不是僅憑猜測來做決定.
優化方案
官方對離屏渲染產生性能問題也進行了優化:
iOS 9.0 之前UIimageView跟UIButton設置圓角都會觸發離屏渲染。
iOS 9.0 之後UIButton設置圓角會觸發離屏渲染,而UIImageView里png圖片設置圓角不會觸發離屏渲染了,如果設置其他陰影效果之類的還是會觸發離屏渲染的。
1、圓角優化
在APP開發中,圓角圖片還是經常出現的。如果一個界面中只有少量圓角圖片或許對性能沒有非常大的影響,但是當圓角圖片比較多的時候就會APP性能產生明顯的影響。
我們設置圓角一般通過如下方式:
imageView.layer.cornerRadius = CGFloat(10);
imageView.layer.masksToBounds = YES;
這樣處理的渲染機制是GPU在當前屏幕緩衝區外新開闢一個渲染緩衝區進行工作,也就是離屏渲染,這會給我們帶來額外的性能損耗,如果這樣的圓角操作達到一定數量,會觸發緩衝區的頻繁合併和上下文的的頻繁切換,性能的代價會巨集觀地表現在用戶體驗上——掉幀。
優化方案1:使用貝塞爾曲線UIBezierPath和Core Graphics框架畫出一個圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
//開始對imageView進行畫圖
UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, 1.0);
//使用貝塞爾曲線畫出一個圓形圖
[[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip];
[imageView drawRect:imageView.bounds];
imageView.image = UIGraphicsGetImageFromCurrentImageContext();
//結束畫圖
UIGraphicsEndImageContext();
優化方案2:使用CAShapeLayer和UIBezierPath設置圓角
UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
imageView.image = [UIImage imageNamed:@"myImg"];
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
//設置大小
maskLayer.frame = imageView.bounds;
//設置圖形樣子
maskLayer.path = maskPath.CGPath;
imageView.layer.mask = maskLayer;
對於方案2需要解釋的是:
● CAShapeLayer繼承於CALayer,可以使用CALayer的所有屬性值;
● CAShapeLayer需要貝塞爾曲線配合使用才有意義(也就是說才有效果)
● 使用CAShapeLayer(屬於CoreAnimation)與貝塞爾曲線可以實現不在view的drawRect(繼承於CoreGraphics走的是CPU,消耗的性能較大)方法中畫出一些想要的圖形
● CAShapeLayer動畫渲染直接提交到手機的GPU當中,相較於view的drawRect方法使用CPU渲染而言,其效率極高,能大大優化記憶體使用情況。
總的來說就是用CAShapeLayer的記憶體消耗少,渲染速度快,建議使用優化方案2。
2、shadow優化
對於shadow,如果圖層是個簡單的幾何圖形或者圓角圖形,我們可以通過設置shadowPath來優化性能,能大幅提高性能。示例如下:
imageView.layer.shadowColor = [UIColor grayColor].CGColor;
imageView.layer.shadowOpacity = 1.0;
imageView.layer.shadowRadius = 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithRect:imageView.frame];
imageView.layer.shadowPath = path.CGPath;
我們還可以通過設置shouldRasterize屬性值為YES來強制開啟離屏渲染。其實就是光柵化(Rasterization)。既然離屏渲染這麼不好,為什麼我們還要強制開啟呢?當一個圖像混合了多個圖層,每次移動時,每一幀都要重新合成這些圖層,十分消耗性能。當我們開啟光柵化後,會在首次產生一個點陣圖緩存,當再次使用時候就會復用這個緩存。但是如果圖層發生改變的時候就會重新產生點陣圖緩存。所以這個功能一般不能用於UITableViewCell中,cell的復用反而降低了性能。最好用於圖層較多的靜態內容的圖形。而且產生的點陣圖緩存的大小是有限制的,一般是2.5個屏幕尺寸。在100ms之內不使用這個緩存,緩存也會被刪除。所以我們要根據使用場景而定。
3、其他的一些優化建議
● 當我們需要圓角效果時,可以使用一張中間透明圖片蒙上去
● 使用ShadowPath指定layer陰影效果路徑
● 使用非同步進行layer渲染(Facebook開源的非同步繪製框架AsyncDisplayKit)
● 設置layer的opaque值為YES,減少複雜圖層合成
● 儘量使用不包含透明(alpha)通道的圖片資源
● 儘量設置layer的大小值為整形值
● 直接讓美工把圖片切成圓角進行顯示,這是效率最高的一種方案
● 很多情況下用戶上傳圖片進行顯示,可以讓服務端處理圓角
● 使用代碼手動生成圓角Image設置到要顯示的View上,利用UIBezierPath(CoreGraphics框架)畫出來圓角圖片