離屏渲染


圖像顯示原理

enter image description here

圖像顯示的大概流程:

  1. 程序運行從內(nèi)存中讀取數(shù)據(jù)
    • 對圖片進行解壓得到像素數(shù)據(jù),若GPU不支持圖片的顏色格式,CPU需要進行格式轉(zhuǎn)換
    • CoreText和CoreGraphics跟進文本內(nèi)容生成位圖
  2. 然后解壓后的數(shù)據(jù)或位圖通過GPU Bus上傳到GPU,GPU需要將每一個frame的紋理(位圖)合成在一起(一秒60次)。每一個紋理會占用VRAM(video RAM),所以需要給 GPU 同時保持紋理的數(shù)量做一個限制。GPU 在合成方面非常高效,但是某些合成任務卻比其他更復雜,并且 GPU在 16.7ms(1/60s)內(nèi)能做的工作也是有限的。
CPU的工作
  • 對象的創(chuàng)建、調(diào)整和銷毀
  • 布局計算
  • 文本計算
  • 文本渲染
  • 圖片的解碼
  • 圖像的繪制
GPU的工作
  • 紋理的渲染
  • 視圖合成
  • 圖形生成

GPU顯示圖像

enter image description here
  1. CPU 計算好顯示內(nèi)容提交到 GPU
  2. GPU 渲染完成后將渲染結(jié)果放入幀緩沖區(qū)
  3. 隨后視頻控制器會按照VSync信號逐行讀取幀緩沖區(qū)的數(shù)據(jù),經(jīng)過可能的數(shù)模轉(zhuǎn)換傳遞給顯示器顯示。

????在最簡單的情況下,幀緩沖區(qū)只有一個,這時幀緩沖區(qū)的讀取和刷新都都會有比較大的效率問題。為了解決效率問題,顯示系統(tǒng)通常會引入兩個緩沖區(qū),即雙緩沖機制。在這種情況下,GPU 會預先渲染好一幀放入一個緩沖區(qū)內(nèi),讓視頻控制器讀取,當下一幀渲染好后,GPU 會直接把視頻控制器的指針指向第二個緩沖器。如此一來效率會有很大的提升。

垂直同步機制
enter image description here

????當視頻控制器還未讀取完成時,即屏幕內(nèi)容剛顯示一半時,GPU 將新的一幀內(nèi)容提交到幀緩沖區(qū)并把兩個緩沖區(qū)進行交換后,視頻控制器就會把新的一幀數(shù)據(jù)的下半段顯示到屏幕上,造成畫面撕裂現(xiàn)象。
????為了解決這個問題,GPU 通常有一個機制叫做垂直同步(簡寫也是 V-Sync)
????首先從過去的CRT顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現(xiàn)一幀畫面,隨后電子槍回到初始位置繼續(xù)下一次掃描。為了把顯示器的顯示過程和系統(tǒng)的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產(chǎn)生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發(fā)出一個水平同步信號(horizonal synchronization),簡稱HSync;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發(fā)出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產(chǎn)生的頻率。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了,但原理仍然沒有變。
????當開啟垂直同步后,GPU 會等待顯示器的VSync信號發(fā)出后,才進行新的一幀渲染和緩沖區(qū)更新。這樣能解決畫面撕裂現(xiàn)象,也增加了畫面流暢度,但需要消費更多的計算資源,也會帶來部分延遲。

卡頓的產(chǎn)生

enter image description here

????在VSync信號到來后,系統(tǒng)圖形服務會通過CADisplayLink等機制通知App,App主線程開始在CPU中計算顯示內(nèi)容,比如視圖的創(chuàng)建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內(nèi)容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結(jié)果提交到幀緩沖區(qū)去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內(nèi),CPU 或者 GPU 沒有完成內(nèi)容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內(nèi)容不變。這就是界面卡頓的原因。
????從上面的圖中可以看到,CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現(xiàn)象。所以開發(fā)時,也需要分別對 CPU 和 GPU 壓力進行評估和優(yōu)化。

離屏渲染

GPU的渲染方式

  • On-Screen Rendering(當前屏幕渲染):指的是GPU的渲染操作是在當前用于顯示的屏幕緩沖區(qū)中進行。


    enter image description here
  • Off-Screen Rendering (離屏渲染),指的是GPU在當前屏幕緩沖區(qū)以外新開辟一個緩沖區(qū)進行渲染操作。


    enter image description here

    ????OffScreen Rendering 則多了一個步驟,GPU 會先創(chuàng)建一個屏外緩沖區(qū)(OffScreenBuffer),然后在其中進行渲染,最后將渲染結(jié)果提交到幀緩沖區(qū)內(nèi)(FrameBuffer);這其中還涉及到了兩次上下文的轉(zhuǎn)換,首先把當前上下文轉(zhuǎn)換到屏外緩沖區(qū)(OffScreenBuffer),然后又轉(zhuǎn)換到幀緩沖區(qū)(FrameBuffer)。整個過程會造成很大的消耗。例如蒙板操作:
    ????在前兩個渲染通道中,GPU分別得到了紋理(texture,也就是那個相機圖標)和layer(藍色的蒙版)的渲染結(jié)果。但這兩個渲染結(jié)果沒有直接放入Render Buffer中,也就表示這是離屏渲染。直到第三個渲染通道,才把兩者組合起來放入Render Buffer中。離屏渲染意味著把渲染結(jié)果臨時保存,等用到時再取出,因此相對于普通渲染更占用資源。

CPU 渲染
????如果我們重寫了drawRect方法,并且使用任何Core Graphics的技術(shù)進行了繪制操作,就涉及到了CPU渲染。由CPU處理的一種特殊渲染方式,在App內(nèi)同步完成,渲染得到的bitmap最后再交由GPU用于顯示,由于CPU自身做渲染的性能也不好,所以這種方式也是需要盡量避免的。

enter image description here

為何需要離屏渲染

????一些復雜的效果,如:圓角,陰影,遮罩,圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制,無法直接渲染出結(jié)果,所以就需要屏幕外渲染被喚起。

????屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU還是GPU)。

????所以當使用離屏渲染的時候會很容易造成性能消耗,因為在OPENGL里離屏渲染會單獨在內(nèi)存中創(chuàng)建一個屏幕外緩沖區(qū)并進行渲染,而屏幕外緩沖區(qū)跟當前屏幕緩沖區(qū)上下文切換是很耗性能的。

觸發(fā)離屏渲染的操作
  • shouldRasterize(光柵化)
  • masks(遮罩)
  • shadows(陰影)
  • edge antialiasing(抗鋸齒)
  • group opacity(不透明)
  • 復雜形狀設(shè)置圓角等
光柵化:

概念:將圖轉(zhuǎn)化為一個個柵格組成的圖象。
特點:每個元素對應幀緩沖區(qū)中的一像素。
????shouldRasterize = YES在其他屬性觸發(fā)離屏渲染的同時,會將光柵化后的內(nèi)容緩存起來,如果對應的layer及其sublayers沒有發(fā)生改變,在下一幀的時候可以直接復用。shouldRasterize = YES,這將隱式的創(chuàng)建一個位圖,各種陰影遮罩等效果也會保存到位圖中并緩存起來,從而減少渲染的頻度(不是矢量圖)。相當于光柵化是把GPU的操作轉(zhuǎn)到CPU上了,生成位圖緩存,直接讀取復用。
????“Color Hits Green and Misses Red”可以檢查當前場景下光柵化操作,綠色表示緩存被復用,紅色表示緩存在被重復創(chuàng)建。
????如果光柵化的層變紅得太頻繁那么光柵化對優(yōu)化可能沒有多少用處。位圖緩存從內(nèi)存中刪除又重新創(chuàng)建得太過頻繁,紅色表明緩存重建得太遲。可以針對性的選擇某個較小而較深的層結(jié)構(gòu)進行光柵化,來嘗試減少渲染時間。

注意:
對于經(jīng)常變動的內(nèi)容,不要開啟光柵化,則會造成大量的離屏渲染,降低圖形性能。

圓角的設(shè)置

  1. 使用cornerRadius

    self.layer.cornerRadius = cornerRadius;    self.layer.masksToBounds = YES;    //防止子view邊界超過父view
    //self.clipsToBounds = YES;
    self.layer.shouldRasterize = YES;       //光柵化
    
    • UIView的clipsToBounds與CALayer的maskToBounds的作用一致,防止子view邊界超過父view
    • cornerRadius默認情況下只對背景色和border起作用
    • 如果最后設(shè)置了 shouldRasterize 為 YES,那也要記住設(shè)置 rasterizationScale 為 contentsScale
  1. 使用貝塞爾曲線 + maskLayer

    - (void)setRoundRect:(CGRect)frame cornerRadius:(CGFloat)cornerRadius {
        CGRect maskFrame = CGRectMake(0, 0, CGRectGetWidth(frame), CGRectGetHeight(frame));
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:maskFrame  cornerRadius:cornerRadius];
        CAShapeLayer *maskLayer = [[CAShapeLayer alloc] init];
        maskLayer.frame = maskFrame;
        maskLayer.path = path.CGPath;
        self.layer.mask = maskLayer;
    }
    

    UIBezierPath對CoreGraphics進行了一層封裝

  2. 使用CoreGraphics繪制圓角圖片做背景(CPU渲染)

    - (void)drawRoundCornerWithCornerRadius:(CGFloat)cornerRadius {
    
        CGFloat width = CGRectGetWidth(self.frame);
        CGFloat height = CGRectGetHeight(self.frame);
    
        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, width, height)];
        [self addSubview:imageView];
        dispatch_async(dispatch_queue_create("backgroundQueue", DISPATCH_QUEUE_CONCURRENT), ^{
        UIGraphicsBeginImageContextWithOptions(self.frame.size, NO, [UIScreen mainScreen].scale);
            CGContextRef context = UIGraphicsGetCurrentContext();
    
            CGContextMoveToPoint(context, 0, 0);
        
            CGContextAddArcToPoint(context, width, 0, width, height, cornerRadius);
            CGContextAddArcToPoint(context, width, height, 0, height, cornerRadius);
            CGContextAddArcToPoint(context, 0, height, 0, 0, cornerRadius);
            CGContextAddArcToPoint(context, 0, 0, width, 0, cornerRadius);
        
            CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
            CGContextDrawPath(context, kCGPathStroke);
        
            UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            dispatch_async(dispatch_get_main_queue(), ^{
            imageView.image = image;
            });
        });
    }
    

    CoreGraphic通常是線程安全的,所以可以進行異步繪制,顯示的時候再放回主線程

  3. 將圖片剪切為圓角(針對圖片)

    - (UIImage *)setRoundCornerRadius:(CGFloat)cornerRadius {
        UIImage *image = nil;
        CGRect imageFrame = CGRectMake(0, 0, self.size.width, self.size.height);
        UIGraphicsBeginImageContextWithOptions(self.size, NO, [UIScreen mainScreen].scale);
        UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:imageFrame cornerRadius:cornerRadius];
        [path addClip];
        [self drawInRect:imageFrame];
        image = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return image;
    }
    
  4. 使用圓角圖片作為蒙板

如何避免離屏渲染

  1. 圓角視圖較少,使用使用cornerRadius
  2. UIImageView 的圓角通過直接截取圖片實現(xiàn),其它視圖的圓角可以通過 Core Graphics 畫出圓角矩形實現(xiàn)。
  3. 對于圖形采用異步繪制
  4. 直接使用圓角素材作為背景

參考資料

iOS離屏渲染優(yōu)化
繪制像素到屏幕上
關(guān)于性能的一些問題(iOS)
解決常見的masksToBounds離屏渲染帶來的性能損耗
iOS 離屏渲染的研究
小心別讓圓角成了你列表的幀數(shù)殺手
iOS 高效添加圓角效果實戰(zhàn)講解
UIKit性能調(diào)優(yōu)實戰(zhàn)講解
iOS 保持界面流暢的技巧
iOS開發(fā):關(guān)于圖形渲染以及界面優(yōu)化的的一些想法
iOS圖形渲染分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容