一、概述
本文從圖像顯示原理開始,延伸到性能優化的常見問題,也提供了一些常見的優化方式。中間也穿插了一些面試時經常會問到的相關技巧和細節。
目錄結構:
1、UIView和CALayer
1.1 描述與區別
1.2 bounds的x和y
1.3 隱式動畫
2、事件傳遞與視圖響應鏈
2.1 響應者鏈條
2.2 事件的產生、傳遞、響應
2.3 hit-test:以及 pointInside: 方法
3、圖像顯示原理
3.1 整體原理描述
3.2 CPU和GPU的分工
3.3 UI繪制原理
3.3.1 系統繪制
3.3.2 異步繪制
3.4 layoutSubViews、setNeedsLayout、layoutIfNeeded 、setNeedsDisplay 方法介紹
4、UI卡頓掉幀原因
4.1 原因
5、CPU 資源消耗解決方案
5.1 綜合描述
5.2 對象創建
5.3 對象調整、銷毀
5.4 預排版、布局計算
5.5 圖片解碼
5.6 圖片繪制
6、GPU 資源消耗原因和解決方案
6.1 綜合描述
6.2 紋理的渲染
6.3 視圖的混合
6.4 圖形的生成
6.4.1 離屏渲染的問題
6.4.2 離屏渲染的觸發因素
6.4.3 ASDK:AsyncDisplayKit(Texture)
6.4.4 光柵化
二、 圖像顯示,事件傳遞,性能優化,離屏渲染
1、UIView和CALayer
- 綜述:
(1)UIView中有個屬性layer,是CALayer類型。
(2)UIView是CALayer的delegate。
(3)UIView繼承自UIResponder,可以響應事件。
(4)CALayer繼承自NSObject,包含要顯示的內容contents,以及backgroundcolor,frame,bounds等控件的尺寸和樣式。
(5)CALayer的contents中是backing store,實際上是bitmap類型的位圖。
(6)兩者都有層級結構。
(7)CALayer中透明度使用opacity表示而不是alpha;中心點使用position表示而不是center。
(8)CALayer的AnchorPoint、Position
-
- anchorPoint屬性是圖層的錨點,范圍在(01,01)表示在x、y軸的比例,這個點永遠可以同position(中心點)重合,當圖層中心點固定后,調整anchorPoint即可達到調整圖層顯示位置的作用(因為它永遠和position重合)
-
- CALayer里的AnchorPoint(錨點):是相對於自身layer,iOS坐標系中(0,0), (1,1)分別表示左上角、右下角。AnchorPoint相當於支點,可以用作旋轉變化、平移、縮放。
- CALayer里的position: 是AnchorPoint點在superLayer中的位置坐標。position點是相對superLayer的。
- 關於Position和AnchorPoint圖示(如果修改anchorPoint則layer的frame會發生改變,position不會發生改變.修改position與anchorPoint中任何一個屬性都不影響另一個屬性)
- 最大差異:
<1> UIView為CALayer顯示提供基礎,UIView負責處理觸摸等事件,參與事件響應鏈。
<2> CALayer只是負責提供內容contents的繪制和顯示。
- 補充:關於bounds的x和y的問題
<1>示例代碼一:只改變父視圖的bounds的x、y
<2>效果一:子視圖參考的坐標系原點向左和向上偏移了20
<3>示例代碼二:改變視圖bounds的w和h,使其大於自己frame的w和h
<4>效果二:視圖frame的x、y、w、h都變了50(撐大了)
bounds的w和h,不僅會影響frame的w和h(兩者的width和height保持一致),還會影響frame的x,y。這種影響是隨着bounds的w和h增加或減少,平均擴充或縮減四周的區域。
-
- 它可以修改自己坐標系的原點位置,進而影響“子view”的顯示位置(改變了子視圖的參考原點)。
- bounds可以通過改變寬高,改變自身的frame,進而影響到自己在父視圖的顯示位置和大小。
- 補充:UIView關聯的layer和單獨的layer在動畫上的不同
<1> 隱式動畫:我們並沒有指定任何動畫類型,僅僅改變了一個屬性,然后CoreAnimation來決定如何並何時去做動畫(區別於顯示動畫需要配置)。當我們改變一個屬性的時候,CoreAnimation是如何判斷動畫類型和持續時間(默認0.25s)的呢?實際上動畫執行的時間取決於當前事務的設置,動畫類型取決於圖層行為。
2、事件傳遞與視圖響應鏈 :
(1)響應者鏈條:是通過遞歸構成的一組UIResponder對象的鏈式序列。
(2)事件的產生、傳遞、處理:
<1>產生:手指觸摸屏幕的某一個view的時候,發生觸摸事件,系統會把該事件加入UIApplication管理的事件隊列中去,這個隊列是先進先出的。
<2>傳遞:UIApplication會從事件隊列中去取最前面的事件,並將事件分發下去:
- 先發送事件給應用程序的主窗口(keyWindow)
- 主窗口會在視圖層次結構上面找到一個最合適的視圖來處理觸摸事件(hit-test 循環遍歷)
- 如果沒有找到最合適處理的子控件,那么就是自己最合適處理
<3>處理:按照響應者鏈條,從找到的最合適的子控件開始處理事件:
- 調用該視圖的touches方法來處理事件
- 如果UIView不處理,向上一個響應者傳遞(如果當前這個view是控制器的view,那么控制器就是上一個響應者;如果當前這個view不是控制器的view,那么父控件就是上一個響應者)
- 如果控制器也不處理,繼續向上給UIWindow傳遞。
- 如果UIWindow也不處理,繼續向上給UIApplication傳遞。
- 如果UIApplication也不處理,丟棄
<4>整體流程示例
(3)核心關鍵點hit-test:以及 pointInside: 方法。給出一個模擬寫法
3、圖像顯示原理
(1)整體原理
<1> CPU:輸出位圖
<2> GPU:圖層渲染,紋理合成
<3> 把結果放到幀緩沖區(frame buffer)中,這些緩沖區可能是在一塊內存區域,也可能單獨分開,看硬件(顯存)。
<4> 再由視頻控制器根據vsync信號(垂直同步信號)在指定時間之前去提取幀緩沖區的屏幕顯示內容
<5> 經過可能的數模轉換傳遞給顯示器,顯示到屏幕上
(2)CPU和GPU的分工
- CPU工作:UI布局,文本計算、繪制、圖片解碼、提交位圖
- GPU渲染管線(OpenGL):頂點着色,圖元裝配,光柵化,片段着色,片段處理
(3)UI繪制原理
- 綜述
當調用UIView的setNeedsDisplay方法時,會調用CALayer的同名方法setNeedsDisplay,這時並沒有立即發生繪制,而只是相當於在當前layer打上了標記, 會在Runloop即將結束時才會調用[CALayer display]。而這個方法的內部會判斷是否實現了displayLayer這個方法,如果沒有實現,那么走系統調用,如果實現了,就為我們異步繪制提供了入口。
- 系統繪制
<1> CALayer內部會創建一個backing store(CGContextRef上下文)
<2> 判斷layer是否有delegate:
(1) 如果有delegate,執行[layer.delegate drawLayer:inContext](在系統內部執行的),然后在這個方法中會調用view的drawRect:方法(我們重寫view的drawRect:方法才會被調用到)。
(2) 如果沒有delegate,會調用layer的drawInContext方法,我們可以重寫layer的該方法,此刻會被調用到。
<3> 最后把繪制完的backing store(位圖)提交給GPU。
- 異步繪制:
<1> 通過實現layer的代理方法displayLayer
<2> 代理負責生成對應的bitmap
<3> 設置該bitmap作為layer.contents屬性的值
(下面講到CPU優化的時候,會再次涉及異步繪制)
(4)四個方法:layoutSubViews、setNeedsLayout、layoutIfNeeded 、setNeedsDisplay
- layoutSubViews:
1、重新定義子元素的位置和大小
2、以下情況會調用
(1)init初始化不會觸發layoutSubviews,但是使用initWithFrame 進行初始化時,當rect的值 非CGRectZero時會觸發。
(2)addSubview會觸發layoutSubviews
(3)frame的值設置前后發生了變化
(4)滾動一個UIScrollView會觸發layoutSubviews
(5)旋轉Screen會觸發父UIView上的layoutSubviews事件
(6)當刪除、添加、修改子視圖的時候,父視圖會調用
(7)調用setNeedsLayout,調用LayoutIfNeeds
- setNeedsLayout
1、未來某個時刻刷新布局,不會立即刷新,需要調用layoutIfNeeded
2、會調用layoutSubViews
- setNeedsDisplay
1、重新繪畫(當前runloop將要結束的時候開始繪制)。
2、會調用drawRect方法。
- layoutIfNeeded
1、立即刷新布局,配合setNeedsLayout使用。
4、UI卡頓掉幀原因
(1)原因
<1> iOS設備的硬件時鍾會發出Vsync(垂直同步信號)。
<2> 在 VSync 信號到來后,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等。
<3> CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。
<4> 隨后 GPU 會把渲染結果提交到幀緩沖區去,等待下一次 VSync 信號到來時顯示到屏幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。
<5> 一般來說,頁面滑動流暢是60fps,也就是1s有60幀更新,即每隔16.7ms就要產生一幀畫面,如果CPU和GPU加起來的處理時間超過了16.7ms,就會造成掉幀甚至卡頓。
<6> CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象。所以開發時,需要分別對 CPU 和 GPU 壓力進行評估和優化。
5、CPU 資源消耗解決方案
- 綜述:
1、使用輕量級對象
2、減少對象的屬性調整
3、做預排版:布局計算
4、減少控件:文本控件畫上去,drawInRect方法
5、開辟子線程:進行耗時的任務,包括創建對象
6、異步圖像解碼:后台線程先把圖片繪制到 CGBitmapContext ,再從 Bitmap 直接創建圖片。
7、異步繪制(上面講過):起子線程,調用CoreGraphic
- 詳細:
1、對象創建:
(1)盡量用輕量級的對象,比如用不到事件處理的地方,可以考慮使用CAlayer取代UIView。
(2)能用基本數據類型,就別用NSNumber類型。
(3)不涉及UI就盡量放到后台線程創建。
(4)Storyboard創建視圖對象消耗資源會大很多。
(5)盡量推遲對象創建時間。
(6)盡量復用。
2、對象調整、銷毀:
(1)減少不必要的屬性調整:比如UIView 顯示相關的屬性(比如 frame/bounds/transform)等實際上都是 CALayer 屬性映射來的,所以對 UIView 的這些屬性進行調整時,消耗的資源要遠大於一般的屬性。
(2)少改視圖層次:調整層次、添加、移除等。
(3)對象丟到后台去釋放
3、預排版、布局計算:
(1)數據加工:JSON轉模型時,提前計算好布局相關數據,封裝一個對象,用來儲存計算好的所有布局數據,包括富文本信息。(可以在子線程里計算)
(2)少用Autolayout。隨着視圖數量的增長,Autolayout 帶來的 CPU 消耗會呈指數級上升。
(3)減少文本控件:少用UILabel等文本控件,可以“畫”上去,同時可以放到后台線程。(類似[str drawInRect: withFont:])
(4)行高緩存:比如FDTemplateLayoutCell,思路和數據加工有相同之處。
(5)文本渲染:自定義控件,用CoreText對文本異步繪制。(文本控件在底層都是在主線程通過 CoreText 排版、繪制為 Bitmap 顯示的)
4、圖片解碼:
(1)解碼概念:不管 JPEG 還是 PNG ,都是壓縮的位圖圖形“數據”( PNG 是無損壓縮,並且支持 alpha 通道, JPEG 是有損壓縮,可以指定 0-100% 的壓縮比),在將磁盤中的圖片渲染到屏幕之前,必須先要得到圖片的原始像素數據(解碼成位圖數據),才能執行后續的繪制操作。
(2)用 UIImage 或 CGImageSource 的那幾個方法創建圖片時,圖片數據並不會立刻解碼。圖片設置到 UIImageView 或者 CALayer.contents 中去,並且 CALayer 被提交到 GPU 前,CGImage 中的數據才會得到解碼。這一步是發生在主線程的,並且不可避免。如果想要繞開這個機制,常見的做法是在后台線程先把圖片繪制到 CGBitmapContext 中,然后從 Bitmap 直接創建圖片。目前常見的網絡圖片庫都自帶這個功能(AFN、SDWebImage)。
5、圖片繪制
(1)線程安全:CoreGraphic方法通常線程安全
(2)異步繪制(上面有講過):CG 開頭的方法把圖像繪制到畫布中,然后從畫布創建圖片並顯示。這個最常見的地方就是 drawRect 里面。也可以是上面介紹過的,實現layer代理的displayLayer方法。
6、其他:
(1)耗時操作盡量放到子線程。
(2)控制線程最大並發數,開辟線程的操作本身也消耗資源。
7、補充: UIImage 的兩種創建方法
(1)非緩存
-
- +(UIImage *)imageWithContentsOfFile:(NSString *)path
- +(UIImage *)imageWithData:(NSData *)data
(2)有緩存:注意使用場景
-
- +(UIImage*)imageNamed:(NSString*)name
6、GPU 資源消耗原因和解決方案
- 綜述:
1、視圖層數控制:盡量減少視圖數量和層次
2、控制圖片大小:GPU能處理的最大紋理尺寸是4096x4096,一旦超過這個尺寸,就會占用CPU資源進行處理
3、減少圖片數量:盡量避免段時間內大量圖片的顯示,盡可能將多張圖片合成一張圖片顯示
4、減少透明的視圖:少用透明圖(alpha<1),不透明的就設置opaque為yes
5、盡量避免出現離屏渲染:光柵化、切角、陰影、遮罩
6、使用第三方異步框架 AsyncDisplayKit(Texture):異步處理文本和布局的計算、渲染、解碼、繪制等任務(同時優化CPU和GPU的資源銷毀)
- 詳述:
GPU 做的事情(OpenGL紋理渲染):
1、接收提交的紋理(Texture)和頂點描述(三角形),應用變換(transform)、混合並渲染,然后輸出到屏幕上。
2、通常能看到的內容,主要也就是紋理(圖片)和形狀(三角模擬的矢量圖形)兩類。
- 可優化的點:紋理渲染(減少圖片數量和大小),視圖混合(減少圖層、控制透明度),圖形生成(避免離屏渲染)
1、紋理的渲染
-
- 減少短時間內大量圖片的顯示:所有的 Bitmap(圖片、文本、柵格化),都要由內存提交到顯存,綁定為 GPU Texture。不論是提交到顯存的過程,還是 GPU 調整和渲染 Texture 的過程,都要消耗不少 GPU 資源。當在較短時間顯示大量圖片時,CPU 占用率很低,GPU 占用非常高,界面仍然會掉幀。避免這種情況的方法只能是盡量減少在短時間內大量圖片的顯示,盡可能將多張圖片合成為一張進行顯示。
- 減少圖片(紋理)大小:當圖片過大,超過 GPU 的最大紋理尺寸時,圖片需要先由 CPU 進行預處理,這對 CPU 和 GPU 都會帶來額外的資源消耗。目前來說,iPhone 4S 以上機型,紋理尺寸上限都是 4096x4096。所以,盡量不要讓圖片和視圖的大小超過這個值。
2、視圖的混合
-
- 減少視圖數量和層數:當多個視圖(或者 CALayer)重疊在一起顯示時,GPU 會首先把他們混合到一起。如果視圖結構過於復雜,混合的過程也會消耗很多 GPU 資源。為了減輕這種情況的 GPU 消耗,應用應當盡量減少視圖數量和層次,
- 優化配置透明度:在不透明的視圖里標明 opaque 屬性以避免無用的 Alpha 通道合成。減少透明的視圖(alpha<1),不透明的就設置opaque為yes。
- 合並圖片:把多個視圖預先渲染為一張圖片來顯示。
3、圖形的生成。
-
- 在OpenGL中,GPU有2種渲染方式:
(1)On-SCreen Rendering:當前屏幕渲染,在當前用於顯示的屏幕緩沖區進行渲染操作。
(2)Off-Screen Rendring: 離屏渲染,在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。
-
- 離屏渲染消耗性能的原因:
(1)需要創建新的緩沖區;
(2)需要多次切換上下文環境。先是從當前屏幕切換到離屏;等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上,又需要將上下文環境從離屏切換到當前屏幕。
-
- 注意觸發離屏渲染的操作:
(1)光柵化,layer.shouldRasterize = YES(把layer轉化為位圖並緩存)
(2)遮罩,layer.mask
(3)圓角,同時設置layer.maskToBounds = Yes,layer.cornerRadis 大於0,考慮通過CoreGraphics繪制裁剪圓角,或者美工提供圓角圖片
(4)陰影,layer.shadowXXX。如果設置了layer.shadowPath就不會產生離屏渲染
-
- 通過后台線程繪制為圖片:
最徹底的解決辦法,就是把需要顯示的圖形在后台線程繪制為圖片,避免使用圓角、陰影、遮罩等屬性。
-
- ASDK:AsyncDisplayKit(Texture)
(1)AsyncDisplayKit 是 Facebook 開源的一個用於保持 iOS 界面流暢的庫.
(2)文本和布局的計算、渲染、解碼、繪制都希望通過各種方式異步執行,但 UIKit 和 Core Animation 相關操作必需在主線程進行。ASDK 的目標,就是盡量把這些任務從主線程挪走,而挪不走的,就盡量優化性能。
(3)ASDK對UIKit進行封裝,提供了幾乎全部控件和屬性
(4)與 UIView 和 CALayer 不同,ASDisplayNode 是線程安全的,它可以在后台線程創建和修改。
-
- 特殊的離屏渲染:一種特殊的“離屏渲染”方式—— CPU渲染。
(1) 如果重寫了drawRect方法,並且使用任何Core Graphics的技術進行了繪制操作,就涉及到了CPU渲染。整個渲染過程由CPU在App內“同步”地完成,渲染得到的bitmap最后再交由GPU用於顯示。
(2) CoreGraphic通常是線程安全的,所以可以進行異步繪制,顯示的時候再放回主線程。
-
- 關於光柵化,shouldRasterize的設置(做了解)
1、shouldRasterize = YES,在其他屬性觸發離屏渲染的同時,會將光柵化后的內容緩存起來,如果對應的layer及其sublayers沒有發生改變,在下一幀的時候可以直接復用。shouldRasterize = YES,這將隱式的創建一個位圖,各種陰影遮罩等效果也會保存到位圖中並緩存起來,從而減少渲染的頻度(不是矢量圖)。
2、如果我們更新已柵格化的layer,會造成大量的離屏渲染。因此CALayer的柵格化選項的開啟與否需要我們仔細衡量使用場景。只能用在圖像內容不變的前提下的:
(1)用於避免靜態內容的復雜特效的重繪,例如前面講到的UIBlurEffect(毛玻璃)
(2)用於避免多個View嵌套的復雜View的重繪。
(3)而對於經常變動的內容,這個時候不要開啟,否則會造成性能的浪費。例如TableViewCell,因為Cell的重繪是很頻繁的(復用),如果Cell的內容不斷變化,則Cell需要不斷重繪,如果此時設置了cell.layer可柵格化,則會造成大量的離屏渲染,降低圖形性能。
(4)使用shouldRasterize后layer會緩存為Bitmap位圖,對一些添加了shawdow等效果的耗費資源較多的靜態內容進行緩存,能夠得到性能的提升。
3、不要過度使用,系統限制了緩存的大小為2.5 x Screen Size。如果過度使用,超出緩存之后,同樣會造成大量的離屏渲染。
4、被柵格化的圖片如果超過100ms沒有被使用,則會被移除。因此我們應該只對連續不斷使用的圖片進行緩存。對於不常使用的圖片緩存是沒有意義,且耗費資源的。
5、監測離屏渲染
Instruments的Core Animation工具中有幾個和離屏渲染相關的檢查選項:
(1)Color Offscreen-Rendered Yellow
開啟后會把那些需要離屏渲染的圖層高亮成黃色,這就意味着黃色圖層可能存在性能問題。
(2)Color Hits Green and Misses Red
如果圖層是綠色,就表示這些緩存被復用;如果是紅色就表示緩存會被重復創建,這就表示該處存在性能問題了。 比如UIView.layer.shouldRasterize = YES 時,生成的位圖會緩沖起來,如果TabelView 滑動的時候(UITableViewCell 復用)使用緩存直接命中,就顯示綠色,反之,如果不命中,這時就顯示紅色。紅色越多,性能越差。
補充:
(1)iOS 9.0 之前UIimageView跟UIButton設置圓角都會觸發離屏渲染
(2)iOS 9.0 之后UIButton設置圓角會觸發離屏渲染,而UIImageView里png圖片設置圓角不會觸發離屏渲染了,如果設置其他陰影效果之類的還是會觸發離屏渲染的。