iOS 視圖---動(dòng)畫(huà)渲染機(jī)制探究
終端的開(kāi)發(fā),首當(dāng)其沖的就是視圖、動(dòng)畫(huà)的渲染,切換等等。用戶(hù)使用 App 時(shí)最直接的體驗(yàn)就是這個(gè)界面好不好看,動(dòng)畫(huà)炫不炫,滑動(dòng)流不流暢。UI就是 App 的門(mén)面,它的體驗(yàn)伴隨著用戶(hù)使用 App 的整個(gè)過(guò)程。如果UI失敗,用戶(hù)是不會(huì)有打開(kāi)第二次的欲望的。
iOS 為開(kāi)發(fā)者提供了豐富的 Framework(UIKit,Core Animation,Core Graphic,OpenGL 等等)來(lái)滿(mǎn)足開(kāi)發(fā)從上層到底層各種各樣的需求。不得不說(shuō)蘋(píng)果很牛逼,很多接口你根本不需要理解背后的原理就能上手使用并且滿(mǎn)足你大部分的需求,但是,如果遇到性能問(wèn)題就容易抓瞎。易用性跟優(yōu)化就是個(gè)矛盾體,就像 ARC 一樣,當(dāng)你沒(méi)有遇到內(nèi)存問(wèn)題的時(shí)候用得很爽,一旦遇到了,就要要求你比在用 MRC 的時(shí)候更加了解 iOS 的內(nèi)存機(jī)制。UI 亦是如此。況且,作為鵝廠(chǎng)的員工當(dāng)然不能僅限于知道怎么用。我們要知其然還要知其所以然。好了,廢話(huà)不說(shuō),我們進(jìn)入主題:看看 iOS 是如何渲染視圖和動(dòng)畫(huà)的,以及在我們遇到渲染的性能問(wèn)題時(shí)怎么做優(yōu)化。
(注意:以下內(nèi)容是筆者的一些踩坑經(jīng)驗(yàn)和總結(jié), 歡迎探討?。?/p>
先來(lái)看看官方對(duì) Core Animation 的一段說(shuō)明:
可以看出iOS渲染視圖的核心是 Core Animation。從底層到上層依此是 GPU->(OpenGL、Core Graphic) -> Core Animation -> UIKit。
在 iOS上,動(dòng)畫(huà)和視圖的渲染其實(shí)是在另外一個(gè)進(jìn)程做的(下面我們叫這個(gè)進(jìn)程 render server),在 iOS 5 以前這個(gè)進(jìn)程叫 SpringBoard,在 iOS 6 之后叫 BackBoard。
下面這幅圖是使用項(xiàng)目錄制視頻的時(shí)候(大量視圖渲染),整個(gè)系統(tǒng)的進(jìn)程情況:
可以很清楚地看到 BackBoard 這個(gè)進(jìn)程的情況。
iOS 上視圖或者動(dòng)畫(huà)渲染的各個(gè)階段:
在 APP 內(nèi)部的有4個(gè)階段:
-
布局:在這個(gè)階段,程序設(shè)置 View / Layer 的層級(jí)信息,設(shè)置 layer 的屬性,如 frame,background color 等等。
-
創(chuàng)建 backing image:在這個(gè)階段程序會(huì)創(chuàng)建 layer 的 backing image,無(wú)論是通過(guò) setContents 將一個(gè) image 傳給 layer,還是通過(guò) [drawRect:] 或 [drawLayer: inContext:] 來(lái)畫(huà)出來(lái)的。所以 [drawRect:] 等函數(shù)是在這個(gè)階段被調(diào)用的。
-
準(zhǔn)備:在這個(gè)階段,Core Animation 框架準(zhǔn)備要渲染的 layer 的各種屬性數(shù)據(jù),以及要做的動(dòng)畫(huà)的參數(shù),準(zhǔn)備傳遞給 render server。同時(shí)在這個(gè)階段也會(huì)解壓要渲染的 image。(除了用 imageNamed:方法從 bundle 加載的 image 會(huì)立刻解壓之外,其他的比如直接從硬盤(pán)讀入,或者從網(wǎng)絡(luò)上下載的 image 不會(huì)立刻解壓,只有在真正要渲染的時(shí)候才會(huì)解壓)。
-
提交:在這個(gè)階段,Core Animation 打包 layer 的信息以及需要做的動(dòng)畫(huà)的參數(shù),通過(guò) IPC(inter-Process Communication)傳遞給 render server。
在 APP 外部的2個(gè)階段:
當(dāng)這些數(shù)據(jù)到達(dá) render server 后,會(huì)被反序列化成 render tree。然后 render server 會(huì)做下面的兩件事:
-
根據(jù) layer 的各種屬性(如果是動(dòng)畫(huà)的,會(huì)計(jì)算動(dòng)畫(huà) layer 的屬性的中間值),用 OpenGL 準(zhǔn)備渲染。
-
渲染這些可視的 layer 到屏幕。
如果做動(dòng)畫(huà)的話(huà),最后的兩個(gè)步驟會(huì)一直重復(fù)知道動(dòng)畫(huà)結(jié)束。
我們都知道 iOS 設(shè)備的屏幕刷新頻率是 60HZ。如果上面的這些步驟在一個(gè)刷新周期之內(nèi)無(wú)法做完(1/60s),就會(huì)造成掉幀。
我們看看有哪些操作可能會(huì)過(guò)度消耗 CPU 或者 GPU,從而造成掉幀。
-
視圖上有太多的 layer 或者幾何形狀:
如果視圖的層級(jí)結(jié)構(gòu)太復(fù)雜的話(huà),當(dāng)某些視圖被渲染或者 frame 被修改的話(huà),CPU 會(huì)花比較多得時(shí)間去重新計(jì)算 frame。尤其如果用 autolayout 的話(huà),會(huì)更消耗 CPU。同時(shí)過(guò)多的幾何結(jié)構(gòu)會(huì)大大增多需要渲染的 OpenGL triangles 以及柵格化的操作(將 OpenGL 的 triangles 轉(zhuǎn)化成像素)
-
太多的 overdraw:overdraw 是指一個(gè)像素點(diǎn)被多次地用顏色填充。這個(gè)主要是由于一些半透明的 layer 相互重疊造成的。GPU 的 fill-rate(用顏色填充像素的速率)是有限的。如果 overdraw 太多的話(huà),勢(shì)必會(huì)降低 GPU 的性能。
-
視圖的延后載入:iOS 只有在展示 viewcontroller 的 view 或者訪(fǎng)問(wèn) viewcontroller 的 view,比如說(shuō) someviewcontroller.view 的時(shí)候才會(huì)加載view。如果在用戶(hù)點(diǎn)擊了某個(gè) button,并且在 button 的響應(yīng)函數(shù)里做了很多消耗 cpu 的工作,這個(gè)時(shí)候如果 present 某個(gè) viewcontroller 的話(huà),會(huì)容易卡頓,尤其是如果viewcontroller 要從 database 里獲取數(shù)據(jù),或者從 nib 文件初始化 view 或者加載圖片會(huì)更卡。
-
離屏的繪制:離屏的繪制有兩種情況:1. 有些效果(如 rounded corners,layer masks,drop shadows 和 layer rasterization)不能直接的繪制到屏幕上,必須先繪制到一個(gè) offscreen 的 image context 上,這種操作會(huì)引入額外的內(nèi)存和 CPU 消耗。2. 實(shí)現(xiàn)了 drawRect 或者 drawLayer:inContext:,為了支持任意的繪制,core graphic 會(huì)創(chuàng)建一個(gè)大小跟要畫(huà)的 view 一樣的 backing image。并且當(dāng)畫(huà)完的以后要傳輸?shù)?render server 上渲染。所以沒(méi)事不要重載 drawRect 等函數(shù)卻什么都不做。
-
圖片解壓:用 imageNamed:從 bundle 里加載會(huì)立馬解壓。一般的情況是在賦值給 UIImageView 的 image 或者 layer 的 contents 或者畫(huà)到一個(gè) core graphic context 里才會(huì)解壓。
渲染性能優(yōu)化的注意點(diǎn):
隱藏的繪制:catextlayer 和 uilabel 都是將 text 畫(huà)入 backing image 的。如果改了一個(gè)包含 text 的 view 的 frame 的話(huà),text 會(huì)被重新繪制。
Rasterize:當(dāng)使用 layer 的 shouldRasterize 的時(shí)候(記得設(shè)置適當(dāng)?shù)?laye r的 rasterizationScale),layer 會(huì)被強(qiáng)制繪制到一個(gè) offscreen image 上,并且會(huì)被緩存起來(lái)。這種方法可以用來(lái)緩存繪制耗時(shí)(比如有比較絢的效果)但是不經(jīng)常改的 layer,如果 layer 經(jīng)常變,就不適合用。
離屏繪制: 使用 Rounded corner, layer masks, drop shadows 的效果可以使用 stretchable images。比如實(shí)現(xiàn) rounded corner,可以將一個(gè)圓形的圖片賦值于 layer 的 content 的屬性。并且設(shè)置好 contentsCenter 和 contentScale 屬性。
Blending and Overdraw :如果一個(gè) layer 被另一個(gè) layer 完全遮蓋,GPU 會(huì)做優(yōu)化不渲染被遮蓋的 layer,但是計(jì)算一個(gè) layer 是否被另一個(gè) layer 完全遮蓋是很耗 cpu 的。將幾個(gè)半透明的 layer 的 color 融合在一起也是很消耗的。
我們要做的:
-
設(shè)置 view 的 backgroundColor 為一個(gè)固定的,不透明的 color。
-
如果一個(gè) view 是不透明的,設(shè)置 opaque 屬性為 YES。(直接告訴程序這個(gè)是不透明的,而不是讓程序去計(jì)算)
這樣會(huì)減少 blending 和 overdraw。
如果使用 image 的話(huà),盡量避免設(shè)置 image 的 alpha 為透明的,如果一些效果需要幾個(gè)圖片融合而成,就讓設(shè)計(jì)用一張圖畫(huà)好,不要讓程序在運(yùn)行的時(shí)候去動(dòng)態(tài)的融合。
好了,介紹完這些渲染優(yōu)化需要注意的點(diǎn),讓我們用 instrument 的 Core Animation 和 GPU Driver 來(lái)看看一些具體的例子。
Core Animation:
Color Blended Layers:看半透明 layer 的遮蓋情況。從綠到紅,越紅遮蓋越大。
這下這兩幅圖是測(cè)量項(xiàng)目詳情頁(yè)的半透明的 layer 的情況。可以看到詳情頁(yè)這里半透明的 layer 還是比較多的,但不是說(shuō)半透明的 layer 很多,范圍很大就要優(yōu)化,要參看 GPU Driver 的測(cè)量情況看,下面會(huì)介紹
Color Hits Green and Misses Red:當(dāng)使用shouldRasterize的時(shí)候,layer drawing 會(huì)被緩存起來(lái),如果 rasterized 的 layer 需要被重新繪制,會(huì)標(biāo)示紅。
下面是項(xiàng)目消息列表的 cell 設(shè)置 shouldRasterize=Y(jié)ES 的情況,每個(gè) cell 的 layer 的 backing image 會(huì)被緩存起來(lái),如果往下滾動(dòng) tableview 的話(huà),由于 cell 會(huì)被復(fù)用,這樣 layer 就會(huì)被重繪,會(huì)被標(biāo)紅。
Color Offscreen-Rendered Yellow:如果要做 offscreen drawing 的話(huà),會(huì)標(biāo)成黃色。
下面是項(xiàng)目的首頁(yè),由于圓形頭像的實(shí)現(xiàn)方式是設(shè)置 roundedCorner 的屬性來(lái)實(shí)現(xiàn),所以會(huì)觸發(fā) offscreen drawing。
Core Animation Template 只是能讓開(kāi)發(fā)者直觀(guān)地看到哪些地方有可能需要優(yōu)化,但是到底要不要優(yōu)化,還是要看 GPU Driver 的表現(xiàn)。
GPU Driver
Renderer Utilization ——如果這個(gè)值大于50%的話(huà),表示 GPU 的性能受到 fill-rate 的限制,可能有太多的 Offscreen rendering,overdraw,blending。
Tiler Utilization ——如果這個(gè)值大于50%,表示可能有太多的 layers。
我們以上面的那個(gè)項(xiàng)目的詳情頁(yè)為例,看看 GPU driver 的測(cè)量:
可以看到這 Renderer Utilization 是20%左右,Tiler Utilization 時(shí)15%,可以不用優(yōu)化,(當(dāng)然這里我們先不考慮 CPU 的使用情況,只是單單針對(duì)上面 Core Animation Template 的一些測(cè)量說(shuō)不用做針對(duì)性的優(yōu)化)
這里想說(shuō)的一點(diǎn)是,以上所說(shuō)的點(diǎn)只是在我們遇到渲染性能問(wèn)題的時(shí)候給我們提供優(yōu)化的方向跟思路。優(yōu)化往往代表著更復(fù)雜,難懂的代碼,在沒(méi)有遇到渲染性能問(wèn)題的時(shí)候不要過(guò)度優(yōu)化。