寫在前面
我們經(jīng)常在面試中,會被問及關(guān)于界面優(yōu)化相關(guān)的問題,比如為什么界面會出現(xiàn)卡頓?如何監(jiān)控卡頓?接著如何解決卡頓?那么本篇文章將重點(diǎn)分析一下卡頓的原理和解決的措施.
一、界面卡頓
通常來說,計(jì)算機(jī)中的顯示過程是下面這樣的,通過CPU
、GPU
、顯示器
協(xié)同工作來將圖片顯示到屏幕上
- 1、
CPU
計(jì)算好顯示內(nèi)容,提交至GPU
- 2、
GPU
經(jīng)過渲染完成后將渲染的結(jié)果放入FrameBuffer
(幀緩存區(qū)) - 3、隨后
視頻控制器
會按照VSync
信號逐行讀取FrameBuffer
的數(shù)據(jù) - 4、經(jīng)過可能的
數(shù)模轉(zhuǎn)換
傳遞給顯示器進(jìn)行顯示
最開始時(shí),FrameBuffer
只有一個(gè),這種情況下FrameBuffer
的讀取和刷新有很大的效率問題,為了解決這個(gè)問題,引入了雙緩存區(qū)
.即雙緩沖機(jī)制
.在這種情況下,GPU
會預(yù)先渲染好一幀
放入FrameBuffer
,讓視頻控制器讀取,當(dāng)下一幀渲染好后,GPU
會直接將視頻控制器的指針指向第二個(gè)FrameBuffer
.
雙緩存機(jī)制雖然解決了效率問題,但是隨之而言的是新的問題,當(dāng)視頻控制器還未讀取完成時(shí),例如屏幕內(nèi)容剛顯示一半,GPU
將新的一幀內(nèi)容提交到FrameBuffer
,并將兩個(gè)FrameBuffer
進(jìn)行交換后,視頻控制器就會將新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成屏幕撕裂
現(xiàn)象.
為了解決這個(gè)問題,采用了垂直同步信號機(jī)制
.當(dāng)開啟垂直同步后,GPU
會等待顯示器的VSync
信號發(fā)出后,才進(jìn)行新的一幀渲染和FrameBuffer
更新.而目前iOS設(shè)備中采用的正是雙緩存區(qū)+VSync
.
屏幕卡頓原因
下面我們來說說,屏幕卡頓的原因.
在VSync
信號到來后,系統(tǒng)圖形服務(wù)會通過 CADisplayLink
等機(jī)制通知 App
,App
主線程開始在CPU
中計(jì)算顯示內(nèi)容.隨后 CPU
會將計(jì)算好的內(nèi)容提交到 GPU
去,由GPU
進(jìn)行變換、合成、渲染.隨后 GPU
會把渲染結(jié)果提交到幀緩沖區(qū)
去,等待下一次 VSync
信號到來時(shí)顯示到屏幕上.由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機(jī)會再顯示
,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變.所以可以簡單理解掉幀
為過時(shí)不候
.
如下圖所示,是一個(gè)顯示過程,第1幀在VSync
到來前,處理完成,正常顯示,第2幀在VSync
到來后,仍在處理中,此時(shí)屏幕不刷新,依舊顯示第1幀,此時(shí)就出現(xiàn)了掉幀
情況,渲染時(shí)就會出現(xiàn)明顯的卡頓現(xiàn)象
.
從圖中可以看出,CPU
和GPU
不論是哪個(gè)阻礙了顯示流程,都會造成掉幀現(xiàn)象
,所以為了給用戶提供更好的體驗(yàn),在開發(fā)中,我們需要進(jìn)行卡頓檢測
以及相應(yīng)的優(yōu)化
.
二、卡頓監(jiān)控
卡頓監(jiān)控的方案一般有兩種:
FPS監(jiān)控
:為了保持流暢的UI
交互,App
的刷新拼搏應(yīng)該保持在60fps
左右,其原因是因?yàn)?code>iOS設(shè)備默認(rèn)的刷新頻率是60次/秒
,而1次刷新(即VSync
信號發(fā)出)的間隔是1000ms/60 = 16.67ms
,所以如果在16.67ms
內(nèi)沒有準(zhǔn)備好下一幀數(shù)據(jù),就會產(chǎn)生卡頓主線程卡頓監(jiān)控
:通過子線程監(jiān)測主線程的RunLoop
,判斷兩個(gè)狀態(tài)(kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
)之間的耗時(shí)是否達(dá)到一定閾值
FPS監(jiān)控
FPS
的監(jiān)控,參照YYKit
中的YYFPSLabel
,主要是通過CADisplayLink
實(shí)現(xiàn).借助link
的時(shí)間差,來計(jì)算一次刷新所需的時(shí)間,然后通過 刷新次數(shù) / 時(shí)間差
得到刷新頻次,并判斷是否其范圍,通過顯示不同的文字顏色來表示卡頓嚴(yán)重程度
.如果只是簡單的監(jiān)測,使用FPS
足夠了.
主線程卡頓監(jiān)控
除了FPS
,還可以通過RunLoop
來監(jiān)控,因?yàn)榭D的是事務(wù),而事務(wù)是交由主線程
的RunLoop
處理的.
實(shí)現(xiàn)思路:檢測主線程每次執(zhí)行消息循環(huán)的時(shí)間,當(dāng)這個(gè)時(shí)間大于規(guī)定的閾值時(shí),就記為發(fā)生了一次卡頓.這個(gè)也是微信卡頓三方matrix
的原理.
卡頓檢測三方庫:
Swift
的卡頓檢測第三方ANREye,其主要思路是:創(chuàng)建子線程進(jìn)行循環(huán)監(jiān)測,每次檢測時(shí)設(shè)置標(biāo)記置為true,然后派發(fā)任務(wù)到主線程,標(biāo)記置為false,接著子線程睡眠超過閾值時(shí),判斷標(biāo)記是否為false,如果沒有,說明主線程發(fā)生了卡頓OC
可以使用 微信matrix、滴滴DoraemonKit
三、界面優(yōu)化
CPU層面的優(yōu)化
1、盡量
用輕量級的對象
代替重量級的對象,可以對性能有所優(yōu)化,例如 不需要相應(yīng)觸摸事件的控件,用CALayer
代替UIView
-
2、盡量減少對
UIView
和CALayer
的屬性修改CALayer
內(nèi)部并沒有屬性,當(dāng)調(diào)用屬性方法時(shí),其內(nèi)部是通過運(yùn)行時(shí)resolveInstanceMethod
為對象臨時(shí)添加一個(gè)方法,并將對應(yīng)屬性值保存在內(nèi)部的一個(gè)Dictionary
中,同時(shí)還會通知delegate
、創(chuàng)建動畫等,非常耗時(shí)UIView
相關(guān)的顯示屬性,例如frame
、bounds
、transform
等,實(shí)際上都是從CALayer
映射來的,對其進(jìn)行調(diào)整時(shí),消耗的資源比一般屬性要大
3、當(dāng)有大量對象釋放時(shí),也是非常耗時(shí)的,盡量挪到后臺線程去釋放
4、盡量
提前計(jì)算視圖布局
,即預(yù)排版
,例如cell
的行高5、
Autolayout
在簡單頁面情況下們可以很好的提升開發(fā)效率,但是對于復(fù)雜視圖而言,會產(chǎn)生嚴(yán)重的性能問題,隨著視圖數(shù)量的增長,Autolayout
帶來的CPU
消耗是呈指數(shù)上升的.所以盡量使用代碼布局
.如果不想手動調(diào)整frame
等,也可以借助三方庫,例如Masonry(OC)、SnapKit(Swift)、ComponentKit、AsyncDisplayKit等
-
6、文本處理的優(yōu)化:當(dāng)一個(gè)界面有大量文本時(shí),其行高的計(jì)算、繪制也是非常耗時(shí)的
-
1)如果對文本沒有特殊要求,可以使用
UILabel
內(nèi)部的實(shí)現(xiàn)方式,且需要放到子線程中進(jìn)行,避免阻塞主線程- 計(jì)算文本寬高:
[NSAttributedString boundingRectWithSize:options:context:]
- 文本繪制:
[NSAttributedString drawWithRect:options:context:]
- 計(jì)算文本寬高:
2)自定義文本控件,利用
TextKit
或最底層的CoreText
對文本異步繪制.并且CoreText
對象創(chuàng)建好后,能直接獲取文本的寬高等信息,避免了多次計(jì)算(調(diào)整和繪制都需要計(jì)算一次).CoreText
直接使用了CoreGraphics
占用內(nèi)存小,效率高
-
-
7、圖片處理(解碼 + 繪制)
當(dāng)使用
UIImage
或CGImageSource
的方法創(chuàng)建圖片時(shí),圖片的數(shù)據(jù)不會立即解碼
,而是在設(shè)置時(shí)解碼(即圖片設(shè)置到UIImageView/CALayer.contents
中,然后在CALayer
提交至GPU
渲染前,CGImage
中的數(shù)據(jù)才進(jìn)行解碼).這一步是無可避免
的,且是發(fā)生在主線程
中的.想要繞開這個(gè)機(jī)制,常見的做法是在子線程中先將圖片繪制到CGBitmapContext
,然后從Bitmap
直接創(chuàng)建圖片,例如SDWebImage
三方框架中對圖片編解碼的處理。這就是Image
的預(yù)解碼當(dāng)使用
CG
開頭的方法繪制圖像到畫布中,然后從畫布中創(chuàng)建圖片時(shí),可以將圖像的繪制在子線程中進(jìn)行
-
8、圖片優(yōu)化
- 盡量使用
PNG
圖片,不使用JPGE
圖片 - 通過
子線程預(yù)解碼,主線程渲染
,即通過Bitmap
創(chuàng)建圖片,在子線程賦值image
- 優(yōu)化圖片大小,盡量避免動態(tài)縮放
- 盡量將多張圖合為一張進(jìn)行顯示
- 盡量使用
9、盡量避免使用透明
view
,因?yàn)槭褂猛该?code>view,會導(dǎo)致在GPU
中計(jì)算像素時(shí),會將透明view
下層圖層的像素也計(jì)算進(jìn)來,即顏色混合處理
10、按需加載,例如在
TableView
中滑動時(shí)不加載圖片,使用默認(rèn)占位圖,而是在滑動停止時(shí)加載11、少使用
addView
給cell
動態(tài)添加view
GPU層面優(yōu)化
相對于CPU
而言,GPU
主要是接收CPU
提交的紋理+頂點(diǎn)
,經(jīng)過一系列transform
,最終混合并渲染,輸出到屏幕上.
1、盡量
減少在短時(shí)間內(nèi)大量圖片的顯示
,盡可能將多張圖片合為一張顯示
,主要是因?yàn)楫?dāng)有大量圖片進(jìn)行顯示時(shí),無論是CPU
的計(jì)算還是GPU
的渲染,都是非常耗時(shí)的,很可能出現(xiàn)掉幀的情況2、盡量避免圖片的尺寸超過
4096×4096
,因?yàn)楫?dāng)圖片超過這個(gè)尺寸時(shí),會先由CPU
進(jìn)行預(yù)處理,然后再提交給GPU
處理,導(dǎo)致額外CPU
資源消耗3、盡量減少視圖數(shù)量和層次,主要是因?yàn)橐晥D過多且重疊時(shí),
GPU
會將其混合,混合的過程也是非常耗時(shí)的4、盡量避免離屏渲染
5、異步渲染,例如可以將
cell
中的所有控件、視圖合成一張圖片進(jìn)行顯示.可以參考Graver三方框架
提示
上述這些優(yōu)化方式的落地實(shí)現(xiàn),需要根據(jù)自身項(xiàng)目進(jìn)行評估,合理的使用進(jìn)行優(yōu)化
寫在后面
和諧學(xué)習(xí),不急不躁.我還是我,顏色不一樣的煙火.