認識CALayer

layer和view的關係

開始開發都是從view開始,並且很長一段時間可能都只認識到view,而只會在某些角落看見layer,好比圓角,好比coreAnimation動畫,還有繪製內容時也使用CALayer,因此對於layer的首要疑問確定是:這貨跟view到底什麼關係?面試

出自 WWDC 2012- iOS App Performance- Graphics and Animations.png

來段文檔:api

Layers provide infrastructure for your views. Specifically, layers make it easier and more efficient to draw and animate the contents of views and maintain high frame rates while doing so. However, there are many things that layers do not do. Layers do not handle events, draw content, participate in the responder chain, or do many other things
  • layer給view提供了基礎設施,使得繪製內容和呈現更高效動畫更容易、更低耗
  • layer不參與view的事件處理、不參與響應鏈

思考一下一個view在系統裏起了什麼做用:就是接受用戶點擊和呈現內容。上面這段的意思就是layer負責了內容呈現部分的工做,而不參與用戶點擊事件處理的工做。緩存

很簡單很好記,對view的理解也加深了。app

內容呈現

知道了layer的工做以後,接下來的疑問就是:內容如何提供?支持哪些內容?怎麼呈現的?ide

翻開CALayer的api,跟內容呈現最相關的幾個就是:工具

  • displaysetNeedsDisplay`displayIfNeeded`
  • drawInContext:和delegate裏面的drawLayer:inContext:
  • 屬性contents
更新機制

第一組3個方法跟view裏面的那一組相似,它們是類似的邏輯。首先一個內容在layer上發生改變,好比顏色變了,要讓用戶立馬看到,就須要圖形系統從新渲染。再試想一下,有可能同時多個layer在很短的時間內同時要刷新,好比打開一個新的結構複雜viewController,好比快速滑動tableView時,這種場景並不特殊。若是每一個layer更新都要系統刷新一遍,那麼會致使紊亂的幀率,有時特別卡有時又很閒。性能

因此機制是反過來的,系統有基本穩定的刷新頻率,而後在layer內容改變的時候,把這個layer作個須要刷新的標記,這就是setNeedsDisplay,每次刷新時,把上次刷新以後被標記的layer一次性所有提交給圖形系統,因此這裏還有一個東西,就是事務(CATransaction)學習

layer刷新就是被調用display,但這個咱們不主動調用,讓系統調用,它能夠把我更好的時機。咱們只須要setNeedsDisplay作標記。若是你真的很是急需,就用displayIfNeeded,對於已被標記爲Needed的layer就立馬刷新。測試

既提供了穩定和諧的通用機制,又照顧到了偶然的特殊需求,很好。優化

內容提供方法

上面只是說了繪製時機的機制,真正的內容繪製在第二組方法裏,根據測試,內容提供的機制是這樣的:

  • display
  • delegate的displayLayer:
  • drawInContext:
  • delegate的drawLayer:inContext:;

這四個方法,但凡是有一個方法實現了,就不會繼續往下進行了,就認爲你已經提供了內容。delegate的方法要檢查delegate是否存在且是否實現了對應的方法。

第1和第2個方法是對應的,第3和第4個方法也是對應的,前面兩個沒有構建內容緩衝區(Backing Store),須要直接提供contents,一種方法就是直接賦值一個CAImageRef:

layer.contents = [UIImage imageNamed:@"xxx"].CGImage;

後兩種方法,會給layer開闢一塊內存用來存儲繪製的內容,在這兩個方法裏,可使用CoreGraphics的那套api來繪製須要的內容。

delegate的做用

從上面還能夠搞清楚一個問題,就是layer的delegate的做用:delegate控制layer的內容,這也是爲何UIView自帶的layer的delegate是默認指定到view自身的,而也由於這樣,絕大多數時候咱們直接修改view的屬性(顏色位置透明度等等),layer的呈現就自動發生變化了。

layer和動畫的關係

在使用CoreAnimation的動畫的時候,是把建立的動畫放到layer上,而簡單的使用動畫,不少時候是使用[UIView animation...],那麼後者其實本質是內部建了一個動畫放到了layer上嗎?是的,動畫的載體就是layer,這就是它們的基本關係。但爲了更高效的動畫,還有更多的細節。

若是你作過位移的動畫,而且試着在動畫的過程裏去輸出view的位置,你會驚訝的發現:在動畫開始後,view的frame就已是結束位置的值了!

按照常識理解,view的位置應該是隨着時間不斷變化的,而這個理解上的錯差正是理解動畫內核的一個好的窗口。

從上面的現象至少能夠得出一點:就是你眼睛看到的,跟系統裏的數據不是一致的,動畫多是一個欺騙把戲。

看段文檔:

Instead, a layer captures the content your app provides and caches it in a bitmap, which is sometimes referred to as the backing store. ... When a change triggers an animation, Core Animation passes the layer’s bitmap and state information to the graphics hardware, which does the work of rendering the bitmap using the new information. Manipulating the bitmap in hardware yields much faster animations than could be done in software.

這段話的含義是:layer的內容生成一個位圖(bitmap),觸發動畫的時候,是把這個動畫和狀態信息傳遞給圖形硬件,圖形硬件使用這兩個數據就能夠構造動畫了。處理位圖對於圖形硬件更快。

模擬一下動畫處理過程就是:一個很複雜的view的動畫,是把它的layer的內容合成一張圖片,而後要旋轉,就是把這張圖旋轉一下顯示出來。實際上圖形系統在渲染的過程裏,對於旋轉、縮放、位移等,只須要加一個矩陣就能夠了(對應就是transform),對於圖形系統而言這些工做就是最基本的操做,很是高效。

因此動畫的呈現和view自己的的數據時分離的,也就出現了動畫時看到的都是結束時的數據。

若是按照常識理解去實現動畫,是怎麼作?

view移動,在界面刷新的方法裏,不斷的更新view的位置,每次更新完,把數據提供給圖形系統,從新繪製。對於有複雜子視圖的view,要把整個子視圖樹都所有重繪。

對比二者,基於layer的欺騙性的動畫節省了什麼?

  • 不用不斷的更新view的數據
  • 不用不斷的和圖形硬件交互數據
  • 對於複雜的view,不用重繪整個圖層樹
  • 處理這些對圖形硬件更擅長

能這麼作的本質緣由我以爲仍是由於咱們須要的動畫是程式化的,有模板、有套路的。哪怕是稍微複雜的動畫,也能夠用關鍵幀動畫來簡化,最後仍是變成一個個離散獨立的數據,按照既定的路線去呈現。若是動畫是即時計算出來的,就無法這麼幹了,好比一個球扔到地上後怎麼彈,是根據球的材料重量大小地面坡度等來計算的。

圖層樹

上面的動畫系統,也就催生了layer3種不一樣的圖層樹:

  • 模型樹(model layer tree),存儲了動畫的結束值
  • 表現樹(presentation tree),包含了動畫正在進行中的值
  • 渲染層(render tree),用來表現實際動畫的數據,文檔無更多說明,應該是跟圖形系統相關的數據,好比提供給GPU的bitmap等。

若是要拿到動畫過程當中view的數據,能夠經過表現樹來獲取。

性能問題

基本就是off-screen離屏渲染的各類問題

1. 圓角

iOS9以後系統已優化,不考慮。解決方案我認爲使用layer覆蓋層最好,圓角問題本質是mask,看下面mask部分。

2. 陰影,解決方案:加上shadowPath,替換shadowOffset

爲何使用shadowPath能夠解決這個問題,我沒有找到其餘文章說這個,系統文檔也只有蛛絲馬跡,但根據各方面資料,我作了一個合理的推測。

label的陰影你會發現是跟隨文字變化的,而若是label有背景色,陰影就是根據外邊框來的。一個imageView,背景色爲空,而後使用一個有鏤空效果的圖片,就會發現陰影是跟着圖片那些不透明的那部分來的。

文字陰影

鏤空圖片陰影

因此我推斷:陰影是根據layer的alpha值來生成的。模擬一下生成的過程:分配一塊一樣大小的shadowlayer,在原layer的alpha不爲0的地方,shadowlayer填上shadowColor,就跟現實裏的影子生成原理同樣,不透明的部分才生成陰影。而後把這個shadowlayer作一個偏移(shadowOffset)加到原layer下面。

並且這個alpha不是指當前layer的內容,而是當前layer和它全部的子layer合成後的alpha,也就是若是layer上面仍是多個子layer,會把這些視圖合成到一塊兒,再查看alpha值。用多個imageView錯開疊加到一塊兒就可測試出來。

也就是陰影層是根據內容即時計算出來的,並且會觸發離屏渲染,因此消耗巨大。

使用shadowPath以後,那麼陰影層的形狀就固定了,就相似於加了一個subLayer,不會觸發離屏渲染。

shadowPath的註釋:

If you specify a value for this property, the layer creates its shadow using the specified path instead of the layer’s composited alpha channel

這裏的composited就是指當前layer和全部子layer混合後的結果。有了上面的解釋,這句話應該就明白了。

注:在iPhone6上還會卡頓,在8和X上已經很流暢了

3. mask

直接使用CALayermask屬性會致使離屏渲染,查看註釋

A layer whose alpha channel is used as a mask to select between the layer's background and the result of compositing the layer's contents with its filtered background

mask做用的也不僅是當前layer的內容,而是layer和它全部子layer的合成內容。這個也是能夠測試的,設置viewA的layer的mask,而後無論在viewA上加多少個視圖都是會被mask做用到。

解決方案是,添加一層layer在最上層來實現蒙版。mask的效果是,alpha>0的部分,內容能夠透出來,而爲0的部分,內容徹底遮蔽。

能夠添加一個alpha正好相反的maskLayer2在最上層,根據混合效果,maskLayer2的alpha爲0的地方內容能夠透出來,對應就是原maskalpha>0的地方,也是內容能夠透過來的地方。

惟一的麻煩就是對於內容變化的視圖,添加一個新視圖後,新視圖的內容會跑到maskLayer2的上面,對這個新視圖就沒有蒙版效果了。

圓角的解決方案之一就是這個,以前圓角的本質也是添加了mask,從而致使的離屏渲染。

4. shouldRasterize光柵化

這個也是比說的,從前面的幾個性能問題裏能夠看出,性能問題主要由於兩點:1.離屏渲染 2.對複雜layer圖層每次都要從新計算合成內容

光柵化的優化是針對後一個問題的,好比有10個視圖,互相疊加在一塊兒,每次都要計算疊加都得內容,開啓這個效果後,就把計算後的內容生成一張位圖(bitmap),以後渲染引擎會緩存和重用這個位圖,而避免從新計算。

舉個例子:前者就相似你要告訴一我的手機長什麼樣子,而後你造了一臺手機給他看,每介紹給一我的你就要造一個手機;後者相似你把手機造好了以後拍了一張照,而後每次要介紹給別人,就給它看這個照片就行了。

缺點就是,若是樣式是不斷變化的,重用效果就會下降,並且存儲位圖會增長內存消耗。

實際測試:在tableView的cell上面添加文字的陰影,而後文字是隨機變化的。陰影會致使離屏渲染,而文字的陰影又沒法使用shadowPath來指定,因此會卡頓明顯。

  • 開啓shouldRasterize以後效果顯著。
  • 文字是否是變化並無區別,可能shouldRasterize的重用和變化的概念和內容上的變化並非一個意思。對於tableView而言,新的cell都是沒獲得重用的,在測試工具裏顯示都是紅色
  • 若是view開啓maskToBounds,效果不好。雖然仍然只是新的cell得不到重用。只能說mask帶來的性能消耗太大
關於離屏渲染的猜想

通過上面幾個觸發離屏渲染的屬性的認知,發現一個共性,就是它們都須要layer和它的子圖層樹合成後的結果。mask是這樣,陰影也是這樣,開啓shouldRasterize以後也是這樣。

假設正常的內容是A,而後渲染出圖形GA,而後你要加一個B內容,那麼就是把內容A和B的結果作一個混合(blend)就行了。

可是若是B的內容是基於A呢?你必須先把A渲染出來,才能去生成B,那麼在生成B的時候A存放在哪裏?這就須要開闢一塊新的緩衝區(frame buffer),把A的結果輸出到這個地方,而不可以直接輸出到屏幕。而後在那個新的環境(context),把A和B合成結束在切回到原來的context,在輸出到屏幕。

這就是我對離屏渲染流程和緣由的猜想。

原文地址


推薦文集

* 抖音效果實現

* 音視頻學習從零到整

* iOS面試題合集

相關文章
相關標籤/搜索