Head
在性能優(yōu)化中饿这,有一個重要的知識點(diǎn)就是卡頓優(yōu)化
,我們以FPS(每秒傳輸幀數(shù)(Frames Per Second))
來衡量它的流暢度撞秋,蘋果的iPhone推薦的刷新率是60Hz长捧,也就是說GPU每秒鐘刷新屏幕60次,這每刷新一次就是一幀frame吻贿,每一幀大概在1/60 = 16.67ms
畫面最佳串结,靜止不變的頁面FPS值是0,這個值是沒有參考意義的舅列,只有當(dāng)頁面在執(zhí)行動畫或者滑動的時候肌割,F(xiàn)PS值才具有參考價值,F(xiàn)PS值的大小體現(xiàn)了頁面的流暢程度高低帐要,當(dāng)?shù)陀?5的時候卡頓會比較明顯
屏幕呈像原理
我們所看到的動態(tài)的屏幕的成像其實(shí)和視頻一樣也是一幀一幀組成的把敞。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進(jìn)行同步,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號宠叼。當(dāng)電子槍換行進(jìn)行掃描時先巴,顯示器會發(fā)出一個水平同步信號(horizonal synchronization)其爵,簡稱 HSync
冒冬;而當(dāng)一幀畫面繪制完成后伸蚯,電子槍回復(fù)到原位,準(zhǔn)備畫下一幀前简烤,顯示器會發(fā)出一個垂直同步信號(vertical synchronization)剂邮,簡稱 VSync
。顯示器通常以固定頻率進(jìn)行刷新横侦,這個刷新率就是 VSync
信號產(chǎn)生的頻率挥萌。
卡頓的產(chǎn)生
接下來介紹完成顯示信息的過程是:CPU 計算數(shù)據(jù)
-> GPU 進(jìn)行渲染
-> 渲染結(jié)果存入幀緩沖區(qū)
-> 視頻控制器會按照 VSync 信號逐幀讀取幀緩沖區(qū)的數(shù)據(jù)
-> 成像
,假如屏幕已經(jīng)發(fā)出了 VSync 但 GPU 還沒有渲染完成枉侧,則只能將上一次的數(shù)據(jù)顯示出來引瀑,以致于當(dāng)前計算的幀數(shù)據(jù)丟失,這樣就產(chǎn)生了卡頓榨馁,當(dāng)前的幀數(shù)據(jù)計算好后只能等待下一個周期去渲染憨栽。
卡頓的優(yōu)化
那么,解決卡頓的方案就很是要在下一次VSync到來之前翼虫,盡可能減少這一幀 CPU 和 GPU 資源的消耗屑柔,要減少的話我們就得先了解這兩者在渲染中的具體分工是什么,和iOS中視圖的產(chǎn)生過程
UIView 和 CALayer
我們都知道珍剑,視圖的職責(zé)是 創(chuàng)建并管理
圖層掸宛,以確保當(dāng)子視圖在層級關(guān)系中 添加或被移除
時,其關(guān)聯(lián)的圖層在圖層樹中也有相同的操作招拙,即保證視圖樹和圖層樹在結(jié)構(gòu)上的一致性唧瘾,那么為什么 iOS 要基于 UIView
和 CALayer
提供兩個平行的層級關(guān)系呢?其原因在于要做 職責(zé)分離
别凤,這樣也能避免很多重復(fù)代碼劈愚。在 iOS 和 Mac OS X 兩個平臺上,事件和用戶交互有很多地方的不同闻妓,基于多點(diǎn)觸控的用戶界面和基于鼠標(biāo)鍵盤的交互有著本質(zhì)的區(qū)別菌羽,這就是為什么 iOS 有 UIKit
和 UIView
,對應(yīng) Mac OS X 有 AppKit
和 NSView
的原因由缆。它們在功能上很相似注祖,但是在實(shí)現(xiàn)上有著顯著的區(qū)別。
CALayer
那么為什么 CALayer 可以呈現(xiàn)可視化內(nèi)容呢均唉?因?yàn)?CALayer 基本等同于一個 紋理是晨。紋理是 GPU 進(jìn)行圖像渲染的重要依據(jù),紋理本質(zhì)上就是一張圖片舔箭,因此 CALayer 也包含一個 contents
屬性指向一塊緩存區(qū)罩缴,稱為 backing store
蚊逢,可以存放位圖(Bitmap)
。iOS 中將該緩存區(qū)保存的圖片稱為 寄宿圖
在實(shí)際開發(fā)中箫章,繪制界面有兩種方式:一種是
手動繪制
烙荷;另一種是 使用圖片
。對此檬寂,iOS 中也有兩種相應(yīng)的實(shí)現(xiàn)方式:
- 使用圖片:
contents image
- 手動繪制:
custom drawing
Contents Image
Contents Image 是指通過 CALayer 的 contents
屬性來配置圖片终抽。然而,contents
屬性的類型為 id桶至。在這種情況下昼伴,可以給 contents 屬性賦予任何值,app 仍可以編譯通過镣屹。但是在實(shí)踐中圃郊,如果 content 的值不是 CGImage ,得到的圖層將是空白的
// Contents Image
UIImage *image = [UIImage imageNamed:@"cat.JPG"];
UIView *v = [UIView new];
v.layer.contents = (__bridge id _Nullable)(image.CGImage);
v.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:v];
我們可以看到女蜈,這樣就可以使用圖片繪制到view上面去
Custom Drawing
Custom Drawing 是指使用 Core Graphics 直接繪制寄宿圖持舆。實(shí)際開發(fā)中,一般通過繼承 UIView 并實(shí)現(xiàn) -drawRect: 方法來自定義繪制鞭光。
- UIView 有一個關(guān)聯(lián)圖層吏廉,即
CALayer
。 - CALayer 有一個可選的 delegate 屬性惰许,實(shí)現(xiàn)了
CALayerDelegate
協(xié)議席覆。UIView 作為 CALayer 的代理實(shí)現(xiàn)了CALayerDelegae
協(xié)議。 - 當(dāng)需要重繪時汹买,即調(diào)用
-drawRect:
佩伤,CALayer 請求其代理給予一個寄宿圖來顯示。 - CALayer 首先會嘗試調(diào)用
-displayLayer:
方法晦毙,此時代理可以直接設(shè)置 contents 屬性生巡。
- (void)displayLayer:(CALayer *)layer;
- 如果代理沒有實(shí)現(xiàn) -displayLayer: 方法,CALayer 則會嘗試調(diào)用
-drawLayer:inContext:
方法见妒。在調(diào)用該方法前孤荣,CALayer 會創(chuàng)建一個空的寄宿圖(尺寸由 bounds 和 contentScale 決定)和一個 Core Graphics 的繪制上下文,為繪制寄宿圖做準(zhǔn)備须揣,作為 ctx 參數(shù)傳入盐股。
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
-
最后,由 Core Graphics 繪制生成的寄宿圖會存入
backing store
耻卡。
若UIView的子類重寫了drawRect
疯汁,則UIView執(zhí)行完drawRect
后,系統(tǒng)會為器layer的content
開辟一塊緩存卵酪,用來存放drawRect繪制的內(nèi)容幌蚊。
即使重寫的drawRect啥也沒做谤碳,也會開辟緩存,消耗內(nèi)存溢豆,所以盡量不要隨便重寫drawRect卻啥也不做
蜒简。 其實(shí),當(dāng)在操作 UI 時沫换,比如改變了 Frame臭蚁、更新了 UIView/CALayer 的層次時最铁,或者手動調(diào)用了 UIView/CALayer 的
setNeedsLayout/setNeedsDisplay
方法后讯赏,在此過程中 app 可能需要更新視圖樹
,相應(yīng)地冷尉,圖層樹
也會被更新其次漱挎,CPU計算要顯示的內(nèi)容,包括
布局計算(Layout)
雀哨、視圖繪制(Display)
磕谅、圖片解碼(Prepare)
當(dāng)runloop
在BeforeWaiting(即將進(jìn)入休眠)
和Exit (即將退出Loop)
時,會通知注冊的監(jiān)聽雾棺,然后對圖層打包(Commit
)膊夹,打包完后,將打包的數(shù)據(jù)(backing store)
發(fā)送給一個獨(dú)立負(fù)責(zé)渲染的進(jìn)程Render Server
數(shù)據(jù)到達(dá)
Render Server
后會被反序列化捌浩,得到圖層樹放刨,按照圖層樹中圖層順序、RBGA值尸饺、圖層frame過濾圖中被遮擋的部分进统,過濾后將圖層樹轉(zhuǎn)成渲染樹,渲染樹的信息會轉(zhuǎn)給OpenGL ES/Metal
至此浪听,前面CPU 所處理的這些事情統(tǒng)稱為 Commit Transaction
Render Server
Render Server 會調(diào)用 GPU螟碎,GPU 開始進(jìn)行頂點(diǎn)著色器
、形狀裝配
迹栓、幾何著色器
掉分、光柵化
、片段著色器
克伊、測試與混合
六個階段酥郭。完成這六個階段的工作后,再將 CPU 和 GPU 計算后的數(shù)據(jù)顯示在屏幕的每個像素點(diǎn)上
- 頂點(diǎn)著色器(Vertex Shader)
- 形狀裝配(Shape Assembly)答毫,又稱 圖元裝配
- 幾何著色器(Geometry Shader)
- 光柵化(Rasterization)
- 片段著色器(Fragment Shader)
- 測試與混合(Tests and Blending)
第一階段褥民,頂點(diǎn)著色器。該階段的輸入是 頂點(diǎn)數(shù)據(jù)(Vertex Data) 數(shù)據(jù)洗搂,比如以數(shù)組的形式傳遞 3 個 3D 坐標(biāo)用來表示一個三角形消返。頂點(diǎn)數(shù)據(jù)是一系列頂點(diǎn)的集合载弄。頂點(diǎn)著色器主要的目的是把 3D 坐標(biāo)轉(zhuǎn)為另一種 3D 坐標(biāo)撵颊,同時頂點(diǎn)著色器可以對頂點(diǎn)屬性進(jìn)行一些基本處理宇攻。
第二階段逞刷,形狀(圖元)裝配亿胸。該階段將頂點(diǎn)著色器輸出的所有頂點(diǎn)作為輸入,并將所有的點(diǎn)裝配成指定圖元的形狀洋丐。圖中則是一個三角形。圖元(Primitive) 用于表示如何渲染頂點(diǎn)數(shù)據(jù),如:點(diǎn)锦爵、線埠啃、三角形台盯。
第三階段蚣常,幾何著色器贞绳。該階段把圖元形式的一系列頂點(diǎn)的集合作為輸入遇八,它可以通過產(chǎn)生新頂點(diǎn)構(gòu)造出新的(或是其它的)圖元來生成其他形狀本昏。例子中,它生成了另一個三角形赖捌。
第四階段祝沸,光柵化。該階段會把圖元映射為最終屏幕上相應(yīng)的像素越庇,生成片段罩锐。片段(Fragment) 是渲染一個像素所需要的所有數(shù)據(jù)。
第五階段卤唉,片段著色器涩惑。該階段首先會對輸入的片段進(jìn)行 裁切(Clipping)。裁切會丟棄超出視圖以外的所有像素搬味,用來提升執(zhí)行效率境氢。
第六階段,測試與混合碰纬。該階段會檢測片段的對應(yīng)的深度值
(z 坐標(biāo))萍聊,判斷這個像素位于其它物體的前面還是后面,決定是否應(yīng)該丟棄悦析。此外寿桨,該階段還會檢查 alpha
值( alpha 值定義了一個物體的透明度),從而對物體進(jìn)行混合。因此亭螟,即使在片段著色器中計算出來了一個像素輸出的顏色挡鞍,在渲染多個三角形的時候最后的像素顏色也可能完全不同。
公式為:
R = S + D * (1 - Sa)
假設(shè)有兩個像素 S(source) 和 D(destination)预烙,S 在 z 軸方向相對靠前(在上面)墨微,D 在 z 軸方向相對靠后(在下面),那么最終的顏色值就是 S(上面像素) 的顏色 + D(下面像素) 的顏色 * (1 - S(上面像素) 顏色的透明度)
所以扁掸,才需要我們在做頁面的時候翘县,盡量控制少的圖層數(shù)
、還有盡量不要使用alpha
- 最終谴分,GPU通過
Frame Buffer(幀緩沖區(qū)锈麸、 雙緩沖機(jī)制)
、視頻控制器
等相關(guān)部件牺蹄,將圖像顯示在屏幕上忘伞。
至此,原生的渲染流程到此結(jié)束沙兰。
原生渲染卡頓優(yōu)化方案
所以解決卡頓現(xiàn)象的主要思路就是:盡可能減少 CPU
和 GPU
資源的消耗氓奈。
CPU
- 盡量用輕量級的對象 如:不用處理事件的 UI 控件可以考慮使用 CALayer;
- 不要頻繁地調(diào)用 UIView 的相關(guān)屬性 如:frame僧凰、bounds探颈、transform 等;
- 盡量提前計算好布局训措,在有需要的時候一次性調(diào)整對應(yīng)屬性,不要多次修改光羞;
- Autolayout 會比直接設(shè)置 frame 消耗更多的 CPU 資源绩鸣;
- 圖片的 size 和 UIImageView 的 size 保持一致;
- 控制線程的最大并發(fā)數(shù)量纱兑;
- 耗時操作放入子線程呀闻;如文本的尺寸計算、繪制潜慎,圖片的解碼捡多、繪制等;
GPU
- 盡量避免短時間內(nèi)大量圖片顯示铐炫;
- GPU 能處理的最大紋理尺寸是 4096 * 4096垒手,超過這個尺寸就會占用 CPU 資源,所以紋理不能超過這個尺寸倒信;
- 盡量減少透視圖的數(shù)量和層次科贬;
- 減少透明的視圖(alpha < 1),不透明的就設(shè)置 opaque 為 YES鳖悠;
- 盡量避免離屏渲染榜掌;
大前端渲染
大前端的開發(fā)框架主要分為兩類:第一類是基于 WebView
的优妙,第二類是類似 React Native
的。
對于第一類 WebView 的大前端渲染憎账,主要工作在 WebKit 中完成套硼。WebKit 的渲染層來自以前 macOS 的 Layer Rendering
架構(gòu),而 iOS 也是基于這一套架構(gòu)胞皱。所以熟菲,從本質(zhì)上來看,WebKit 和 iOS 原生渲染差別不大朴恳。
第二類的類 React Native
更簡單抄罕,渲染直接走的是 iOS 原生的渲染。那么于颖,我們?yōu)槭裁磿杏X WebView
和類 React Native
比原生渲染得慢呢呆贿?
從第一次內(nèi)容加載來看,即使是本地加載森渐,大前端也要比原生多出腳本代碼解析
的工作做入。
WebView 需要額外解析 HTML + CSS + JavaScript
代碼,而類 React Native 方案則需要解析 JSON + JavaScript
同衣。HTML + CSS
的復(fù)雜度要高于 JSON
竟块,所以解析起來會比 JSON 慢。也就是說耐齐,首次內(nèi)容加載時浪秘,WebView
會比類 React Native
慢。
從語言本身的解釋執(zhí)行性能來看埠况,大前端加載后的界面更新會通過 JavaScript
解釋執(zhí)行耸携,而 JavaScript 解釋執(zhí)行性能要比原生差,特別是解釋執(zhí)行復(fù)雜邏輯或大量計算時辕翰。所以夺衍,大前端的運(yùn)算速度,要比原生慢不少喜命。
說完了大前端的渲染沟沙,你會發(fā)現(xiàn),相對于原生渲染壁榕,無論是 WebView 還是類 React Native 都會因?yàn)槟_本語言本身的性能問題而在存在性能差距矛紫。那么,對于 Flutter 這種沒有使用腳本語言护桦,并且渲染引擎也是全新的框架含衔,其渲染方式有什么不同,性能又怎樣呢?
Flutter 渲染
Flutter 界面是由 Widget
組成的贪染,所有 Widget
組成 Widget Tree
缓呛,界面更新時會更新 Widget Tree
,然后再更新 Element Tree杭隙,最后更新 RenderObject Tree哟绊。
接下來的渲染流程,F(xiàn)lutter 渲染在 Framework 層會有 Build
痰憎、Wiget Tree
票髓、Element Tree
、RenderObject Tree
铣耘、Layout
洽沟、Paint
、Composited Layer
等幾個階段蜗细。將 Layer 進(jìn)行組合裆操,生成紋理,使用 OpenGL 的接口向 GPU 提交渲染內(nèi)容進(jìn)行光柵化與合成炉媒,是在 Flutter 的 C++ 層踪区,使用的是 Skia 庫。包括提交到 GPU 進(jìn)程后吊骤,合成計算缎岗,顯示屏幕的過程和 iOS 原生基本是類似的,因此性能也差不多白粉。