前端的開發(fā),是離不開頁面的惩阶,那我們就需要了解挎狸,從最基本的視圖創(chuàng)建和繪制過程。
再來看一下断楷,當(dāng)前優(yōu)化的點(diǎn)有哪些锨匆,并且分析一下開源的幾種框架他們是怎么做的優(yōu)化。
UIView和CLAyer的關(guān)系
iOS開發(fā)中冬筒,內(nèi)容的顯示恐锣,是CLayer來做的,CLayer等同于一個(gè)紋理舞痰,紋理是GPU渲染的重要依據(jù)土榴;雖然我們一直操作的都是UIView,他們之間的關(guān)系是相互持有,UIView的一個(gè)屬性就是他內(nèi)部的CLayer响牛,而CLayer也通過delegate來持有UIView玷禽,設(shè)置frame/color這些都是在UIView內(nèi)部直接調(diào)用了layer.frame...,其結(jié)論就是呀打,UIView生成視圖樹论衍,我們操縱的是視圖樹,但是圖層樹也會(huì)同步添加或者刪除聚磺。
之前看過一篇文章坯台,寫為什么要設(shè)計(jì)成這樣;如果把設(shè)計(jì)的權(quán)交給問這個(gè)問題的人瘫寝,那么整個(gè)框架肯定變成了UILayer這種形式蜒蕾,既能接受事件稠炬、又能對外展示。但是隨著時(shí)間的推移咪啡,功能的添加和風(fēng)格的切換首启,我們會(huì)不斷的在這個(gè)UILayer類中添加和修改,很快整個(gè)類就無法維護(hù)和繼續(xù)了撤摸;所以在認(rèn)定展示是一個(gè)基本屬性后毅桃,作為根本,不需要太多的改變准夷,各自負(fù)責(zé)的功能也要遵守單一原則钥飞。文章地址
視圖的創(chuàng)建、修改到顯示的過程
首先衫嵌,由 app 處理事件(Handle Events)读宙,如:用戶的點(diǎn)擊操作,在此過程中 app 可能需要更新 視圖樹楔绞,相應(yīng)地结闸,圖層樹 也會(huì)被更新。
其次酒朵,app 通過 CPU 完成對顯示內(nèi)容的計(jì)算桦锄,如:視圖的創(chuàng)建、布局計(jì)算蔫耽、圖片解碼结耀、文本繪制等。在完成對顯示內(nèi)容的計(jì)算之后针肥,app 對圖層進(jìn)行打包饼记,并在下一次 RunLoop 時(shí)將其發(fā)送至 Render Server,即完成了一次 Commit Transaction 操作慰枕。
Render Server 主要執(zhí)行 Open GL具则、Core Graphics 相關(guān)程序(現(xiàn)在的渲染引擎改成Metal),并調(diào)用 GPU
GPU 則在物理層上完成了對圖像的渲染具帮。
最終博肋,GPU 通過 Frame Buffer、視頻控制器等相關(guān)部件蜂厅,將圖像顯示在屏幕上匪凡。
Core Animation不僅能做動(dòng)畫,還可以進(jìn)行渲染操作掘猿。
Core Graphics 2D的繪圖引擎病游,主要用于運(yùn)行時(shí)繪制圖像
commit經(jīng)過的是以下四個(gè)步驟
Lyout/Display/Prepare/Commit
Lyout 布局:設(shè)置 layer 的屬性,如 frame,background color 等衬衬,設(shè)置layer的層級(jí)信息买猖。layoutSubviews 在這一步調(diào)用
Display : 生成位圖,這一步會(huì)調(diào)用drawrect 方法滋尉;這一步有兩種情況需要區(qū)分玉控,我們要顯示的layer,有兩種方式可以設(shè)置顯示的位圖狮惜;
一高诺、系統(tǒng)默認(rèn)的,會(huì)在layer中創(chuàng)建一個(gè)backing store來存放生成的位圖碾篡。當(dāng)然虱而,系統(tǒng)的調(diào)用過程是給我們開了一個(gè)口子的,那就是drawRect,我們可以在這里邊使用UIKit或者Graphics進(jìn)行繪制一個(gè)要顯示的位圖耽梅。
二薛窥、我們可以攔截胖烛,自己繪制出位圖眼姐,直接設(shè)置到layer.cntents中。這一步很重要佩番,因?yàn)榇蟛糠诌M(jìn)行異步渲染的框架众旗,都是在這一步做的攔截,進(jìn)行自己的繪制趟畏。
Prepare :準(zhǔn)備提交的參數(shù)贡歧,包括動(dòng)畫的參數(shù);整個(gè)動(dòng)畫我們不需要做額外的操作赋秀,只需要起始和終止條件利朵,然后交給CA(Core Animation),他會(huì)在render server進(jìn)行計(jì)算猎莲,計(jì)算動(dòng)畫的中間狀態(tài)绍弟,然后重復(fù)完整個(gè)動(dòng)畫。
這一步還會(huì)進(jìn)行圖片的解碼操作著洼。
Commit:CA進(jìn)行數(shù)據(jù)的提交樟遣,遞歸提交。
在GPU參與以前身笤,都是CPU在進(jìn)行一系列的工作豹悬,所以優(yōu)化分為CPU約束型(CPU bound) 和 GPU約束型(GPU bound)。
針對上邊寫的流程液荸,從創(chuàng)建到渲染完成瞻佛,整個(gè)步驟包括commit的四步,render server中的繪制娇钱,還有就是到了GPU中的渲染
Lyout:
減少視圖的創(chuàng)建伤柄;使用輕量級(jí)的視圖CALyer代替UIView涡尘;盡量重用cell;
Display:
可以采用異步繪制的方式响迂,充分利用CPU的多核考抄。
GPU:
- 減少Blending,并且使用沒有Alpha通道的圖片蔗彤;
- 減少圖層的層級(jí)川梅;目前很多庫,都采用了繪制成一個(gè)位圖來GPU進(jìn)行渲染然遏。
- 盡量避免離屏渲染贫途。
以上只是較大的要注意的地方,很多小的注意點(diǎn)在最后列出待侵。
關(guān)于離屏渲染的問題
我們認(rèn)為的關(guān)于CPU的離屏渲染丢早,可能會(huì)造成一些額外的開銷,但不是真正的離屏渲染秧倾,通過Xcode的Color Offscreen_Rendered Yellow怨酝,看下是不是變黃色。
離屏渲染發(fā)生在GPU上那先,他的性能消耗是在上下文的切換农猬;通常下邊的操作會(huì)引發(fā)離屏渲染:
1.cornerRadius+clipsToBounds 切圓角,如果兩個(gè)函數(shù)一起出現(xiàn)售淡,就會(huì)引發(fā)離屏渲染斤葱。
只有cornerRadius,表示只需要一個(gè)圓角揖闸,不需要將外圍切掉揍堕,不會(huì)引發(fā)離屏渲染。
優(yōu)化:可以使用Graphics提前繪制圓角的Texture汤纸,然后交個(gè)GPU衩茸。
- shadow 引發(fā)離屏渲染,通過通過shadowPath屬性可以避免蹲嚣。
3.group opacity - mask
- UIBlurEffect
優(yōu)化:不用系統(tǒng)的模糊效果递瑰,另外實(shí)現(xiàn)CIGaussianBlur
....
具體可以參考即刻
其他框架做的渲染優(yōu)化
美團(tuán)的Graver,采用了異步繪制 + 自己實(shí)現(xiàn)繪制的方式;
自己實(shí)現(xiàn)繪制就是將多個(gè)層級(jí)的layer隙畜,繪制到一個(gè)layer上抖部,將多層級(jí),降低為只有一個(gè)層級(jí)议惰,GPU渲染毫無壓力慎颗,不用進(jìn)行分層繪制了、也不會(huì)引發(fā)離屏渲染
具體方式是通過CoreText進(jìn)行l(wèi)ayer的繪制。
異步繪制:實(shí)現(xiàn)layer的displayer的代理方法俯萎,然后在這里邊進(jìn)行界面的異步繪制傲宜,繪制完回到主線程進(jìn)行顯示。
這地方稍微拓展一下layer的渲染過程夫啊,上邊說過函卒,分為兩種,系統(tǒng)自己的渲染和我們可以介入的方式撇眯。
大部分的異步繪制报嵌,都是自己實(shí)現(xiàn)了displayerLayer這個(gè)代理方法,在這里進(jìn)行異步繪制熊榛,美團(tuán)的Graver就是攔截了這個(gè)函數(shù)使用Core Graphics 和 Core Text進(jìn)行繪制的锚国。
系統(tǒng)自己的渲染流程
會(huì)判斷l(xiāng)ayer有沒有delegate - > 有代理(也就是有UIView)并且實(shí)現(xiàn)了drawLayer:inContext 方法,就會(huì)進(jìn)行繪制玄坦,這個(gè)時(shí)候血筑,如果我們自己實(shí)現(xiàn)了drawReact ,會(huì)在drawLayer:inContext 方法中被直接調(diào)用。
VVeboTableViewDemo
這個(gè)思路是計(jì)算滑動(dòng)手指松開時(shí)的坐標(biāo)煎楣,優(yōu)先渲染周圍的幾個(gè)豺总。參考自保持頁面流暢的技巧
YYKit中文本的異步繪制也是在layer 的display函數(shù)中,直接進(jìn)行了異步繪制转质。他是自己實(shí)現(xiàn)了一個(gè)具有異步繪制功能的layer园欣,然后我們使用的時(shí)候帖世,可以將UILabel中內(nèi)部的layer換成這個(gè)休蟹,然后進(jìn)行Graphics的異步繪制,他做了一些優(yōu)化日矫,作者在他的博客中也提到了赂弓,控制并發(fā)線程的數(shù)量、及時(shí)取消不需要的繪制任務(wù)哪轿。
控制線程數(shù)量
控制線程數(shù)量這個(gè)通過他自建的queue 池盈魁,實(shí)現(xiàn)邏輯就是開辟線程的時(shí)候,他是通過串行隊(duì)列異步開啟窃诉,就等于有多少個(gè)串行隊(duì)列杨耙,就能開辟多少個(gè)線程。
及時(shí)取消任務(wù)
及時(shí)取消任務(wù)飘痛,他的解釋就是滑動(dòng)過快珊膜,很多任務(wù)其實(shí)不需要繪制了,就把不需要繪制的任務(wù)取消掉宣脉,每個(gè)layer都有一個(gè)標(biāo)記车柠,在異步繪制的時(shí)候,判斷當(dāng)前自己身上的標(biāo)記和最新的標(biāo)記比對,如果不一樣竹祷,直接返回谈跛。(上下滑動(dòng)過程中、設(shè)置字體等會(huì)觸發(fā)重繪)
監(jiān)控runloop周期進(jìn)行提交渲染
還有一個(gè)就是和Texture一樣的就是塑陵,監(jiān)測了runloop的周期感憾,在runloop的waiting和exit階段才進(jìn)行繪制提交(我們自己繪制的時(shí)候,設(shè)置contetnts令花,就會(huì)觸發(fā)CA的提交)吹菱,注意,這地方設(shè)置runloop的observer的時(shí)候彭则,優(yōu)先級(jí)是比系統(tǒng)的低鳍刷,系統(tǒng)的commit先提交完,再處理自定義的俯抖。
工欲善其事必先利其器
檢測工具的使用输瓜,我們在開發(fā)中,還有在線上芬萍,都需要對界面進(jìn)行調(diào)試和監(jiān)控尤揣。具體的監(jiān)控思想有以下幾種。
開發(fā):
Instruments肯定是比較理想的檢查工具柬祠,Core Animation..
關(guān)于離屏渲染北戏、視圖的混合都可以直接觀察(目前改變顏色這些直接在Xcode的debug工具里邊)
線上的時(shí)候:
- 我們卡頓的檢測可以使用Runloop進(jìn)行狀態(tài)時(shí)間的檢測,添加observer進(jìn)行監(jiān)控漫蛔。
- FPS的統(tǒng)計(jì)嗜愈,通過加入幀時(shí)間定時(shí)器,判斷FPS
- 配合內(nèi)存的監(jiān)控莽龟,如果內(nèi)存過高蠕嫁,也將堆棧信息保存下來。
以上幾種最好一起使用毯盈,因?yàn)镕PS獲取堆棧的時(shí)候剃毒,很可能抓取的是卡頓之后函數(shù)的堆棧信息。
微信的Matrix的思路
為了解決抓取調(diào)用棧不準(zhǔn)的問題搂赋,微信直接使用了另一種思路赘阀,每50毫秒抓取一次,保存最近的20個(gè)函數(shù)調(diào)用棧脑奠,發(fā)生卡頓后基公,判斷這20個(gè)調(diào)用棧中出現(xiàn)次數(shù)最多的那個(gè)調(diào)用棧,最耗時(shí)的方法就在里邊捺信。
這樣做會(huì)造成百分之3的CPU上升酌媒,這個(gè)微信團(tuán)隊(duì)自己測試過欠痴。
退火算法
先判斷每次抓取堆棧的hash是不是一樣,重復(fù)的不上傳秒咨,只上傳一次喇辽。