iOS屏幕撕裂、屏幕卡頓、離屏渲染的相關探究

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

一、屏幕撕裂

圖片來源于網(wǎng)絡

在探究屏幕撕裂問題之前,我們需要先了解下屏幕顯示圖像的原理。
圖片來源于網(wǎng)絡

電子槍按照上圖顯示的那樣,從上往下逐行掃描,掃描完成后,就顯示一幀的畫面,隨后電子槍回到初始位置。當電子槍換到新的一行時,會發(fā)出水平同步信號(Horizonal Synchronization),簡稱Hsync。而當一幀顯示完成后,電子槍回到初始位置,準備畫下一幀之前,顯示器會發(fā)出一個垂直同步信號(Vertical Synchronization),簡稱VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是VSync信號產生的頻率。盡管現(xiàn)在的設備大都是液晶顯示屏了,但原理仍然沒有變。
image.png

從上圖可以看出,如果幀緩存區(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)象,也增加了畫面流暢度,但是需要消費更多的計算資源,也會帶來部分延遲(屏幕卡頓)。

二、屏幕卡頓

圖片來源于網(wǎng)絡

VSync信號到來后,系統(tǒng)圖形服務會通過CADisplayLink等機制通知App,App主線程開始在 CPU中計算顯示內容,比如視圖的創(chuàng)建布局計算、圖片解碼文本繪制等。隨后CPU會將計算好的內容提交到GPU去,由GPU進行變換、合成渲染。隨后GPU會把渲染結果提交到幀緩沖區(qū)去,等待下一次VSync信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個VSync時間內,CPU或者GPU沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是屏幕卡頓的原因。

從上面的圖中可以看到,CPUGPU不論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時,也需要分別對CPUGPU壓力進行評估和優(yōu)化。

垂直同步信號與雙緩沖的意義

強制同步屏幕刷新,以掉幀為代價解決屏幕撕裂問題。

屏幕卡頓的本質

CPUGPU渲染流水線耗時過長,導致掉幀。

三、離屏渲染

APP通常的渲染流程:

image.png

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

而離屏渲染的流程:
image.png

與普通情況下GPU直接將渲染好的內容放入Framebuffer中不同,需要先額外創(chuàng)建離屏渲染緩沖區(qū) Offscreen Buffer,將提前渲染好的內容放入其中,等到合適的時機再將Offscreen Buffer中的內容進一步疊加、渲染,完成后將結果切換到Framebuffer中。

離屏渲染帶來的問題

離屏渲染時由于App需要提前對部分內容進行額外的渲染并保存到Offscreen Buffer,以及需要在必要時刻對Offscreen BufferFramebuffer進行內容切換,所以會需要更長的處理時間(實際上這兩步關于 buffer 的切換代價都非常大)。

并且Offscreen Buffer本身就需要額外的空間,大量的離屏渲染會造成內存過大的壓力。與此同時,Offscreen Buffer的總大小也有限,不能超過屏幕總像素的2.5倍。

可見離屏渲染的開銷非常大,一旦需要離屏渲染的內容過多,很容易造成掉幀的問題。所以大部分情況下,我們都應該盡量避免離屏渲染。

為什么需要離屏渲染

既然離屏渲染會帶來那么多的問題,那為什么又需要離屏渲染呢?

  • 一些特殊效果需要使用額外的Offscreen Buffer來保存渲染的中間狀態(tài),所以不得不使用離屏渲染。比如系統(tǒng)自動觸發(fā)的陰影、圓角等。
  • 出于效率的目的,可以將內容提前渲染保存在Offscreen Buffer中,達到復用的目的。
光柵化(shouldRasterize )

開啟光柵化后,會觸發(fā)離屏渲染Render Server會強制將CALayer的渲染位圖結果bitmap保存下來,這樣下次再需要渲染時就可以直接復用,從而提高效率。

而保存的bitmap包含layersubLayer、圓角陰影、組透明度 group opacity等,所以如果layer的構成包含上述幾種元素,結構復雜且需要反復利用,那么就可以考慮打開光柵化。

圓角、陰影組透明度等會由系統(tǒng)自動觸發(fā)離屏渲染,那么打開光柵化可以節(jié)約第二次及以后的渲染時間。而多層 subLayer的情況由于不會自動觸發(fā)離屏渲染,所以相比之下會多花費第一次離屏渲染的時間,但是可以節(jié)約后續(xù)的重復渲染的開銷。

不過使用光柵化的時候需要注意以下幾點:

  • 如果layer不能被復用,則沒有必要打開光柵化
  • 如果layer不是靜態(tài),需要被頻繁修改,比如處于動畫之中,那么開啟離屏渲染反而影響效率
    離屏渲染緩存內容有時間限制,緩存內容100ms內如果沒有被使用,那么就會被丟棄,無法進行復用
  • 離屏渲染緩存空間有限,超過2.5倍屏幕像素大小的話也會失效,無法復用
圓角的離屏渲染

圖片來源于網(wǎng)絡

layer由三層組成,我們設置圓角,會這樣做:

view.layer.cornerRadius = 2

而蘋果文檔指出,cornerRadius只會默認設置backgroundColorborder的圓角,而不會設置content的圓角,除非同時設置了layer.masksToBoundstrue(對應UIViewclipsToBounds屬性)

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以及所有subLayercontent都進行裁剪,所以會觸發(fā)離屏渲染。

離屏渲染的邏輯

圖層的疊加繪制大概遵循畫家算法,在這種算法下會按層繪制,首先繪制距離較遠的場景,然后用繪制距離較近的場景覆蓋較遠的部分。

圖片來源于網(wǎng)絡

在普通的layer繪制中,上層的sublayer會覆蓋下層的sublayer,下層sublayer繪制完之后就可以拋棄了,從而節(jié)約空間提高效率。所有sublayer依次繪制完畢之后,整個繪制過程完成,就可以進行后續(xù)的呈現(xiàn)了。
圖片來源于網(wǎng)絡

而當我們設置了cornerRadius以及masksToBounds進行圓角 + 裁剪時,如前文所述masksToBounds裁剪屬性會應用到所有sublayer上。這也就意味著所有sublayer必須要重新被應用一次圓角+裁剪,這也就意味著所有sublayer第一次被繪制完之后,并不能立刻被丟棄,而必須要被保存在 Offscreen buffer等待下一輪圓角+裁剪,這也就誘發(fā)了離屏渲染。
圖片來源于網(wǎng)絡

實際上不只是圓角+裁剪,如果設置了透明度+組透明(layer.allowsGroupOpacity+layer.opacity),陰影屬性(shadowOffset)等,都會產生類似的效果,因為組透明度陰影都是和裁剪類似的,會作用與layer以及其所有sublayer上,這就導致必然會引起離屏渲染

避免圓角離屏渲染

除了盡量減少圓角裁剪的使用,還有什么別的辦法可以避免圓角+裁剪引起的離屏渲染嗎?

由于剛才我們提到,圓角引起離屏渲染的本質是裁剪的疊加,導致masksToBoundslayer以及所有sublayer進行二次處理。那么我們只要避免使用masksToBounds進行二次處理,而是對所有的sublayer進行預處理,就可以只進行畫家算法,用一次疊加就完成繪制。

那么可行的實現(xiàn)方法大概有下面幾種:

  • 直接使用帶圓角的圖片,或者替換背景色為帶圓角的純色背景圖,從而避免使用圓角裁剪。不過這種方法需要依賴具體情況,并不通用。
  • 再增加一個和背景色相同的遮罩mask覆蓋在最上層,蓋住四個角,營造出圓角的形狀。但這種方式難以解決背景色為圖片或漸變色的情況。
  • 貝塞爾曲線繪制閉合帶圓角的矩形,在上下文中設置只有內部可見,再將不帶圓角的layer渲染成圖片,添加到貝塞爾矩形中。這種方法效率更高,但是layer的布局一旦改變,貝塞爾曲線都需要手動地重新繪制,所以需要對frame、color等進行手動地監(jiān)聽并重繪。
  • 重寫drawRect,用CoreGraphics相關方法,在需要應用圓角時進行手動繪制。不過CoreGraphics效率也很有限,如果需要多次調用也會有效率問題。
觸發(fā)離屏渲染原因的總結
  • 使用了masklayer(layer.mask)
  • 需要進行裁剪的layer(layer.masksToBounds / view.clipsToBounds)
  • 設置了組透明度YES,并且透明度不為1layer(layer.allowsGroupOpacity/layer.opacity)
  • 添加了投影的layer(layer.shadow)
  • 采用了光柵化的layer(layer.shouldRasterize)
  • 繪制了文字的layer(UILabel, CATextLayer, Core Text 等)
總結一下:設置圓角不一定會導致離屏渲染,離屏渲染不一定是由于設置圓角產生的

參考:iOS 渲染原理解析

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容