Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> UIView 繪制渲染機制

UIView 繪制渲染機制

編輯:關於Android編程

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];

我們上圖的堆棧信息 截圖 ,看到巴拉巴拉一大堆調用堆棧信息,其實這就是個函數做的孽 。如何不能理解,那直接看下面的流程圖吧。

流程圖

Created with Rapha?l 2.1.0程序啟動 UIApplicationMain()主線程:我是UI線程不能停,Runloop來和我一起吧。MainRunloop create and run MainRunloop:我想睡覺了,observer,你那邊有事嗎?observer:我去檢查一下_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()我去看看 圖層樹中有沒有待處理的對象有沒有?CPU:我在更新圖層樹,一會交給Core Animation運走Core Animation:把待處理的圖層對象 通過IPC發送到渲染服務進程GPU:渲染服務進程開始渲染工作GPU:Compositing\Offscreen Rendering 展示到屏幕 告訴runloop 讓它睡會吧。有東西,我在叫你observer。yesno

所謂的列表卡頓,到底是什麼原因引發的?

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 函數觸發的。

流程圖

Created with Rapha?l 2.1.0[self.view addSubview:view][CALayer drawInContext:][UIView(CALayerDelegate) drawLayer:inContext:][ZYYView drawRect:]

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 

@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之間關系

流程圖

Created with Rapha?l 2.1.0自定義ZYYView初始化、坐標確認 alloc setFrame?[self.view addSubview:view]; 確認 addsubview ?(隱式) 此view的layer的CALayerDelegate設置成此view1、首先CPU會為layer分配一塊內存用來繪制bitmap,叫做backing store2、layer創建指向這塊bitmap緩沖區的指針,叫做CGContextRef調用此view的self.layer的drawInContext:方法執行 - (void)drawInContext:(CGContextRef)ctxif([self.delegate responseToSelector:@selector(drawLayer:inContext:)])執行(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx是否調用[super drawLayer:layer inContext:ctx] 執行 - (void)drawRect:(CGRect)rect使用 UIkit繪制API 或者 CoreGraphics繪制API,繪制bitmap將layer的content指向生成的bitmap交付layer的content屬性drawrect 方法不調用drawrect 方法不調用yesnoyesnoyesnoyesno

底層原理

我們回過頭思考 圖形的上下文 CGContextRef的創建歷程。

? addsubview 的時候 觸發的
? CPU會為layer分配一塊內存用來繪制bitmap,叫做backing store
? layer創建指向這塊bitmap緩沖區的指針,叫做CGContextRef
? 通過CoreGraphic的api,也叫Quartz2D,繪制bitmap
? 將layer的content指向生成的bitmap

其實 CGContextRef 的創建過程 就是CPU的工作過程
CPU這一塊最耗時的地方往往在Core Graphic的繪制上,關於Core Graphic的性能優化是另一個話題了,又會牽扯到很多東西,就不在這裡討論了
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額外的計算工作。

Size

這個問題,主要是處理image帶來的,假如內存裡有一張400x400的圖片,要放到100x100的imageview裡,如果不做任何處理,直接丟進去,問題就大了,這意味著,GPU需要對大圖進行縮放到小的區域顯示,需要做像素點的sampling,這種smapling的代價很高,又需要兼顧pixel alignment。計算量會飙升。

Offscreen Rendering And Mask(離屏渲染)

如果我們對layer做這樣的操作:

label.layer.cornerRadius  = 5.0f;
label.layer.masksToBounds = YES;

會產生offscreen rendering,它帶來的最大的問題是,當渲染這樣的layer的時候,需要額外開辟內存,繪制好radius,mask,然後再將繪制好的bitmap重新賦值給layer。
因此繼續性能的考慮,Quartz提供了優化的api:

  label.layer.cornerRadius       = 5.0f;
  label.layer.masksToBounds      = YES;
  label.layer.shouldRasterize    = YES;
  label.layer.rasterizationScale = label.layer.contentsScale;

簡單的說,這是一種cache機制。
同樣GPU的性能也可以通過instrument去衡量:

這裡寫圖片描述

紅色代表GPU需要做額外的工作來渲染View,綠色代表GPU無需做額外的工作來處理bitmap。

未完待續。。。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved