在性能優化中,有一個重要的知識點就是卡頓優化
,咱們以FPS(每秒傳輸幀數(Frames Per Second))
來衡量它的流暢度,蘋果的iPhone推薦的刷新率是60Hz,也就是說GPU每秒鐘刷新屏幕60次,這每刷新一次就是一幀frame,每一幀大概在1/60 = 16.67ms
畫面最佳,靜止不變的頁面FPS值是0,這個值是沒有參考意義的,只有當頁面在執行動畫或者滑動的時候,FPS值才具備參考價值,FPS值的大小體現了頁面的流暢程度高低,當低於45的時候卡頓會比較明顯前端
咱們所看到的動態的屏幕的成像其實和視頻同樣也是一幀一幀組成的。爲了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或者其餘硬件)會用硬件時鐘產生一系列的定時信號。當電子槍換行進行掃描時,顯示器會發出一個水平同步信號(horizonal synchronization),簡稱 HSync
;而當一幀畫面繪製完成後,電子槍回覆到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號(vertical synchronization),簡稱 VSync
。顯示器一般以固定頻率進行刷新,這個刷新率就是 VSync
信號產生的頻率。 數組
接下來介紹完成顯示信息的過程是:CPU 計算數據
-> GPU 進行渲染
-> 渲染結果存入幀緩衝區
-> 視頻控制器會按照 VSync 信號逐幀讀取幀緩衝區的數據
-> 成像
,假如屏幕已經發出了 VSync 但 GPU 尚未渲染完成,則只能將上一次的數據顯示出來,以至於當前計算的幀數據丟失,這樣就產生了卡頓,當前的幀數據計算好後只能等待下一個週期去渲染。 緩存
那麼,解決卡頓的方案就非常要在下一次VSync到來以前,儘量減小這一幀 CPU 和 GPU 資源的消耗,要減小的話咱們就得先了解這二者在渲染中的具體分工是什麼,和iOS中視圖的產生過程性能優化
咱們都知道,視圖的職責是 建立並管理
圖層,以確保當子視圖在層級關係中 添加或被移除
時,其關聯的圖層在圖層樹中也有相同的操做,即保證視圖樹和圖層樹在結構上的一致性,那麼爲何 iOS 要基於 UIView
和 CALayer
提供兩個平行的層級關係呢?其緣由在於要作 職責分離
,這樣也能避免不少重複代碼。在 iOS 和 Mac OS X 兩個平臺上,事件和用戶交互有不少地方的不一樣,基於多點觸控的用戶界面和基於鼠標鍵盤的交互有着本質的區別,這就是爲何 iOS 有 UIKit
和 UIView
,對應 Mac OS X 有 AppKit
和 NSView
的緣由。它們在功能上很類似,可是在實現上有着顯著的區別。bash
那麼爲何 CALayer 能夠呈現可視化內容呢?由於 CALayer 基本等同於一個 紋理。紋理是 GPU 進行圖像渲染的重要依據,紋理本質上就是一張圖片,所以 CALayer 也包含一個 contents
屬性指向一塊緩存區,稱爲 backing store
,能夠存放位圖(Bitmap)
。iOS 中將該緩存區保存的圖片稱爲 寄宿圖
架構
手動繪製
;另外一種是
使用圖片
。 對此,iOS 中也有兩種相應的實現方式:
contents image
custom drawing
Contents Image 是指經過 CALayer 的 contents
屬性來配置圖片。然而,contents
屬性的類型爲 id。在這種狀況下,能夠給 contents 屬性賦予任何值,app 仍能夠編譯經過。可是在實踐中,若是 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 是指使用 Core Graphics 直接繪製寄宿圖。實際開發中,通常經過繼承 UIView 並實現 -drawRect: 方法來自定義繪製。app
CALayer
。CALayerDelegate
協議。UIView 做爲 CALayer 的代理實現了CALayerDelegae
協議。-drawRect:
,CALayer 請求其代理給予一個寄宿圖來顯示。-displayLayer:
方法,此時代理能夠直接設置 contents 屬性。- (void)displayLayer:(CALayer *)layer;
複製代碼
-drawLayer:inContext:
方法。在調用該方法前,CALayer 會建立一個空的寄宿圖(尺寸由 bounds 和 contentScale 決定)和一個 Core Graphics 的繪製上下文,爲繪製寄宿圖作準備,做爲 ctx 參數傳入。- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
複製代碼
最後,由 Core Graphics 繪製生成的寄宿圖會存入 backing store
。 框架
drawRect
,則UIView執行完drawRect
後,系統會爲器layer的content
開闢一塊緩存,用來存放drawRect繪製的內容。 即便重寫的drawRect啥也沒作,也會開闢緩存,消耗內存,因此儘可能不要隨便重寫drawRect卻啥也不作
。 其實,當在操做 UI 時,好比改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay
方法後,在此過程當中 app 可能須要更新 視圖樹
,相應地,圖層樹
也會被更新oop
其次,CPU計算要顯示的內容,包括佈局計算(Layout)
、視圖繪製(Display)
、圖片解碼(Prepare)
當runloop
在 BeforeWaiting(即將進入休眠)
和 Exit (即將退出Loop)
時,會通知註冊的監聽,而後對圖層打包(Commit
),打包完後,將打包的數據(backing store)
發送給一個獨立負責渲染的進程 Render Server
數據到達Render Server
後會被反序列化,獲得圖層樹,按照圖層樹中圖層順序、RBGA值、圖層frame過濾圖中被遮擋的部分,過濾後將圖層樹轉成渲染樹,渲染樹的信息會轉給 OpenGL ES/Metal
至此,前面CPU 所處理的這些事情統稱爲 Commit Transaction
Render Server 會調用 GPU,GPU 開始進行頂點着色器
、形狀裝配
、幾何着色器
、光柵化
、片斷着色器
、測試與混合
六個階段。完成這六個階段的工做後,再將 CPU 和 GPU 計算後的數據顯示在屏幕的每一個像素點上
第二階段,形狀(圖元)裝配。該階段將頂點着色器輸出的全部頂點做爲輸入,並將全部的點裝配成指定圖元的形狀。圖中則是一個三角形。圖元(Primitive) 用於表示如何渲染頂點數據,如:點、線、三角形。
第三階段,幾何着色器。該階段把圖元形式的一系列頂點的集合做爲輸入,它能夠經過產生新頂點構造出新的(或是其它的)圖元來生成其餘形狀。例子中,它生成了另外一個三角形。
第四階段,光柵化。該階段會把圖元映射爲最終屏幕上相應的像素,生成片斷。片斷(Fragment) 是渲染一個像素所須要的全部數據。
第五階段,片斷着色器。該階段首先會對輸入的片斷進行 裁切(Clipping)。裁切會丟棄超出視圖之外的全部像素,用來提高執行效率。
第六階段,測試與混合。該階段會檢測片斷的對應的深度值
(z 座標),判斷這個像素位於其它物體的前面仍是後面,決定是否應該丟棄。此外,該階段還會檢查 alpha
值( alpha 值定義了一個物體的透明度),從而對物體進行混合。所以,即便在片斷着色器中計算出來了一個像素輸出的顏色,在渲染多個三角形的時候最後的像素顏色也可能徹底不一樣。 公式爲:
R = S + D * (1 - Sa)
複製代碼
假設有兩個像素 S(source) 和 D(destination),S 在 z 軸方向相對靠前(在上面),D 在 z 軸方向相對靠後(在下面),那麼最終的顏色值就是 S(上面像素) 的顏色 + D(下面像素) 的顏色 * (1 - S(上面像素) 顏色的透明度)
因此,才須要咱們在作頁面的時候,儘可能控制少的圖層數
、還有儘可能不要使用alpha
Frame Buffer(幀緩衝區、 雙緩衝機制)
、視頻控制器
等相關部件,將圖像顯示在屏幕上。至此,原生的渲染流程到此結束。
因此解決卡頓現象的主要思路就是:儘量減小 CPU
和 GPU
資源的消耗。 ######CPU
大前端的開發框架主要分爲兩類:第一類是基於 WebView
的,第二類是相似 React Native
的。
對於第一類 WebView 的大前端渲染,主要工做在 WebKit 中完成。WebKit 的渲染層來自之前 macOS 的 Layer Rendering
架構,而 iOS 也是基於這一套架構。因此,從本質上來看,WebKit 和 iOS 原生渲染差異不大。
第二類的類 React Native
更簡單,渲染直接走的是 iOS 原生的渲染。那麼,咱們爲何會感受 WebView
和類 React Native
比原生渲染得慢呢?
從第一次內容加載來看,即便是本地加載,大前端也要比原生多出腳本代碼解析
的工做。
WebView 須要額外解析 HTML + CSS + JavaScript
代碼,而類 React Native 方案則須要解析 JSON + JavaScript
。HTML + CSS
的複雜度要高於 JSON
,因此解析起來會比 JSON 慢。也就是說,首次內容加載時,WebView
會比類 React Native
慢。
從語言自己的解釋執行性能來看,大前端加載後的界面更新會經過 JavaScript
解釋執行,而 JavaScript 解釋執行性能要比原生差,特別是解釋執行復雜邏輯或大量計算時。因此,大前端的運算速度,要比原生慢很多。
說完了大前端的渲染,你會發現,相對於原生渲染,不管是 WebView 仍是類 React Native 都會由於腳本語言自己的性能問題而在存在性能差距。那麼,對於 Flutter 這種沒有使用腳本語言,而且渲染引擎也是全新的框架,其渲染方式有什麼不一樣,性能又怎樣呢?
Flutter 界面是由 Widget
組成的,全部 Widget
組成 Widget Tree
,界面更新時會更新 Widget Tree
,而後再更新 Element Tree,最後更新 RenderObject Tree。
接下來的渲染流程,Flutter 渲染在 Framework 層會有 Build
、Wiget Tree
、Element Tree
、RenderObject Tree
、Layout
、Paint
、Composited Layer
等幾個階段。將 Layer 進行組合,生成紋理,使用 OpenGL 的接口向 GPU 提交渲染內容進行光柵化與合成,是在 Flutter 的 C++ 層,使用的是 Skia 庫。包括提交到 GPU 進程後,合成計算,顯示屏幕的過程和 iOS 原生基本是相似的,所以性能也差很少。