第一次認(rèn)識(shí)到CALayer是在某次面試時(shí)被問到“l(fā)ayer跟view是什么關(guān)系”狈网,對(duì)layer的一些東西也在開發(fā)重逐漸了解,但是對(duì)于它缺少一個(gè)全面的認(rèn)識(shí),所以對(duì)它進(jìn)行一下全面的挖掘。
layer和view的關(guān)系
開始開發(fā)都是從view開始遍尺,而且很長(zhǎng)一段時(shí)間可能都只認(rèn)識(shí)到view,而只會(huì)在某些角落看見layer,比如圓角叮姑,比如coreAnimation動(dòng)畫踢代,還有繪制內(nèi)容時(shí)也使用CALayer,所以對(duì)于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)更高效動(dòng)畫更容易客情、更低耗
- layer不參與view的事件處理其弊、不參與響應(yīng)鏈
思考一下一個(gè)view在系統(tǒng)里起了什么作用:就是接受用戶點(diǎn)擊和呈現(xiàn)內(nèi)容。上面這段的意思就是layer負(fù)責(zé)了內(nèi)容呈現(xiàn)部分的工作膀斋,而不參與用戶點(diǎn)擊事件處理的工作梭伐。
很簡(jiǎn)單很好記,對(duì)view的理解也加深了仰担。
內(nèi)容呈現(xiàn)
知道了layer的工作之后糊识,接下來的疑問就是:內(nèi)容如何提供?支持哪些內(nèi)容惰匙?怎么呈現(xiàn)的技掏?
翻開CALayer的api,跟內(nèi)容呈現(xiàn)最相關(guān)的幾個(gè)就是:
-
display
和setNeedsDisplay``displayIfNeeded
-
drawInContext:
和delegate里面的drawLayer:inContext:
等 - 屬性
contents
更新機(jī)制
第一組3個(gè)方法跟view里面的那一組類似,它們是相似的邏輯项鬼。首先一個(gè)內(nèi)容在layer上發(fā)生改變,比如顏色變了劲阎,要讓用戶立馬看到绘盟,就需要圖形系統(tǒng)重新渲染。再試想一下,有可能同時(shí)多個(gè)layer在很短的時(shí)間內(nèi)同時(shí)要刷新龄毡,比如打開一個(gè)新的結(jié)構(gòu)復(fù)雜viewController吠卷,比如快速滑動(dòng)tableView時(shí),這種場(chǎng)景并不特殊沦零。如果每個(gè)layer更新都要系統(tǒng)刷新一遍祭隔,那么會(huì)導(dǎo)致紊亂的幀率,有時(shí)特別卡有時(shí)又很閑路操。
所以機(jī)制是反過來的疾渴,系統(tǒng)有基本穩(wěn)定的刷新頻率,然后在layer內(nèi)容改變的時(shí)候屯仗,把這個(gè)layer做個(gè)需要刷新的標(biāo)記搞坝,這就是setNeedsDisplay
,每次刷新時(shí),把上次刷新之后被標(biāo)記的layer一次性全部提交給圖形系統(tǒng)魁袜,所以這里還有一個(gè)東西桩撮,就是事務(wù)(CATransaction)。
layer刷新就是被調(diào)用display
峰弹,但這個(gè)我們不主動(dòng)調(diào)用店量,讓系統(tǒng)調(diào)用,它可以把我更好的時(shí)機(jī)鞠呈。我們只需要setNeedsDisplay
做標(biāo)記垫桂。如果你真的非常急需,就用displayIfNeeded
,對(duì)于已被標(biāo)記為Needed
的layer就立馬刷新粟按。
既提供了穩(wěn)定和諧的通用機(jī)制诬滩,又照顧到了偶然的特殊需求,很好灭将。
內(nèi)容提供方法
上面只是說了繪制時(shí)機(jī)的機(jī)制疼鸟,真正的內(nèi)容繪制在第二組方法里,根據(jù)測(cè)試庙曙,內(nèi)容提供的機(jī)制是這樣的:
display
- delegate的
displayLayer:
drawInContext:
- delegate的
drawLayer:inContext:
;
這四個(gè)方法空镜,但凡有一個(gè)方法實(shí)現(xiàn)了,就不會(huì)繼續(xù)往下進(jìn)行了捌朴,就認(rèn)為你已經(jīng)提供了內(nèi)容吴攒。delegate的方法要檢查delegate是否存在且是否實(shí)現(xiàn)了對(duì)應(yīng)的方法。
第1和第2個(gè)方法是對(duì)應(yīng)的砂蔽,第3和第4個(gè)方法也是對(duì)應(yīng)的洼怔,前面兩個(gè)沒有構(gòu)建內(nèi)容緩沖區(qū)(Backing Store),需要直接提供contents
,一種方法就是直接賦值一個(gè)CAImageRef
:
layer.contents = [UIImage imageNamed:@"xxx"].CGImage;
后兩種方法,會(huì)給layer開辟一塊內(nèi)存用來存儲(chǔ)繪制的內(nèi)容左驾,在這兩個(gè)方法里镣隶,可以使用CoreGraphics
的那套api來繪制需要的內(nèi)容极谊。
delegate的作用
從上面還可以搞清楚一個(gè)問題,就是layer的delegate的作用:delegate控制layer的內(nèi)容安岂,這也是為什么UIView
自帶的layer的delegate是默認(rèn)指定到view自身的轻猖,而也因?yàn)檫@樣,絕大多數(shù)時(shí)候我們直接修改view的屬性(顏色位置透明度等等)域那,layer的呈現(xiàn)就自動(dòng)發(fā)生變化了咙边。
layer和動(dòng)畫的關(guān)系
在使用CoreAnimation的動(dòng)畫的時(shí)候,是把創(chuàng)建的動(dòng)畫放到layer上次员,而簡(jiǎn)單的使用動(dòng)畫败许,很多時(shí)候是使用[UIView animation...]
,那么后者其實(shí)本質(zhì)是內(nèi)部建了一個(gè)動(dòng)畫放到了layer上嗎翠肘?是的檐束,動(dòng)畫的載體就是layer,這就是它們的基本關(guān)系束倍。但為了更高效的動(dòng)畫被丧,還有更多的細(xì)節(jié)。
如果你做過位移的動(dòng)畫绪妹,并且試著在動(dòng)畫的過程里去輸出view的位置甥桂,你會(huì)驚訝的發(fā)現(xiàn):在動(dòng)畫開始后,view的frame就已經(jīng)是結(jié)束位置的值了邮旷!
按照常識(shí)理解黄选,view的位置應(yīng)該是隨著時(shí)間不斷變化的,而這個(gè)理解上的錯(cuò)差正是理解動(dòng)畫內(nèi)核的一個(gè)好的窗口婶肩。
從上面的現(xiàn)象至少可以得出一點(diǎn):就是你眼睛看到的办陷,跟系統(tǒng)里的數(shù)據(jù)不是一致的,動(dòng)畫可能是一個(gè)欺騙把戲律歼。
看段文檔:
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)容生成一個(gè)位圖(bitmap),觸發(fā)動(dòng)畫的時(shí)候民镜,是把這個(gè)動(dòng)畫和狀態(tài)信息傳遞給圖形硬件,圖形硬件使用這兩個(gè)數(shù)據(jù)就可以構(gòu)造動(dòng)畫了险毁。處理位圖對(duì)于圖形硬件更快制圈。
模擬一下動(dòng)畫處理過程就是:一個(gè)很復(fù)雜的view的動(dòng)畫,是把它的layer的內(nèi)容合成一張圖片畔况,然后要旋轉(zhuǎn)鲸鹦,就是把這張圖旋轉(zhuǎn)一下顯示出來。實(shí)際上圖形系統(tǒng)在渲染的過程里跷跪,對(duì)于旋轉(zhuǎn)馋嗜、縮放、位移等域庇,只需要加一個(gè)矩陣就可以了(對(duì)應(yīng)就是transform
),對(duì)于圖形系統(tǒng)而言這些工作就是最基本的操作嵌戈,非常高效覆积。
所以動(dòng)畫的呈現(xiàn)和view本身的的數(shù)據(jù)時(shí)分離的听皿,也就出現(xiàn)了動(dòng)畫時(shí)看到的都是結(jié)束時(shí)的數(shù)據(jù)熟呛。
如果按照常識(shí)理解去實(shí)現(xiàn)動(dòng)畫,是怎么做尉姨?
view移動(dòng)庵朝,在界面刷新的方法里,不斷的更新view的位置又厉,每次更新完九府,把數(shù)據(jù)提供給圖形系統(tǒng),重新繪制覆致。對(duì)于有復(fù)雜子視圖的view侄旬,要把整個(gè)子視圖樹都全部重繪。
對(duì)比兩者煌妈,基于layer的欺騙性的動(dòng)畫節(jié)省了什么儡羔?
- 不用不斷的更新view的數(shù)據(jù)
- 不用不斷的和圖形硬件交互數(shù)據(jù)
- 對(duì)于復(fù)雜的view,不用重繪整個(gè)圖層樹
- 處理這些對(duì)圖形硬件更擅長(zhǎng)
能這么做的本質(zhì)原因我覺得還是因?yàn)槲覀冃枰膭?dòng)畫是程式化的璧诵,有模板汰蜘、有套路的。哪怕是稍微復(fù)雜的動(dòng)畫之宿,也可以用關(guān)鍵幀動(dòng)畫來簡(jiǎn)化族操,最后還是變成一個(gè)個(gè)離散獨(dú)立的數(shù)據(jù),按照既定的路線去呈現(xiàn)比被。如果動(dòng)畫是即時(shí)計(jì)算出來的色难,就沒法這么干了,比如一個(gè)球扔到地上后怎么彈等缀,是根據(jù)球的材料重量大小地面坡度等來計(jì)算的枷莉。
圖層樹
上面的動(dòng)畫系統(tǒng),也就催生了layer3種不同的圖層樹:
- 模型樹(model layer tree)项滑,存儲(chǔ)了動(dòng)畫的結(jié)束值
- 表現(xiàn)樹(presentation tree),包含了動(dòng)畫正在進(jìn)行中的值
- 渲染層(render tree),用來表現(xiàn)實(shí)際動(dòng)畫的數(shù)據(jù)依沮,文檔無更多說明,應(yīng)該是跟圖形系統(tǒng)相關(guān)的數(shù)據(jù)枪狂,比如提供給GPU的bitmap等危喉。
如果要拿到動(dòng)畫過程中view的數(shù)據(jù),可以通過表現(xiàn)樹來獲取州疾。
性能問題
基本就是off-screen
離屏渲染的各種問題
1. 圓角
iOS9之后系統(tǒng)已優(yōu)化辜限,不考慮。解決方案我認(rèn)為使用layer覆蓋層最好严蓖,圓角問題本質(zhì)是mask薄嫡,看下面mask部分氧急。
2. 陰影,解決方案:加上shadowPath
,替換shadowOffset
為什么使用shadowPath
可以解決這個(gè)問題毫深,我沒有找到其他文章說這個(gè)吩坝,系統(tǒng)文檔也只有蛛絲馬跡,但根據(jù)各方面資料哑蔫,我做了一個(gè)合理的推測(cè)钉寝。
label的陰影你會(huì)發(fā)現(xiàn)是跟隨文字變化的,而如果label有背景色闸迷,陰影就是根據(jù)外邊框來的嵌纲。一個(gè)imageView,背景色為空,然后使用一個(gè)有鏤空效果的圖片腥沽,就會(huì)發(fā)現(xiàn)陰影是跟著圖片那些不透明的那部分來的逮走。
所以我推斷:陰影是根據(jù)layer的alpha值來生成的。模擬一下生成的過程:分配一塊同樣大小的shadowlayer,在原layer的alpha不為0的地方今阳,shadowlayer填上shadowColor师溅,就跟現(xiàn)實(shí)里的影子生成原理一樣,不透明的部分才生成陰影酣栈。然后把這個(gè)shadowlayer做一個(gè)偏移(shadowOffset)加到原layer下面险胰。
而且這個(gè)alpha不是指當(dāng)前l(fā)ayer的內(nèi)容,而是當(dāng)前l(fā)ayer和它所有的子layer合成后的alpha,也就是如果layer上面還是多個(gè)子layer矿筝,會(huì)把這些視圖合成到一起起便,再查看alpha值。用多個(gè)imageView錯(cuò)開疊加到一起就可測(cè)試出來窖维。
也就是陰影層是根據(jù)內(nèi)容即時(shí)計(jì)算出來的榆综,而且會(huì)觸發(fā)離屏渲染,所以消耗巨大铸史。
使用shadowPath之后鼻疮,那么陰影層的形狀就固定了,就類似于加了一個(gè)subLayer,不會(huì)觸發(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上還會(huì)卡頓挪哄,在8和X上已經(jīng)很流暢了
3. mask
直接使用CALayer
的mask
屬性會(huì)導(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)容迹炼。這個(gè)也是可以測(cè)試的,設(shè)置viewA的layer的mask,然后不管在viewA上加多少個(gè)視圖都是會(huì)被mask作用到。
解決方案是斯入,添加一層layer在最上層來實(shí)現(xiàn)蒙版砂碉。mask的效果是,alpha>0的部分刻两,內(nèi)容可以透出來增蹭,而為0的部分,內(nèi)容完全遮蔽闹伪。
可以添加一個(gè)alpha正好相反的maskLayer2在最上層沪铭,根據(jù)混合效果壮池,maskLayer2的alpha為0的地方內(nèi)容可以透出來偏瓤,對(duì)應(yīng)就是原maskalpha>0的地方,也是內(nèi)容可以透過來的地方椰憋。
唯一的麻煩就是對(duì)于內(nèi)容變化的視圖厅克,添加一個(gè)新視圖后,新視圖的內(nèi)容會(huì)跑到maskLayer2的上面橙依,對(duì)這個(gè)新視圖就沒有蒙版效果了证舟。
圓角的解決方案之一就是這個(gè),之前圓角的本質(zhì)也是添加了mask窗骑,從而導(dǎo)致的離屏渲染女责。
4. shouldRasterize
光柵化
這個(gè)也是比說的,從前面的幾個(gè)性能問題里可以看出创译,性能問題主要因?yàn)閮牲c(diǎn):1.離屏渲染 2.對(duì)復(fù)雜layer圖層每次都要重新計(jì)算合成內(nèi)容
光柵化的優(yōu)化是針對(duì)后一個(gè)問題的抵知,比如有10個(gè)視圖,互相疊加在一起软族,每次都要計(jì)算疊加都得內(nèi)容刷喜,開啟這個(gè)效果后,就把計(jì)算后的內(nèi)容生成一張位圖(bitmap),之后渲染引擎會(huì)緩存和重用這個(gè)位圖立砸,而避免重新計(jì)算掖疮。
舉個(gè)例子:前者就類似你要告訴一個(gè)人手機(jī)長(zhǎng)什么樣子,然后你造了一臺(tái)手機(jī)給他看颗祝,每介紹給一個(gè)人你就要造一個(gè)手機(jī)浊闪;后者類似你把手機(jī)造好了之后拍了一張照,然后每次要介紹給別人螺戳,就給它看這個(gè)照片就好了搁宾。
缺點(diǎn)就是,如果樣式是不斷變化的温峭,重用效果就會(huì)降低猛铅,而且存儲(chǔ)位圖會(huì)增加內(nèi)存消耗。
實(shí)際測(cè)試:在tableView的cell上面添加文字的陰影凤藏,然后文字是隨機(jī)變化的奸忽。陰影會(huì)導(dǎo)致離屏渲染堕伪,而文字的陰影又無法使用shadowPath來指定,所以會(huì)卡頓明顯栗菜。
- 開啟
shouldRasterize
之后效果顯著欠雌。 - 文字是不是變化并沒有區(qū)別,可能
shouldRasterize
的重用和變化的概念和內(nèi)容上的變化并不是一個(gè)意思疙筹。對(duì)于tableView而言富俄,新的cell都是沒得到重用的,在測(cè)試工具里顯示都是紅色 - 如果view開啟maskToBounds而咆,效果很差霍比。雖然仍然只是新的cell得不到重用。只能說mask帶來的性能消耗太大
關(guān)于離屏渲染的猜測(cè)
經(jīng)過上面幾個(gè)觸發(fā)離屏渲染的屬性的認(rèn)知暴备,發(fā)現(xiàn)一個(gè)共性悠瞬,就是它們都需要layer和它的子圖層樹合成后的結(jié)果。mask是這樣涯捻,陰影也是這樣浅妆,開啟shouldRasterize
之后也是這樣。
假設(shè)正常的內(nèi)容是A障癌,然后渲染出圖形GA,然后你要加一個(gè)B內(nèi)容,那么就是把內(nèi)容A和B的結(jié)果做一個(gè)混合(blend)就好了凌外。
但是如果B的內(nèi)容是基于A呢?你必須先把A渲染出來涛浙,才能去生成B康辑,那么在生成B的時(shí)候A存放在哪里?這就需要開辟一塊新的緩沖區(qū)(frame buffer)蝗拿,把A的結(jié)果輸出到這個(gè)地方晾捏,而不能夠直接輸出到屏幕。然后在那個(gè)新的環(huán)境(context)哀托,把A和B合成結(jié)束在切回到原來的context,在輸出到屏幕惦辛。
這就是我對(duì)離屏渲染流程和原因的猜測(cè)。
更新1:
這里有個(gè)動(dòng)畫是基于CAShapeLayer的仓手,動(dòng)畫調(diào)整的屬性是strokeStart
跟strokeEnd
,就是一個(gè)路徑只繪制指定的一部分胖齐,不斷修改這一部分形成動(dòng)畫。這個(gè)跟之前的layer形成bitmap傳給圖形系統(tǒng)再構(gòu)建動(dòng)畫有沖突嗽冒,因?yàn)樾纬蒪itmap后路徑數(shù)據(jù)就丟失了呀伙,不可能通過圖片+額外的簡(jiǎn)單數(shù)據(jù)形成這個(gè)動(dòng)畫。最可能的是CAShapeLayer
根據(jù)自身路徑和strokeStart``strokeEnd
兩個(gè)屬性計(jì)算頂點(diǎn)(vertex)數(shù)據(jù),然后傳給圖形系統(tǒng)繪制添坊。不斷修改屬性剿另,不斷繪制。這可能是CAShapeLayer
針對(duì)自身做的特殊處理,所以會(huì)跟CALayer的說法不一致雨女。