屏幕顯示圖像的原理:
高中物理應該學過顯示器是如何顯示圖像的:需要顯示的圖像經過CRT電子槍以極快的速度一行一行的掃描,掃描出來就呈現了一幀畫面,隨后電子槍又會回到初始位置循環掃描,形成了我們看到的圖片或視頻。
為了讓顯示器的顯示跟視頻控制器同步,當電子槍新掃描一行的時候,準備掃描的時發送一個水平同步信號(HSync信號),顯示器的刷新頻率就是HSync信號產生的頻率。然后CPU計算好frame等屬性,將計算好的內容交給GPU去渲染,GPU渲染好之后就會放入幀緩沖區。然后視頻控制器會按照HSync信號逐行讀取幀緩沖區的數據,經過可能的數模轉換傳遞給顯示器,就顯示出來了。這里只是簡作描述,專業描述請自行查詢。
GPU屏幕渲染有兩種方式:
(1)On-Screen Rendering (當前屏幕渲染)?
指的是GPU的渲染操作是在當前用于顯示的屏幕緩沖區進行。
(2)Off-Screen Rendering (離屏渲染)
指的是在GPU在當前屏幕緩沖區以外開辟一個緩沖區進行渲染操作。
當前屏幕渲染不需要額外創建新的緩存,也不需要開啟新的上下文,相對于離屏渲染性能更好。但是受當前屏幕渲染的局限因素限制(只有自身上下文、屏幕緩存有限等),當前屏幕渲染有些情況下的渲染解決不了的,就使用到離屏渲染。
相比于當前屏幕渲染,離屏渲染的代價是很高的,主要體現在兩個方面:
(1)創建新緩沖區
要想進行離屏渲染,首先要創建一個新的緩沖區。
(2)上下文切換
離屏渲染的整個過程,需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen),等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上有需要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。
由于垂直同步的機制,如果在一個 HSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
既然離屏渲染這么耗性能,為什么有這套機制呢?
有些效果被認為不能直接呈現于屏幕,而需要在別的地方做額外的處理預合成。圖層屬性的混合體沒有預合成之前不能直接在屏幕中繪制,所以就需要屏幕外渲染。屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU還是GPU)。
下面的情況或操作會引發離屏渲染:
- 為圖層設置遮罩(layer.mask)
-?將圖層的layer.masksToBounds / view.clipsToBounds屬性設置為true
-?將圖層layer.allowsGroupOpacity屬性設置為YES和layer.opacity小于1.0
-?為圖層設置陰影(layer.shadow *)。
-?為圖層設置layer.shouldRasterize=true
-?具有layer.cornerRadius,layer.edgeAntialiasingMask,layer.allowsEdgeAntialiasing的圖層
-?文本(任何種類,包括UILabel,CATextLayer,Core Text等)。
-?使用CGContext在drawRect :方法中繪制大部分情況下會導致離屏渲染,甚至僅僅是一個空的實現。
優化方案
官方對離屏渲染產生性能問題也進行了優化:
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.boundscornerRadius:imageView.frame.size.width]addClip];
[imageView drawRect:imageView.bounds];
imageView.image=UIGraphicsGetImageFromCurrentImageContext();
//結束畫圖
UIGraphicsEndImageContext();
[self.view addSubview:imageView];
優化方案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;
[self.view addSubview:imageView];
對于方案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=[UIColorgrayColor].CGColor;
imageView.layer.shadowOpacity=1.0;
imageView.layer.shadowRadius=2.0;
UIBezierPath *path=[UIBezierPathbezierPathWithRect: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框架)畫出來圓角圖片
Core Animation工具檢測離屏渲染
對于離屏渲染的檢測,蘋果為我們提供了一個測試工具Core Animation。可以在Xcode->Open Develeper Tools->Instruments中找到
Core Animation工具用來監測Core Animation性能,提供可見的FPS值,并且提供幾個選項來測量渲染性能。如下圖:
下面我們來說明每個選項的功能:
Color Blended Layers:這個選項如果勾選,你能看到哪個layer是透明的,GPU正在做混合計算。顯示紅色的就是透明的,綠色就是不透明的。
Color Hits Green and Misses Red:如果勾選這個選項,且當我們代碼中有設置shouldRasterize為YES,那么紅色代表沒有復用離屏渲染的緩存,綠色則表示復用了緩存。我們當然希望能夠復用。
Color Copied Images:按照官方的說法,當圖片的顏色格式GPU不支持的時候,Core Animation會
拷貝一份數據讓CPU進行轉化。例如從網絡上下載了TIFF格式的圖片,則需要CPU進行轉化,這個區域會顯示成藍色。還有一種情況會觸發Core Animation的copy方法,就是字節不對齊的時候。如下圖:
Color Immediately:默認情況下Core Animation工具以每毫秒10次的頻率更新圖層調試顏色,如果勾選這個選項則移除10ms的延遲。對某些情況需要這樣,但是有可能影響正常幀數的測試。
Color Misaligned Images:勾選此項,如果圖片需要縮放則標記為黃色,如果沒有像素對齊則標記為紫色。像素對齊我們已經在上面有所介紹。
Color Offscreen-Rendered Yellow:用來檢測離屏渲染的,如果顯示黃色,表示有離屏渲染。當然還要結合Color Hits Green and Misses Red來看,是否復用了緩存。
Color OpenGL Fast Path Blue:這個選項對那些使用OpenGL的圖層才有用,像是GLKView或者 CAEAGLLayer,如果不顯示藍色則表示使用了CPU渲染,繪制在了屏幕外,顯示藍色表示正常。
Flash Updated Regions:當對圖層重繪的時候回顯示黃色,如果頻繁發生則會影響性能。可以用增加緩存來增強性能。