這篇文章我們來探究下屏幕撕裂、屏幕卡頓、離屏渲染。
一、屏幕撕裂

在探究屏幕撕裂問題之前,我們需要先了解下屏幕顯示圖像的原理。

電子槍按照上圖顯示的那樣,從上往下逐行掃描,掃描完成后,就顯示一幀的畫面,隨后電子槍回到初始位置。當電子槍換到新的一行時,會發(fā)出
水平同步信號(Horizonal Synchronization),簡稱Hsync。而當一幀顯示完成后,電子槍回到初始位置,準備畫下一幀之前,顯示器會發(fā)出一個垂直同步信號(Vertical Synchronization),簡稱VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是VSync信號產生的頻率。盡管現(xiàn)在的設備大都是液晶顯示屏了,但原理仍然沒有變。
從上圖可以看出,如果幀緩存區(qū)只有一個,這時幀緩存區(qū)的讀取和刷新都都會有比較大的效率問題。因此引入了
雙緩存機制。在這種情況下,GPU會預先渲染好一幀放入一個緩沖區(qū)內,讓顯示控制器讀取,當下一幀渲染好后,GPU會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。雙緩存機制雖然能解決效率問題,但是又帶來了一個新的問題。在
顯示控制器從幀緩存區(qū)進行讀取圖像進行顯示時,如果當前這一幀的內容還未讀取完成,GPU又將新的一幀內容提交到幀緩沖區(qū)并把兩個幀緩沖區(qū)進行更新后,顯示控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,就會造成屏幕撕裂的現(xiàn)象。
屏幕撕裂產生的原因
屏幕撕裂就是在于顯卡輸出幀的速度比顯示器快,顯示器的處理速度跟不上顯卡,在顯示器處理顯卡丟過來的第1幀的時候,第2幀就又到了,導致同一個畫面同時出現(xiàn)1、2兩幀,屏幕撕裂就產生了。
如何解決屏幕撕裂
在雙緩存基礎上,引入垂直同步信號。
垂直同步信號
開啟垂直同步后,顯卡繪制3D圖形前會等待垂直同步信號,當該信號到達時,顯卡才開始繪制3D圖形,如果顯卡性能較為強勁,在下個垂直同步信號到來之前已經完成了對該幀的渲染,顯卡就會暫停處理,等下個垂直同步信號到來后才開始渲染下一幀。通俗的來講,垂直同步就是讓顯卡每秒輸出的幀數(shù)等于顯示器的刷新率。垂直同步是用來防止畫面撕裂的,反之,關閉垂直同步就會出現(xiàn)撕裂、跳幀的情況。
垂直同步信號雖然能解決屏幕撕裂現(xiàn)象,也增加了畫面流暢度,但是需要消費更多的計算資源,也會帶來部分延遲(屏幕卡頓)。
二、屏幕卡頓

在
VSync信號到來后,系統(tǒng)圖形服務會通過CADisplayLink等機制通知App,App主線程開始在 CPU中計算顯示內容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后CPU會將計算好的內容提交到GPU去,由GPU進行變換、合成、渲染。隨后GPU會把渲染結果提交到幀緩沖區(qū)去,等待下一次VSync信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個VSync時間內,CPU或者GPU沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是屏幕卡頓的原因。從上面的圖中可以看到,
CPU和GPU不論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時,也需要分別對CPU和GPU壓力進行評估和優(yōu)化。
垂直同步信號與雙緩沖的意義
強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。
屏幕卡頓的本質
CPU和GPU渲染流水線耗時過長,導致掉幀。
三、離屏渲染
APP通常的渲染流程:

App通過
CPU和GPU的合作,不停地將內容渲染完成放入Framebuffer幀緩沖器中,而顯示屏幕不斷地從Framebuffer中獲取內容,顯示實時的內容。而離屏渲染的流程:

與普通情況下GPU直接將渲染好的內容放入Framebuffer中不同,需要先
額外創(chuàng)建離屏渲染緩沖區(qū) Offscreen Buffer,將提前渲染好的內容放入其中,等到合適的時機再將Offscreen Buffer中的內容進一步疊加、渲染,完成后將結果切換到Framebuffer中。
離屏渲染帶來的問題
離屏渲染時由于App需要提前對部分內容進行額外的渲染并保存到Offscreen Buffer,以及需要在必要時刻對Offscreen Buffer和Framebuffer進行內容切換,所以會需要更長的處理時間(實際上這兩步關于 buffer 的切換代價都非常大)。
并且Offscreen Buffer本身就需要額外的空間,大量的離屏渲染會造成內存過大的壓力。與此同時,Offscreen Buffer的總大小也有限,不能超過屏幕總像素的2.5倍。
可見離屏渲染的開銷非常大,一旦需要離屏渲染的內容過多,很容易造成掉幀的問題。所以大部分情況下,我們都應該盡量避免離屏渲染。
為什么需要離屏渲染
既然離屏渲染會帶來那么多的問題,那為什么又需要離屏渲染呢?
- 一些特殊效果需要使用額外的
Offscreen Buffer來保存渲染的中間狀態(tài),所以不得不使用離屏渲染。比如系統(tǒng)自動觸發(fā)的陰影、圓角等。 - 出于效率的目的,可以將內容提前渲染保存在
Offscreen Buffer中,達到復用的目的。
光柵化(shouldRasterize )
開啟光柵化后,會觸發(fā)離屏渲染,Render Server會強制將CALayer的渲染位圖結果bitmap保存下來,這樣下次再需要渲染時就可以直接復用,從而提高效率。
而保存的bitmap包含layer的subLayer、圓角、陰影、組透明度 group opacity等,所以如果layer的構成包含上述幾種元素,結構復雜且需要反復利用,那么就可以考慮打開光柵化。
圓角、陰影、組透明度等會由系統(tǒng)自動觸發(fā)離屏渲染,那么打開光柵化可以節(jié)約第二次及以后的渲染時間。而多層 subLayer的情況由于不會自動觸發(fā)離屏渲染,所以相比之下會多花費第一次離屏渲染的時間,但是可以節(jié)約后續(xù)的重復渲染的開銷。
不過使用光柵化的時候需要注意以下幾點:
- 如果
layer不能被復用,則沒有必要打開光柵化 - 如果
layer不是靜態(tài),需要被頻繁修改,比如處于動畫之中,那么開啟離屏渲染反而影響效率
離屏渲染緩存內容有時間限制,緩存內容100ms內如果沒有被使用,那么就會被丟棄,無法進行復用 - 離屏渲染緩存空間有限,超過
2.5倍屏幕像素大小的話也會失效,無法復用
圓角的離屏渲染

layer由三層組成,我們設置圓角,會這樣做:
view.layer.cornerRadius = 2
而蘋果文檔指出,cornerRadius只會默認設置backgroundColor和border的圓角,而不會設置content的圓角,除非同時設置了layer.masksToBounds為true(對應UIView的clipsToBounds屬性)
Setting the radius to a value greater than 0.0 causes the layer to begin drawing rounded corners on its background. By default, the corner radius does not apply to the image in the layer’s contents property; it applies only to the background color and border of the layer. However, setting the masksToBounds property to true causes the content to be clipped to the rounded corners.
因此我們就知道,如果只是設置了cornerRadius而沒有設置masksToBounds,由于不需要疊加裁剪,此時是并不會觸發(fā)離屏渲染的。而當設置了裁剪屬性的時候,由于masksToBounds會對 layer以及所有subLayer的content都進行裁剪,所以會觸發(fā)離屏渲染。
離屏渲染的邏輯
圖層的疊加繪制大概遵循畫家算法,在這種算法下會按層繪制,首先繪制距離較遠的場景,然后用繪制距離較近的場景覆蓋較遠的部分。

在普通的
layer繪制中,上層的sublayer會覆蓋下層的sublayer,下層sublayer繪制完之后就可以拋棄了,從而節(jié)約空間提高效率。所有sublayer依次繪制完畢之后,整個繪制過程完成,就可以進行后續(xù)的呈現(xiàn)了。
而當我們設置了
cornerRadius以及masksToBounds進行圓角 + 裁剪時,如前文所述masksToBounds裁剪屬性會應用到所有的sublayer上。這也就意味著所有的sublayer必須要重新被應用一次圓角+裁剪,這也就意味著所有的sublayer在第一次被繪制完之后,并不能立刻被丟棄,而必須要被保存在 Offscreen buffer中等待下一輪圓角+裁剪,這也就誘發(fā)了離屏渲染。
實際上不只是
圓角+裁剪,如果設置了透明度+組透明(layer.allowsGroupOpacity+layer.opacity),陰影屬性(shadowOffset)等,都會產生類似的效果,因為組透明度、陰影都是和裁剪類似的,會作用與layer以及其所有sublayer上,這就導致必然會引起離屏渲染。
避免圓角離屏渲染
除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染嗎?
由于剛才我們提到,圓角引起離屏渲染的本質是裁剪的疊加,導致masksToBounds對layer以及所有sublayer進行二次處理。那么我們只要避免使用masksToBounds進行二次處理,而是對所有的sublayer進行預處理,就可以只進行畫家算法,用一次疊加就完成繪制。
那么可行的實現(xiàn)方法大概有下面幾種:
- 直接使用
帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這種方法需要依賴具體情況,并不通用。 - 再增加一個和背景色相同的
遮罩mask覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色為圖片或漸變色的情況。 - 用
貝塞爾曲線繪制閉合帶圓角的矩形,在上下文中設置只有內部可見,再將不帶圓角的layer渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,但是layer的布局一旦改變,貝塞爾曲線都需要手動地重新繪制,所以需要對frame、color等進行手動地監(jiān)聽并重繪。 - 重寫
drawRect,用CoreGraphics相關方法,在需要應用圓角時進行手動繪制。不過CoreGraphics效率也很有限,如果需要多次調用也會有效率問題。
觸發(fā)離屏渲染原因的總結
- 使用了
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, Core Text 等)
總結一下:設置圓角不一定會導致離屏渲染,離屏渲染不一定是由于設置圓角產生的
參考:iOS 渲染原理解析