背景
app如何快速顯示首屏?
滑動列表時(shí)候如何做到流暢?
當(dāng)我們說界面卡了我們在說什么?
......
應(yīng)用運(yùn)行的卡頓率是一個(gè)十分重要的指標(biāo),相比慢、發(fā)熱、占用內(nèi)存高來講,卡頓是用戶第一時(shí)間能感知的東西,三步兩卡的應(yīng)用基本逃不出被卸載的命運(yùn),要想優(yōu)化卡頓就要搞清楚畫面卡住不動的原因,這就需要對整個(gè)渲染過程有一定了解,本文會從圖層說起,來聊聊整個(gè)渲染過程以及優(yōu)化點(diǎn),在寫這篇文章之前筆者努力在想,對于完全沒有做過圖形處理相關(guān)工作的道友來說,理解這個(gè)過程是有一些難度的,那么要怎么寫才可以脈絡(luò)清晰又淺顯易懂呢,想來想去還是從日常開發(fā)中的界面UI開始分析吧,畢竟可直接感知
從一個(gè)簡單的界面開始
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = UIColor.blueColor;
// 蒙板視圖
UIView *maskView = [[UIView alloc] initWithFrame:self.view.bounds];
maskView.backgroundColor = [UIColor redColor];
maskView.alpha = 0.3;
[self.view addSubview:maskView];
// 初始化屏幕大小的矩形路徑
UIBezierPath *bpath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, [[UIScreen mainScreen] bounds].size.width, [[UIScreen mainScreen] bounds].size.height) cornerRadius:8.0];
// 中間添加一個(gè)圓形的路徑
[bpath appendPath:[UIBezierPath bezierPathWithArcCenter:maskView.center radius:100 startAngle:0 endAngle:2*M_PI clockwise:NO]];
//創(chuàng)建一個(gè)layer設(shè)置其CGPath為我們創(chuàng)建的路徑并且賦值給蒙板視圖的layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = bpath.CGPath;
maskView.layer.mask = shapeLayer;
}
效果如下:我們設(shè)置self.view的背景色為藍(lán)色,然后添加了一層中間鏤空的紅色透明度0.3的蒙板,經(jīng)過疊加、裁剪、混合(后面會詳述)就是圖中的效果

數(shù)一數(shù)得到圖中的效果,我們一共用了幾個(gè)元素
- 一個(gè)藍(lán)色背景UIView
- 一個(gè)蒙板視圖UIView
- 一個(gè)用貝塞爾曲線修改過的CAShapeLayer
我們自己創(chuàng)建的元素有三個(gè),背景視圖上面添加蒙板視圖,蒙板視圖的圖層的蒙板層mask設(shè)置為自定義的鏤空CAShapeLayer層,可見開發(fā)中既有視圖又有圖層,會不會覺得有些冗余呢
視圖和圖層為何拆分開來又聚合在一起呢
這是自圖形界面發(fā)明出來就廣泛應(yīng)用的設(shè)計(jì),拆開之后,layer負(fù)責(zé)界面顯示,view負(fù)責(zé)事件分發(fā),聚合一起是因?yàn)椴僮髌饋砀奖悖荒苷f開發(fā)者設(shè)置完事件層次之后,再設(shè)置一遍圖層層次吧
iOS中用UIView和CALayer來描述兩者,所有視圖相關(guān)的類都繼承自UIView,所有圖層相關(guān)的類都繼承自CALayer,view是layer的代理并且蘋果建議我們不要修改這種關(guān)系,為了對開發(fā)者更友好,view中暴露了一些界面設(shè)置相關(guān)的屬性,所有view顯示相關(guān)的設(shè)置最終都會映射到與之綁定的layer上,這樣的api設(shè)計(jì)有意隱藏了CALayer的部分功能,對開發(fā)者來講不用關(guān)心那么多顯示相關(guān)的細(xì)節(jié)
因此當(dāng)我們研究渲染過程的時(shí)候,只需要關(guān)注CALayer即可
渲染數(shù)據(jù)流
縱觀我們的app界面,圖層上面加圖層,圖層又有子圖層,整個(gè)結(jié)構(gòu)是以CALayer或其子類對象為節(jié)點(diǎn)連接而成的樹型結(jié)構(gòu),我們通常稱之為圖層樹,蘋果稱其為模型樹,layer默認(rèn)是個(gè)矩形,我們可以通過path屬性修改其形狀,可以修改為圓形、三角形等任何規(guī)則不規(guī)則的形狀,那么從圖層樹到屏幕上顯示的界面都需要哪些步驟呢,如果你去查資料你可能會翻到圖層樹->呈現(xiàn)樹->渲染樹,那么他們都是什么東西呢,數(shù)據(jù)是如何流轉(zhuǎn)的,CPU、GPU和渲染引擎都扮演了怎樣的角色呢,我們來拆解下渲染數(shù)據(jù)流的處理過程
用CALayer構(gòu)建圖層樹
我們編寫的所有UI代碼,最終都會以一個(gè)個(gè)的CALayer對象的形式被添加到渲染數(shù)據(jù)流中,在此過程中會做如下處理
- 視圖懶加載
這是常規(guī)設(shè)計(jì),通常所有界面元素都會用懶加載的方式去處理,即只有視圖需要顯示的時(shí)候才去加載它,以最大化優(yōu)化內(nèi)存,此過程同時(shí)會進(jìn)行圖片解壓縮(當(dāng)設(shè)置資源路徑到UIImage或者UIImageVIew的時(shí)候)- 布局計(jì)算
加載完視圖之后,會進(jìn)行addSubview、addSublayer操作,這是在設(shè)置圖層之間的關(guān)系,所有圖層會以superlayer、sublayer指針連接起來成為一個(gè)樹型結(jié)構(gòu),一個(gè)節(jié)點(diǎn)只能有一個(gè)superlayer,可以有無限個(gè)sublayer,每個(gè)節(jié)點(diǎn)承載著與父子節(jié)點(diǎn)的位置關(guān)系以及渲染屬性,當(dāng)發(fā)生布局改變的時(shí)候,若是單純的修改某個(gè)節(jié)點(diǎn)的渲染屬性,則開銷較小,若是修改層級,則整個(gè)圖層樹都需要重新計(jì)算修正- Core Graphics繪制
如果實(shí)現(xiàn)了-drawRect:或者-drawLayer:inContext:方法,系統(tǒng)會以當(dāng)前l(fā)ayer為畫布創(chuàng)建一個(gè)寄宿圖來單獨(dú)繪制字符串或者圖片,若是圖片,同樣會進(jìn)行圖片解壓縮操作
以上過程圖層樹已生成,就是以CALayer對象為節(jié)點(diǎn)的樹型結(jié)構(gòu)
Core Animation構(gòu)建呈現(xiàn)樹
圖層樹僅僅是一個(gè)數(shù)據(jù)結(jié)構(gòu),而GPU只負(fù)責(zé)計(jì)算處理圖形圖像數(shù)據(jù),因此在發(fā)送數(shù)據(jù)到渲染服務(wù)之前還需要把圖層樹圖形圖像化,在此過程中會做如下處理
- CALayer生成圖形
遍歷圖層樹,取出每個(gè)layer節(jié)點(diǎn),根據(jù)渲染屬性生成圖形,通常都是矩形(可以是任意形狀就像文章開篇那個(gè)界面一樣)- 圖片生成位圖
無論是直接通過UIImage或者UIImageView加載的圖片還是通過drawRect或者drawLayer:inContext繪制的圖片都會在此過程中生成位圖(第一步僅僅是解碼生成非壓縮二進(jìn)制流)
以上過程呈現(xiàn)樹已生成,圖層樹、呈現(xiàn)樹構(gòu)建過程都是由CPU負(fù)責(zé)計(jì)算
渲染服務(wù)
呈現(xiàn)樹已經(jīng)是圖形圖像組織而成的樹型結(jié)構(gòu),Core Animation通過IPC進(jìn)程通信將其發(fā)送到渲染服務(wù)進(jìn)程
- 生成紋理
如上的圖形圖像都是非格式化的數(shù)據(jù),在計(jì)算機(jī)圖形學(xué)里面通常稱為Buffer,而GPU能處理的是格式化紋理數(shù)據(jù)Texture,在此過程中Core Animation會將呈現(xiàn)樹Buffer數(shù)據(jù)通過渲染引擎轉(zhuǎn)化為紋理數(shù)據(jù)Texture,自此渲染樹已生成,這一步驟仍然由CPU計(jì)算- 頂點(diǎn)數(shù)據(jù):包括頂點(diǎn)坐標(biāo)、紋理坐標(biāo)、頂點(diǎn)法線和頂點(diǎn)顏色等屬性,頂點(diǎn)數(shù)據(jù)構(gòu)成的圖元信息(點(diǎn)、線、三角形等)需要參數(shù)代入繪制指令
- 頂點(diǎn)著色器:將輸入的局部坐標(biāo)變換到世界坐標(biāo)、觀察坐標(biāo)和裁剪坐標(biāo)
- 圖元裝配:將輸入的頂點(diǎn)組裝成指定的圖元,這個(gè)階段會進(jìn)行裁剪和背面剔除相關(guān)優(yōu)化
- 幾何著色器:將輸入的圖元擴(kuò)展成多邊形,將物體坐標(biāo)變換為窗口坐標(biāo)
- 光柵化:將多邊形轉(zhuǎn)化為離散屏幕像素點(diǎn)并得到片元信息
- 片元著色器:通過片元信息為像素點(diǎn)著色,這個(gè)階段會進(jìn)行光照計(jì)算、陰影處理等特效處理
- 測試混合階段:依次進(jìn)行裁切測試、Alpha測試、模板測試和深度測試
- 幀緩存:最終生成的圖像存儲在幀緩存,然后放入渲染緩沖區(qū)
- 顯示到屏幕
以上過程都由渲染引擎處理,除了生成紋理是CPU負(fù)責(zé),其他都全權(quán)由GPU負(fù)責(zé),以上就是界面渲染的全部過程,那我們自定義的畫布呢,比如常見的播放器業(yè)務(wù)、地圖業(yè)務(wù)都是怎么最終顯示到屏幕的呢
自定義畫布
這篇文章有詳述其組織渲染過程,但是渲染到幀緩存之后,顯示到屏幕過程是怎樣的呢
iOS中是不支持直接渲染到屏幕的,我們的自定義畫布,需要配合Core Animation來完成最終的顯示,諸如播放器、地圖等業(yè)務(wù)得到的渲染緩沖renderBuffer,需要通過layer關(guān)聯(lián)到Core Animation層,待到生成渲染樹的時(shí)候替換原來的內(nèi)容,此過程只是一個(gè)指針替換,即將渲染樹對應(yīng)層的指針指向renderBuffer,然后由渲染引擎通過GPU最終將其呈現(xiàn)到屏幕
圖層截圖黑屏問題
播放器、地圖類業(yè)務(wù)圖層截圖的時(shí)候會黑屏,原因是圖層截圖截取的是呈現(xiàn)樹,此時(shí)自定義畫布得到的renderBuffer還沒有替換layer原來的內(nèi)容,解決這個(gè)問題需要在截圖的時(shí)候,將renderBuffer的內(nèi)容在layer的上下文上單獨(dú)繪制
當(dāng)我們說界面卡了我們在說什么
屏幕會以60幀每秒的頻率刷新,就是16.7毫秒一幀,系統(tǒng)渲染進(jìn)程是不會卡的,除非發(fā)生了系統(tǒng)錯誤或者硬件錯誤,通常渲染服務(wù)進(jìn)程會一直以16.7毫秒一幀不停的刷新,這個(gè)過程由VSync信號驅(qū)動,VSync信號由硬件時(shí)鐘生成,每秒鐘發(fā)出60次,渲染服務(wù)進(jìn)程接收到VSync信號后,會通過IPC通知到活躍的App進(jìn)程,進(jìn)程內(nèi)的主線程和添加了CADisplayLink的線程的Runloop在啟動后會注冊對應(yīng)的CFRunLoopSource,通過mach_port接收傳過來的VSync信號,Runloop隨之執(zhí)行一次以驅(qū)動整個(gè)app的運(yùn)行
頁面卡頓,現(xiàn)象是當(dāng)我們滑動列表或者點(diǎn)擊一個(gè)按鈕之后頁面沒有響應(yīng),本質(zhì)原因是主線程卡了,因?yàn)檎麄€(gè)UI界面的構(gòu)建、計(jì)算、合成紋理過程都是在主線程進(jìn)行的,主線程16.7毫秒之內(nèi)沒有執(zhí)行完當(dāng)前任務(wù),該任務(wù)可能是:
- 非UI業(yè)務(wù)邏輯耗時(shí)過多
- 圖層樹構(gòu)建組織過程耗時(shí)過多
- 呈現(xiàn)樹生成過程耗時(shí)過多
總之是渲染樹沒有更新導(dǎo)致看上去還是上一幀的畫面,這就是卡頓
卡頓優(yōu)化
優(yōu)化卡頓無非就是減少上面幾個(gè)步驟的耗時(shí),使Runloop能在16.7毫秒之內(nèi)執(zhí)行完一次
- 非UI業(yè)務(wù)邏輯耗時(shí)
盡可能的將其放在非主線程執(zhí)行,完成之后異步刷新UI
- 圖層樹構(gòu)建組織過程耗時(shí)
1、布局計(jì)算的耗時(shí)與圖層的個(gè)數(shù)、圖層之間的層級關(guān)系和位置關(guān)系呈線性關(guān)系,即圖層越多越耗時(shí),圖層關(guān)系越復(fù)雜越耗時(shí),因此盡量用更少的圖層個(gè)數(shù)和簡單的圖層關(guān)系來布局就是優(yōu)化方向,而且盡量不要動態(tài)修改圖層的層級關(guān)系,否則整個(gè)圖層樹都需要重新計(jì)算修正
2、盡量不要重寫-drawRect:或者-drawLayer:inContext:方法,因?yàn)镃ore Animation不得不生成一張layer等大小的寄宿圖用于繪制,不僅占用額外的內(nèi)存而且繪制過程是CPU計(jì)算的
3、圖片解碼盡量在需要展示之前進(jìn)行,SDWebImage做的就很好,不僅優(yōu)化了圖片文件IO而且圖片解碼也是在非主線程執(zhí)行,完成之后異步刷新UI
- 呈現(xiàn)樹生成過程耗時(shí)
這里要糾正個(gè)問題,離屏渲染是常規(guī)操作,經(jīng)過優(yōu)化的播放器、地圖等業(yè)務(wù)都是用的離屏渲染,發(fā)生在主線程的離屏渲染才有性能問題,可以用CPU渲染也可以用GPU渲染,離屏渲染也是同理
1、CALayer生成圖形,離屏渲染發(fā)生在這個(gè)階段,對于特定圖層的圓角、圖層遮罩、陰影或者是圖層光柵化都會使Core Animation不得不進(jìn)行當(dāng)前圖層的離屏繪制,不過在界面設(shè)計(jì)的時(shí)候,大多數(shù)設(shè)計(jì)師都鐘愛于以上效果,使用起來也沒有太大影響,只是會造成多余的計(jì)算、耗時(shí)和內(nèi)存占用,如果不是列表型的界面,可以盡情的使用,對于列表型界面,我們可以禁用shouldRasterize(就是光柵化),這將會讓圖層離屏渲染一次后把結(jié)果保存起來,后面刷新會直接用緩存的結(jié)果
2、圖片生成位圖,顯然圖片越多、越大計(jì)算量越大,就越耗時(shí),因此如果能用CALayer實(shí)現(xiàn)的效果,盡量不要讓設(shè)計(jì)師出圖,不僅占用存儲空間、占用內(nèi)存而且加載耗時(shí)
總結(jié)
本文從一個(gè)簡單的界面開始詳細(xì)闡述了iOS渲染過程以及卡頓優(yōu)化點(diǎn),有些內(nèi)容官方文檔寫的很清楚,甚至還有demo,大家在學(xué)習(xí)的時(shí)候首先應(yīng)該關(guān)注的就是官方文檔,下面給出兩個(gè)官方參考鏈接: