UIView 繪製渲染機制
#前言
APP頁面優化對小編來說一直是難題,最近一直在不斷的學習和總結 ,發現APP頁面優化說到底離不開view的繪製和渲染機制。網上有很多精彩的部落格,小編借鑑之前N多大牛研究成果,同時結合自己遇到的一些問題,整理了這篇部落格。
嘗試和大家一起探討以下問題:
- view繪製渲染機制和runloop什麼關係?
- 所謂的列表卡頓,到底是什麼原因引發的?
- 我們經常在drawrect方法裡繪製程式碼,但該方法是誰呼叫的 何時呼叫的?
- drawrect方法內為何第一行程式碼往往要獲取圖形的上下文?
- layer的代理必須是view嗎,可以是vc嗎,為何CALayerDelegate 不能主動遵循?
- view繪製機制和CPU之間關係?
- view渲染機制和GPU之間關係?
- 所有的切圓角都很浪費效能嗎?
- 離屏渲染很nb嗎?
- 那些繪製API都是哪個類提供的 我如何系統的學習它?
- 如何優化CPU /GPU使用率?
view繪製渲染機制和runloop什麼關係?
程式碼示例
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ZYYView *view = [[ZYYView alloc] init];
view.backgroundColor = [UIColor whiteColor];
view.bounds = CGRectMake(0, 0, 100, 100);
view.center = CGPointMake(100, 100);
[self.view addSubview:view];
}
@end
@implementation ZYYView
- (void)drawRect:(CGRect)rect {
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,200));
CGContextSetRGBFillColor(con, 0 , 0, 1, 1);
CGContextFillPath(con);
}
@end
堆疊展示
底層原理
當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動呼叫了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法後,這個 UIView/CALayer 就被標記為待處理,並被提交到一個全域性的容器去。
蘋果註冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回撥去執行一個很長的函式:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函式裡會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪製和調整,並更新 UI 介面。
這個函式內部的呼叫棧大概是這樣的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
我們上圖的堆疊資訊 截圖 ,看到巴拉巴拉一大堆呼叫堆疊資訊,其實這就是個函式做的孽 。如何不能理解,那直接看下面的流程圖吧。
流程圖
所謂的列表卡頓,到底是什麼原因引發的?
iOS的mainRunloop是一個60fps的回撥,也就是說每16.7ms會繪製一次螢幕,這個時間段內要完成view的緩衝區建立,view內容的繪製(如果重寫了drawRect),這些CPU的工作。然後將這個緩衝區交給GPU渲染,這個過程又包括多個view的拼接(compositing),紋理的渲染(Texture)等,最終顯示在螢幕上。整個過程就是我們上面畫的流程圖。 因此,如果在16.7ms內完不成這些操作,比如,CPU做了太多的工作,或者view層次過於多,圖片過於大,導致GPU壓力太大,就會導致“卡”的現象,也就是丟幀
我們經常在drawrect方法裡繪製程式碼,但該方法是誰呼叫的 何時呼叫的?
產品繪圖需求
首先我們假設有這樣一個需求:實現下面的橢圓效果:
程式碼示例
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ZYYView *view = [[ZYYView alloc] init];
view.backgroundColor = [UIColor whiteColor];
view.bounds = CGRectMake(0, 0, 100, 100);
view.center = CGPointMake(100, 100);
[self.view addSubview:view];
}
@end
@implementation ZYYView
- (void)drawRect:(CGRect)rect {
CGContextRef con = UIGraphicsGetCurrentContext();
CGContextAddEllipseInRect(con, CGRectMake(0,0,100,200));
CGContextSetRGBFillColor(con, 0, 0, 1, 1);
CGContextFillPath(con);
}
@end
堆疊展示
底層原理
1、 在[ZYYView drawRect:] 方法之前,先呼叫了 [UIView(CALayerDelegate) drawLayer:inContext:] 和 [CALayer drawInContext:]
2、如果 [self.view addSubview:view]; 被登出掉 則 drawRect 不執行。可以肯定 drawRect
方法是由 addSubview 函式觸發的。
流程圖
drawrect方法內為何第一行程式碼總要獲取圖形的上下文
程式碼示例
CGContextRef con = UIGraphicsGetCurrentContext();
堆疊展示
底層原理
每一個UIView都有一個layer,每一個layer都有個content,這個content指向的是一塊快取,叫做backing store
當UIView被繪製時(從 CA::Transaction::commit:以後),CPU執行drawRect,通過context將資料寫入backing store
當backing store寫完後,通過render server交給GPU去渲染,將backing store中的bitmap資料顯示在螢幕上
所以在 drawRect 方法中 要首先獲取 context
layer的代理必須是view嗎,可以是vc嗎?為何CALayerDelegate 不能主動遵循?
程式碼示例
程式碼1
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
ZYYLayer *layer = [ZYYLayer layer];
layer.bounds = CGRectMake(0, 0, 100, 100);
layer.position = CGPointMake(100, 100);
[layer setNeedsDisplay];
[self.view.layer addSublayer:layer];
}
@end
@implementation ZYYLayer
- (void)drawInContext:(CGContextRef)ctx {
CGContextAddEllipseInRect(ctx, CGRectMake(0,0,100,200));
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
CGContextFillPath(ctx);
}
@end
程式碼二
#import <UIKit/UIKit.h>
@interface ViewController : UIViewController
@end
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
CALayer *layer = [CALayer layer];
layer.bounds = CGRectMake(0, 0, 100, 100);
layer.position = CGPointMake(100, 100);
layer.delegate = self;
[layer setNeedsDisplay];
[self.view.layer addSublayer:layer];
}
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
CGContextAddEllipseInRect(ctx, CGRectMake(0,0,100,200));
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
CGContextFillPath(ctx);
}
@end
圖示展示
綜合以上2種不同的繪製函式加上uiview下的drawrect方法 一起區別 :
編號 | 所在的類或類別 | 方法 | 出現範圍 | 可以使用的API | viewDidLoad | 優先順序 |
---|---|---|---|---|---|---|
1 | UIView(UIViewRendering) | drawRect | 自定義view類 | UIkit 、CoreGraphics | 3 | |
2 | CALayer | drawInContext | 自定義layer類 | CoreGraphics | [layer setNeedsDisplay] | 1 |
3 | NSObject (CALayerDelegate) | drawLayer:inContext | vc類、自定義layer、view類 | UIkit、CoreGraphics | [layer setNeedsDisplay] layer.delegate = self | 2 |
底層原理
不能再將某個UIView設定為CALayer的delegate,因為UIView物件已經是它內部根層的delegate,再次設定為其他層的delegate就會出問題。
在設定代理的時候,它並不要求我們遵守協議,說明這個方法為非正式協議,就不需要再額外的顯示遵守協議了
view繪製機制和CPU之間關係
建立物件
效能瓶頸:
建立物件會分配記憶體,物件過多,比較消耗 CPU 資源 。
優化方案:
1、儘量用輕量的物件代替重量的物件,可以對效能有所優化。比如 CALayer 比 UIView 要輕量,如果不需要響應觸控事件,用 CALayer 顯示會更加合適。如果物件不涉及 UI 操作,則儘量放到後臺執行緒去建立,但如果是包含了 CALayer 的控制元件,都只能在主執行緒建立和操作。
2、通過 Storyboard 建立檢視物件時,其資源消耗會比直接通過程式碼建立物件要大非常多。
3、使用懶載入,儘量推遲物件建立的時間,並把物件的建立分散到多個任務中去。
調整物件
調整物件檢視層級
效能瓶頸:
物件的調整也經常是消耗 CPU 資源的地方。檢視層次調整時,UIView、CALayer 之間會出現很多方法呼叫與通知。
優化方案:
儘量的避免或者減少調整檢視層次、新增和移除檢視。
調整物件佈局計算
效能瓶頸:檢視佈局的計算是 App 中最為常見的消耗 CPU 資源的地方
優化方案:不論通過何種技術對檢視進行佈局,其最終都會落到對 UIView.frame/bounds/center 等屬性的調整上。對這些屬性的調整非常消耗資源,所以儘量提前計算好佈局,如果一次性可以調整好對應屬性,就不要多次、頻繁的計算和調整這些屬性。
調整物件文字計算
效能瓶頸:如果一個介面中包含大量文字(比如微博微信朋友圈等),文字的寬高計算會佔用很大一部分資源。
優化方案:用 [NSAttributedString boundingRectWithSize:options:context:] 來計算文字寬高,用 -[NSAttributedString drawWithRect:options:context:] 來繪製文字,記住放到後臺執行緒進行以避免阻塞主執行緒。
影象的繪製
流程圖
底層原理
我們回過頭思考 圖形的上下文 CGContextRef的建立歷程。
• addsubview 的時候 觸發的
• CPU會為layer分配一塊記憶體用來繪製bitmap,叫做backing store
• layer建立指向這塊bitmap緩衝區的指標,叫做CGContextRef
• 通過CoreGraphic的api,也叫Quartz2D,繪製bitmap
• 將layer的content指向生成的bitmap
其實 CGContextRef 的建立過程 就是CPU的工作過程
CPU 將view變成了bitmap 完成自己工作,剩下就是GPU的工作了。
view渲染機制和GPU之間關係
GPU功能
GPU處理的單位是Texture
基本上我們控制GPU都是通過OpenGL來完成的,但是從bitmap到Texture之間需要一座橋樑,Core Animation正好充當了這個角色:
Core Animation對OpenGL的api有一層封裝,當我們的要渲染的layer已經有了bitmap content的時候,這個content一般來說是一個CGImageRef,CoreAnimation會建立一個OpenGL的Texture並將CGImageRef(bitmap)和這個Texture繫結,通過TextureID來標識。
這個對應關係建立起來之後,剩下的任務就是GPU如何將Texture渲染到螢幕上了。
GPU工作模式:
整個過程也就是一件事:CPU將準備好的bitmap放到RAM裡,GPU去搬這快記憶體到VRAM中處理。
而這個過程GPU所能承受的極限大概在16.7ms完成一幀的處理,所以最開始提到的60fps其實就是GPU能處理的最高頻率。
GPU效能瓶頸
因此,GPU的挑戰有兩個:
• 將資料從RAM搬到VRAM中
• 將Texture渲染到螢幕上
這兩個中瓶頸基本在第二點上。渲染Texture基本要處理這麼幾個問題:
Compositing:
Compositing是指將多個紋理拼到一起的過程,對應UIKit,是指處理多個view合到一起的情況,如
[self.view addsubview : subview]
如果view之間沒有疊加,那麼GPU只需要做普通渲染即可。 如果多個view之間有疊加部分,GPU需要做blending。
加入兩個view大小相同,一個疊加在另一個上面,那麼計算公式如下:
R = S+D*(1-Sa)
R: 為最終的畫素值
S: 代表 上面的Texture(Top Texture)
D: 代表下面的Texture(lower Texture)
Sa代表Texture的alpha值。
其中S,D都已經pre-multiplied各自的alpha值。
假如Top Texture(上層view)的alpha值為1,即不透明。那麼它會遮住下層的Texture。即,R = S。是合理的。 假如Top Texture(上層view)的alpha值為0.5,S 為 (1,0,0),乘以alpha後為(0.5,0,0)。D為(0,0,1)。 得到的R為(0.5,0,0.5)。
基本上每個畫素點都需要這麼計算一次。
因此,view的層級很複雜,或者view都是半透明的(alpha值不為1)都會帶來GPU額外的計算工作。
應用應當儘量減少檢視數量和層次,並在不透明的視圖裡標明 opaque 屬性以避免無用的 Alpha 通道合成。
Size
這個問題,主要是處理image帶來的,假如記憶體裡有一張400x400的圖片,要放到100x100的imageview裡,如果不做任何處理,直接丟進去,問題就大了,這意味著,GPU需要對大圖進行縮放到小的區域顯示,需要做畫素點的sampling,這種smapling的代價很高,又需要兼顧pixel alignment。計算量會飆升。
shouldRasterize
其中shouldRasterize(光柵化)是比較特別的一種:
光柵化概念:將圖轉化為一個個柵格組成的圖象。
光柵化特點:每個元素對應幀緩衝區中的一畫素。
shouldRasterize = YES在其他屬性觸發離屏渲染的同時,會將光柵化後的內容快取起來,如果對應的layer及其sublayers沒有發生改變,在下一幀的時候可以直接複用。shouldRasterize = YES,這將隱式的建立一個位圖,各種陰影遮罩等效果也會儲存到點陣圖中並快取起來,從而減少渲染的頻度(不是向量圖)。
相當於光柵化是把GPU的操作轉到CPU上了,生成點陣圖快取,直接讀取複用。
當你使用光柵化時,你可以開啟“Color Hits Green and Misses Red”來檢查該場景下光柵化操作是否是一個好的選擇。綠色表示快取被複用,紅色表示快取在被重複建立。
如果光柵化的層變紅得太頻繁那麼光柵化對優化可能沒有多少用處。點陣圖快取從記憶體中刪除又重新建立得太過頻繁,紅色表明快取重建得太遲。可以針對性的選擇某個較小而較深的層結構進行光柵化,來嘗試減少渲染時間。
注意:
對於經常變動的內容,這個時候不要開啟,否則會造成效能的浪費
例如我們日程經常打交道的TableViewCell,因為TableViewCell的重繪是很頻繁的(因為Cell的複用),如果Cell的內容不斷變化,則Cell需要不斷重繪,如果此時設定了cell.layer可光柵化。則會造成大量的離屏渲染,降低圖形效能。
Offscreen Rendering And Mask(離屏渲染)
GPU螢幕渲染有以下兩種方式:
On-Screen Rendering
意為當前螢幕渲染,指的是GPU的渲染操作是在當前用於顯示的螢幕緩衝區中進行。
Off-Screen Rendering
意為離屏渲染,指的是GPU在當前螢幕緩衝區以外新開闢一個緩衝區進行渲染操作。
設定了以下屬性時,都會觸發離屏繪製:
shouldRasterize(光柵化)
masks(遮罩)
shadows(陰影)
edge antialiasing(抗鋸齒)
group opacity(不透明)
複雜形狀設定圓角等
漸變
為什麼會使用離屏渲染
當使用圓角,陰影,遮罩的時候,圖層屬性的混合體被指定為在未預合成之前不能直接在螢幕中繪製,所以就需要螢幕外渲染被喚起。
螢幕外渲染並不意味著軟體繪製,但是它意味著圖層必須在被顯示之前在一個螢幕外上下文中被渲染(不論CPU還是GPU)。
效能瓶頸:
如果我們對layer做這樣的操作:
label.layer.cornerRadius = 5.0f;
label.layer.masksToBounds = YES;
會產生offscreen rendering,它帶來的最大的問題是,當渲染這樣的layer的時候,需要額外開闢記憶體,繪製好radius,mask,然後再將繪製好的bitmap重新賦值給layer。所以當使用離屏渲染的時候會很容易造成效能消耗,螢幕外緩衝區跟當前螢幕緩衝區上下文切換是很耗效能的。
優化方案:
1、因此繼續效能的考慮,Quartz提供了優化的api:
label.layer.cornerRadius = 5.0f;
label.layer.masksToBounds = YES;
label.layer.shouldRasterize = YES;
label.layer.rasterizationScale = label.layer.contentsScale;
簡單的說,這是一種cache機制。
2、只需要圓角的某些場合,也可以用一張已經繪製好的圓角圖片覆蓋到原本檢視上面來模擬相同的視覺效果。
3、最徹底的解決辦法,就是把需要顯示的圖形在後臺執行緒繪製為圖片,避免使用圓角、陰影、遮罩等屬性.
同樣GPU的效能也可以通過instrument去衡量:
紅色代表GPU需要做額外的工作來渲染View,綠色代表GPU無需做額外的工作來處理bitmap。
所有的切圓角都很浪費效能嗎?
iOS 9.0 之前UIimageView跟UIButton設定圓角都會觸發離屏渲染
iOS 9.0 之後UIButton設定圓角會觸發離屏渲染,而UIImageView裡png圖片設定圓角不會觸發離屏渲染了,如果設定其他陰影效果之類的還是會觸發離屏渲染的。
未完待續。。。