第一次認(rèn)識到CALayer是在某次面試時被問到“l(fā)ayer跟view是什么關(guān)系”童本,對layer的一些東西也在開發(fā)重逐漸了解,但是對于它缺少一個全面的認(rèn)識,所以對它進行一下全面的挖掘。
layer和view的關(guān)系
開始開發(fā)都是從view開始,而且很長一段時間可能都只認(rèn)識到view,而只會在某些角落看見layer旅赢,比如圓角,比如coreAnimation動畫,還有繪制內(nèi)容時也使用CALayer蜈亩,所以對于layer的首要疑問肯定是:這貨跟view到底什么關(guān)系?
來段文檔:
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提供了基礎(chǔ)設(shè)施前翎,使得繪制內(nèi)容和呈現(xiàn)更高效動畫更容易稚配、更低耗
- layer不參與view的事件處理、不參與響應(yīng)鏈
思考一下一個view在系統(tǒng)里起了什么作用:就是接受用戶點擊和呈現(xiàn)內(nèi)容港华。上面這段的意思就是layer負(fù)責(zé)了內(nèi)容呈現(xiàn)部分的工作道川,而不參與用戶點擊事件處理的工作。
很簡單很好記立宜,對view的理解也加深了冒萄。
內(nèi)容呈現(xiàn)
知道了layer的工作之后,接下來的疑問就是:內(nèi)容如何提供橙数?支持哪些內(nèi)容尊流?怎么呈現(xiàn)的?
翻開CALayer的api,跟內(nèi)容呈現(xiàn)最相關(guān)的幾個就是:
-
display
和setNeedsDisplay``displayIfNeeded
-
drawInContext:
和delegate里面的drawLayer:inContext:
等 - 屬性
contents
更新機制
第一組3個方法跟view里面的那一組類似商模,它們是相似的邏輯奠旺。首先一個內(nèi)容在layer上發(fā)生改變,比如顏色變了施流,要讓用戶立馬看到响疚,就需要圖形系統(tǒng)重新渲染。再試想一下瞪醋,有可能同時多個layer在很短的時間內(nèi)同時要刷新忿晕,比如打開一個新的結(jié)構(gòu)復(fù)雜viewController,比如快速滑動tableView時银受,這種場景并不特殊践盼。如果每個layer更新都要系統(tǒng)刷新一遍,那么會導(dǎo)致紊亂的幀率宾巍,有時特別卡有時又很閑咕幻。
所以機制是反過來的,系統(tǒng)有基本穩(wěn)定的刷新頻率顶霞,然后在layer內(nèi)容改變的時候肄程,把這個layer做個需要刷新的標(biāo)記锣吼,這就是setNeedsDisplay
,每次刷新時,把上次刷新之后被標(biāo)記的layer一次性全部提交給圖形系統(tǒng)蓝厌,所以這里還有一個東西玄叠,就是事務(wù)(CATransaction)。
layer刷新就是被調(diào)用display
拓提,但這個我們不主動調(diào)用读恃,讓系統(tǒng)調(diào)用,它可以把我更好的時機代态。我們只需要setNeedsDisplay
做標(biāo)記寺惫。如果你真的非常急需,就用displayIfNeeded
,對于已被標(biāo)記為Needed
的layer就立馬刷新胆数。
既提供了穩(wěn)定和諧的通用機制肌蜻,又照顧到了偶然的特殊需求互墓,很好必尼。
內(nèi)容提供方法
上面只是說了繪制時機的機制,真正的內(nèi)容繪制在第二組方法里篡撵,根據(jù)測試判莉,內(nèi)容提供的機制是這樣的:
display
- delegate的
displayLayer:
drawInContext:
- delegate的
drawLayer:inContext:
;
這四個方法,但凡有一個方法實現(xiàn)了育谬,就不會繼續(xù)往下進行了券盅,就認(rèn)為你已經(jīng)提供了內(nèi)容。delegate的方法要檢查delegate是否存在且是否實現(xiàn)了對應(yīng)的方法膛檀。
第1和第2個方法是對應(yīng)的锰镀,第3和第4個方法也是對應(yīng)的,前面兩個沒有構(gòu)建內(nèi)容緩沖區(qū)(Backing Store),需要直接提供contents
,一種方法就是直接賦值一個CAImageRef
:
layer.contents = [UIImage imageNamed:@"xxx"].CGImage;
后兩種方法咖刃,會給layer開辟一塊內(nèi)存用來存儲繪制的內(nèi)容泳炉,在這兩個方法里,可以使用CoreGraphics
的那套api來繪制需要的內(nèi)容嚎杨。
delegate的作用
從上面還可以搞清楚一個問題花鹅,就是layer的delegate的作用:delegate控制layer的內(nèi)容,這也是為什么UIView
自帶的layer的delegate是默認(rèn)指定到view自身的枫浙,而也因為這樣刨肃,絕大多數(shù)時候我們直接修改view的屬性(顏色位置透明度等等),layer的呈現(xiàn)就自動發(fā)生變化了箩帚。
layer和動畫的關(guān)系
在使用CoreAnimation的動畫的時候真友,是把創(chuàng)建的動畫放到layer上,而簡單的使用動畫紧帕,很多時候是使用[UIView animation...]
盔然,那么后者其實本質(zhì)是內(nèi)部建了一個動畫放到了layer上嗎?是的,動畫的載體就是layer轻纪,這就是它們的基本關(guān)系油额。但為了更高效的動畫,還有更多的細節(jié)刻帚。
如果你做過位移的動畫潦嘶,并且試著在動畫的過程里去輸出view的位置,你會驚訝的發(fā)現(xiàn):在動畫開始后崇众,view的frame就已經(jīng)是結(jié)束位置的值了掂僵!
按照常識理解,view的位置應(yīng)該是隨著時間不斷變化的顷歌,而這個理解上的錯差正是理解動畫內(nèi)核的一個好的窗口锰蓬。
從上面的現(xiàn)象至少可以得出一點:就是你眼睛看到的,跟系統(tǒng)里的數(shù)據(jù)不是一致的眯漩,動畫可能是一個欺騙把戲芹扭。
看段文檔:
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的內(nèi)容生成一個位圖(bitmap),觸發(fā)動畫的時候,是把這個動畫和狀態(tài)信息傳遞給圖形硬件赦抖,圖形硬件使用這兩個數(shù)據(jù)就可以構(gòu)造動畫了舱卡。處理位圖對于圖形硬件更快。
模擬一下動畫處理過程就是:一個很復(fù)雜的view的動畫队萤,是把它的layer的內(nèi)容合成一張圖片轮锥,然后要旋轉(zhuǎn),就是把這張圖旋轉(zhuǎn)一下顯示出來要尔。實際上圖形系統(tǒng)在渲染的過程里舍杜,對于旋轉(zhuǎn)、縮放赵辕、位移等既绩,只需要加一個矩陣就可以了(對應(yīng)就是transform
),對于圖形系統(tǒng)而言這些工作就是最基本的操作,非常高效匆帚。
所以動畫的呈現(xiàn)和view本身的的數(shù)據(jù)時分離的熬词,也就出現(xiàn)了動畫時看到的都是結(jié)束時的數(shù)據(jù)。
如果按照常識理解去實現(xiàn)動畫吸重,是怎么做互拾?
view移動,在界面刷新的方法里嚎幸,不斷的更新view的位置颜矿,每次更新完,把數(shù)據(jù)提供給圖形系統(tǒng)嫉晶,重新繪制骑疆。對于有復(fù)雜子視圖的view田篇,要把整個子視圖樹都全部重繪。
對比兩者箍铭,基于layer的欺騙性的動畫節(jié)省了什么泊柬?
- 不用不斷的更新view的數(shù)據(jù)
- 不用不斷的和圖形硬件交互數(shù)據(jù)
- 對于復(fù)雜的view,不用重繪整個圖層樹
- 處理這些對圖形硬件更擅長
能這么做的本質(zhì)原因我覺得還是因為我們需要的動畫是程式化的诈火,有模板兽赁、有套路的。哪怕是稍微復(fù)雜的動畫冷守,也可以用關(guān)鍵幀動畫來簡化刀崖,最后還是變成一個個離散獨立的數(shù)據(jù),按照既定的路線去呈現(xiàn)拍摇。如果動畫是即時計算出來的亮钦,就沒法這么干了,比如一個球扔到地上后怎么彈充活,是根據(jù)球的材料重量大小地面坡度等來計算的蜂莉。
圖層樹
上面的動畫系統(tǒng),也就催生了layer3種不同的圖層樹:
- 模型樹(model layer tree)堪唐,存儲了動畫的結(jié)束值
- 表現(xiàn)樹(presentation tree),包含了動畫正在進行中的值
- 渲染層(render tree),用來表現(xiàn)實際動畫的數(shù)據(jù)巡语,文檔無更多說明翎蹈,應(yīng)該是跟圖形系統(tǒng)相關(guān)的數(shù)據(jù)淮菠,比如提供給GPU的bitmap等。
如果要拿到動畫過程中view的數(shù)據(jù)荤堪,可以通過表現(xiàn)樹來獲取合陵。
性能問題
基本就是off-screen
離屏渲染的各種問題
1. 圓角
iOS9之后系統(tǒng)已優(yōu)化,不考慮澄阳。解決方案我認(rèn)為使用layer覆蓋層最好拥知,圓角問題本質(zhì)是mask,看下面mask部分碎赢。
2. 陰影低剔,解決方案:加上shadowPath
,替換shadowOffset
為什么使用shadowPath
可以解決這個問題,我沒有找到其他文章說這個肮塞,系統(tǒng)文檔也只有蛛絲馬跡襟齿,但根據(jù)各方面資料,我做了一個合理的推測枕赵。
label的陰影你會發(fā)現(xiàn)是跟隨文字變化的猜欺,而如果label有背景色,陰影就是根據(jù)外邊框來的拷窜。一個imageView,背景色為空开皿,然后使用一個有鏤空效果的圖片涧黄,就會發(fā)現(xiàn)陰影是跟著圖片那些不透明的那部分來的。
所以我推斷:陰影是根據(jù)layer的alpha值來生成的赋荆。模擬一下生成的過程:分配一塊同樣大小的shadowlayer,在原layer的alpha不為0的地方笋妥,shadowlayer填上shadowColor,就跟現(xiàn)實里的影子生成原理一樣窄潭,不透明的部分才生成陰影挽鞠。然后把這個shadowlayer做一個偏移(shadowOffset)加到原layer下面。
而且這個alpha不是指當(dāng)前l(fā)ayer的內(nèi)容狈孔,而是當(dāng)前l(fā)ayer和它所有的子layer合成后的alpha,也就是如果layer上面還是多個子layer信认,會把這些視圖合成到一起,再查看alpha值均抽。用多個imageView錯開疊加到一起就可測試出來嫁赏。
也就是陰影層是根據(jù)內(nèi)容即時計算出來的,而且會觸發(fā)離屏渲染油挥,所以消耗巨大潦蝇。
使用shadowPath之后,那么陰影層的形狀就固定了深寥,就類似于加了一個subLayer,不會觸發(fā)離屏渲染攘乒。
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
就是指當(dāng)前l(fā)ayer和所有子layer混合后的結(jié)果。有了上面的解釋惋鹅,這句話應(yīng)該就明白了则酝。
注:在iPhone6上還會卡頓,在8和X上已經(jīng)很流暢了
3. mask
直接使用CALayer
的mask
屬性會導(dǎo)致離屏渲染闰集,查看注釋
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作用的也不只是當(dāng)前l(fā)ayer的內(nèi)容沽讹,而是layer和它所有子layer的合成內(nèi)容。這個也是可以測試的武鲁,設(shè)置viewA的layer的mask,然后不管在viewA上加多少個視圖都是會被mask作用到爽雄。
解決方案是,添加一層layer在最上層來實現(xiàn)蒙版沐鼠。mask的效果是挚瘟,alpha>0的部分,內(nèi)容可以透出來饲梭,而為0的部分乘盖,內(nèi)容完全遮蔽。
可以添加一個alpha正好相反的maskLayer2在最上層排拷,根據(jù)混合效果侧漓,maskLayer2的alpha為0的地方內(nèi)容可以透出來,對應(yīng)就是原maskalpha>0的地方监氢,也是內(nèi)容可以透過來的地方布蔗。
唯一的麻煩就是對于內(nèi)容變化的視圖藤违,添加一個新視圖后,新視圖的內(nèi)容會跑到maskLayer2的上面纵揍,對這個新視圖就沒有蒙版效果了顿乒。
圓角的解決方案之一就是這個,之前圓角的本質(zhì)也是添加了mask泽谨,從而導(dǎo)致的離屏渲染璧榄。
4. shouldRasterize
光柵化
這個也是比說的,從前面的幾個性能問題里可以看出吧雹,性能問題主要因為兩點:1.離屏渲染 2.對復(fù)雜layer圖層每次都要重新計算合成內(nèi)容
光柵化的優(yōu)化是針對后一個問題的骨杂,比如有10個視圖,互相疊加在一起雄卷,每次都要計算疊加都得內(nèi)容搓蚪,開啟這個效果后,就把計算后的內(nèi)容生成一張位圖(bitmap),之后渲染引擎會緩存和重用這個位圖丁鹉,而避免重新計算妒潭。
舉個例子:前者就類似你要告訴一個人手機長什么樣子,然后你造了一臺手機給他看揣钦,每介紹給一個人你就要造一個手機雳灾;后者類似你把手機造好了之后拍了一張照,然后每次要介紹給別人冯凹,就給它看這個照片就好了谎亩。
缺點就是,如果樣式是不斷變化的谈竿,重用效果就會降低团驱,而且存儲位圖會增加內(nèi)存消耗。
實際測試:在tableView的cell上面添加文字的陰影空凸,然后文字是隨機變化的。陰影會導(dǎo)致離屏渲染寸痢,而文字的陰影又無法使用shadowPath來指定呀洲,所以會卡頓明顯。
- 開啟
shouldRasterize
之后效果顯著啼止。 - 文字是不是變化并沒有區(qū)別道逗,可能
shouldRasterize
的重用和變化的概念和內(nèi)容上的變化并不是一個意思。對于tableView而言献烦,新的cell都是沒得到重用的滓窍,在測試工具里顯示都是紅色 - 如果view開啟maskToBounds,效果很差巩那。雖然仍然只是新的cell得不到重用吏夯。只能說mask帶來的性能消耗太大
關(guān)于離屏渲染的猜測
經(jīng)過上面幾個觸發(fā)離屏渲染的屬性的認(rèn)知此蜈,發(fā)現(xiàn)一個共性,就是它們都需要layer和它的子圖層樹合成后的結(jié)果噪生。mask是這樣裆赵,陰影也是這樣,開啟shouldRasterize
之后也是這樣跺嗽。
假設(shè)正常的內(nèi)容是A战授,然后渲染出圖形GA,然后你要加一個B內(nèi)容,那么就是把內(nèi)容A和B的結(jié)果做一個混合(blend)就好了。
但是如果B的內(nèi)容是基于A呢桨嫁?你必須先把A渲染出來植兰,才能去生成B,那么在生成B的時候A存放在哪里璃吧?這就需要開辟一塊新的緩沖區(qū)(frame buffer)钉跷,把A的結(jié)果輸出到這個地方,而不能夠直接輸出到屏幕肚逸。然后在那個新的環(huán)境(context)爷辙,把A和B合成結(jié)束在切回到原來的context,在輸出到屏幕。
這就是我對離屏渲染流程和原因的猜測朦促。
更新1:
這里有個動畫是基于CAShapeLayer的膝晾,動畫調(diào)整的屬性是strokeStart
跟strokeEnd
,就是一個路徑只繪制指定的一部分,不斷修改這一部分形成動畫务冕。這個跟之前的layer形成bitmap傳給圖形系統(tǒng)再構(gòu)建動畫有沖突血当,因為形成bitmap后路徑數(shù)據(jù)就丟失了,不可能通過圖片+額外的簡單數(shù)據(jù)形成這個動畫禀忆。最可能的是CAShapeLayer
根據(jù)自身路徑和strokeStart``strokeEnd
兩個屬性計算頂點(vertex)數(shù)據(jù),然后傳給圖形系統(tǒng)繪制臊旭。不斷修改屬性,不斷繪制箩退。這可能是CAShapeLayer
針對自身做的特殊處理离熏,所以會跟CALayer的說法不一致。
每天學(xué)一點戴涝,快樂多一點滋戳,歡迎大家進入我的交流群761407670(備注123)
作者:FindCrt
鏈接:http://www.reibang.com/p/e3c118e56c9a