從iOS的圖片圓角想到渲染

whimflash 7年前發布 | 14K 次閱讀 GPU iOS開發 移動開發

圓角是一種很常見的視圖效果,相比于直角,它更加柔和優美,易于接受。設置圓角會帶來一定的性能損耗,如何提高性能是一個需要重點討論的話題。

大家常見的圓角代碼 x.layer.cornerRadius = xx; x.clipsToBounds = YES; 這兩行確實實現了圓角視覺效果。其實使用 x.layer.cornerRadius = xx; 已經實現了圓角,只不過在某些控件是不生效的,因為某些圖層在被切割圓角圖層之上而被顯示出來了。而 x.clipsToBounds = YES; 帶來的后果就是產生 離屏渲染 。可以使用instruments中的CoreAnimation工具,打開 Color Offscren-Rednered Yellow 選項,可見黃色區域部分即是離屏渲染部分。

那么離屏渲染會帶來什么?當然后資源損耗,可能產生卡頓。因為在iPhone設備的硬件資源有差異,當離屏渲染不多時,并不是很明顯感覺到它的缺點。

什么是像素

像素,為視頻顯示的基本單位,譯自英文“pixel”,pix是英語單詞picture的常用簡寫,加上英語單詞“元素”element,就得到pixel,故“像素”表示“畫像元素”之意,有時亦被稱為pel(picture element)。每個這樣的消息元素不是一個點或者一個方塊,而是一個抽象的取樣。像素是由紅,綠,藍三種顏色組件構成的。因此,位圖數據有時也被叫做 RGB 數據。

顯示機制

一個像素是如何繪制到屏幕上去的?有很多種方式將一些東西映射到顯示屏上,他們需要調用不同的框架、許多功能和方法的結合體。這里我們大概看一下屏幕之后發生的事情。

圖像想顯示到屏幕上使人肉眼可見都需借助像素的力量。它們密集的排布在手機屏幕上,將任何圖形通過不同的色值表現出來。計算機顯示的流程大致可以描述為將圖像轉化為一系列像素點的排列然后打印在屏幕上,由圖像轉化為像素點的過程又可以稱之為光柵化,就是從矢量的點線面的描述,變成像素的描述。

回溯歷史,可以從過去的 CRT 顯示器原理說起。CRT 的電子槍按照上面方式,從上到下一行行掃描,掃描完成后顯示器就呈現一幀畫面,隨后電子槍回到初始位置繼續下一次掃描。為了把顯示器的顯示過程和系統的視頻控制器進行同步,顯示器(或者其他硬件)會用硬件時鐘產生一系列的定時信號。當電子槍換到新的一行,準備進行掃描時,顯示器會發出一個水平同步信號(horizonal synchronization),簡稱 HSync;而當一幀畫面繪制完成后,電子槍回復到原位,準備畫下一幀前,顯示器會發出一個垂直同步信號(vertical synchronization),簡稱 VSync。顯示器通常以固定頻率進行刷新,這個刷新率就是 VSync 信號產生的頻率。盡管現在的設備大都是液晶顯示屏了,但原理仍然沒有變。

關于卡頓的簡單原理解釋

在 VSync 信號到來后,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主線程開始在 CPU 中計算顯示內容,比如視圖的創建、布局計算、圖片解碼、文本繪制等。隨后 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨后 GPU 會把渲染結果提交到幀緩沖區去,等待下一次 VSync 信號到來時顯示到屏幕上。由于垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是界面卡頓的原因。

CPU 和 GPU 不論哪個阻礙了顯示流程,都會造成掉幀現象。所以開發時,也需要分別對 CPU 和 GPU 壓力進行評估和優化。

渲染機制

當像素映射到屏幕上的時候,后臺發生了很多事情。但一旦它們顯示到屏幕上,每一個像素均由三個顏色組件構成:紅,綠,藍。三個獨立的顏色單元會根據給定的顏色顯示到一個像素上。在 iPhoneSE 的顯示器上有1,136×640=727,040個像素,因此有2,181,120個顏色單元。在一些Retina屏幕上,這一數字將達到百萬以上。所有的圖形堆棧一起工作以確保每次正確的顯示。當你滾動整個屏幕的時候,數以百萬計的顏色單元必須以每秒60次的速度刷新,這就是一個很大的工作量。

簡單來說,iOS的顯示機制大致如此:

Display 的上一層便是圖形處理單元 GPU,GPU 是一個專門為圖形高并發計算而量身定做的處理單元。這也是為什么它能同時更新所有的像素,并呈現到顯示器上。它的并發本性讓它能高效的將不同紋理合成起來。所以,開發中我們應該盡量讓CPU負責主線程的UI調動,把圖形顯示相關的工作交給GPU來處理。

GPU Driver 是直接和 GPU 交流的代碼塊。不同的GPU是不同的性能怪獸,但是驅動使它們在下一個層級上顯示的更為統一,典型的下一層級有 OpenGL/OpenGL ES.

OpenGL(Open Graphics Library) 是一個提供了 2D 和 3D 圖形渲染的 API。GPU 是一塊非常特殊的硬件,OpenGL 和 GPU 密切的工作以提高GPU的能力,并實現硬件加速渲染。

OpenGL 之上擴展出很多東西。在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,然而在 OS X 上,繞過 Core Animation 直接使用 Core Graphics 繪制的情況并不少見。對于一些專門的應用,尤其是游戲,程序可能直接和 OpenGL/OpenGL ES 交流。

需要強調的是,GPU 是一個非常強大的圖形硬件,并且在顯示像素方面起著核心作用。它連接到 CPU。從硬件上講兩者之間存在某種類型的總線,并且有像 OpenGL,Core Animation 和 Core Graphics 這樣的框架來在 GPU 和 CPU 之間精心安排數據的傳輸。為了將像素顯示到屏幕上,一些處理將在 CPU 上進行。然后數據將會傳送到 GPU,最終像素顯示到屏幕上。

正如上圖顯示,GPU 需要將每一個 frame 的紋理(位圖)合成在一起(一秒60次)。每一個紋理會占用 VRAM(video RAM),所以需要給 GPU 同時保持紋理的數量做一個限制。GPU 在合成方面非常高效,但是某些合成任務卻比其他更復雜,并且 GPU在 16.7ms(1/60s)內能做的工作也是有限的。

另外一個問題就是將數據傳輸到 GPU 上。為了讓 GPU 訪問數據,需要將數據從 RAM 移動到 VRAM 上。這就是提及到的上傳數據到 GPU。這些看起來貌似微不足道,但是一些大型的紋理卻會非常耗時。

最終,CPU 開始運行程序。你可能會讓 CPU 從 bundle 加載一張 PNG 的圖片并且解壓它。這所有的事情都在 CPU 上進行。然后當你需要顯示解壓縮后的圖片時,它需要以某種方式上傳到 GPU。一些看似平凡的,比如顯示文本,對 CPU 來說卻是一件非常復雜的事情,這會促使 Core Text 和 Core Graphics 框架更緊密的集成來根據文本生成一個位圖。一旦準備好,它將會被作為一個紋理上傳到 GPU 并準備顯示出來。當你滾動或者在屏幕上移動文本時,同樣的紋理能夠被復用,CPU 只需簡單的告訴 GPU 新的位置就行了,所以 GPU 就可以重用存在的紋理了。CPU 并不需要重新渲染文本,并且位圖也不需要重新上傳到 GPU。

在圖形世界中,合成是一個描述不同位圖如何放到一起來創建你最終在屏幕上看到圖像的過程。屏幕上一切事物皆紋理。一個紋理就是一個包含 RGBA 值的長方形,比如,每一個像素里面都包含紅、綠、藍和透明度的值。在 Core Animation 世界中這就相當于一個 CALayer。

每一個 layer 是一個紋理,所有的紋理都以某種方式堆疊在彼此的頂部。對于屏幕上的每一個像素,GPU 需要算出怎么混合這些紋理來得到像素 RGB 的值。這就是合成。

如果我們所擁有的是一個和屏幕大小一樣并且和屏幕像素對齊的單一紋理,那么屏幕上每一個像素相當于紋理中的一個像素,紋理的最后一個像素也就是屏幕的最后一個像素。

如果我們有第二個紋理放在第一個紋理之上,然后GPU將會把第二個紋理合成到第一個紋理中。有很多種不同的合成方法,但是如果我們假定兩個紋理的像素對齊,并且使用正常的混合模式,我們便可以用公式來計算每一個像素: R = S + D * ( 1 – Sa )
結果的顏色是源色彩(頂端紋理)+目標顏色(低一層的紋理)*(1-源顏色的透明度)。在這個公式中所有的顏色都假定已經預先乘以了它們的透明度。

接著我們進行第二個假定,兩個紋理都完全不透明,比如 alpha=1。如果目標紋理(低一層的紋理)是藍色(RGB=0,0,1),并且源紋理(頂層的紋理)顏色是紅色(RGB=1,0,0),因為 Sa 為1,所以結果為: R = S
結果是源顏色的紅色。這正是我們所期待的(紅色覆蓋了藍色)。如果源顏色層為50%的透明,比如 alpha=0.5,既然 alpha 組成部分需要預先乘進 RGB 的值中,那么 S 的 RGB 值為(0.5, 0, 0),公式看起來便會像這樣:

                       0.5   0               0.5
R = S + D * (1 - Sa) = 0   + 0 * (1 - 0.5) = 0
                       0     1               0.5

我們最終得到RGB值為(0.5, 0, 0.5),是一個紫色。這正是我們所期望將透明紅色合成到藍色背景上所得到的。

記住我們剛剛只是將紋理中的一個像素合成到另一個紋理的像素上。當兩個紋理覆蓋在一起的時候,GPU需要為所有像素做這種操作。正如你所知道的一樣,許多程序都有很多層,因此所有的紋理都需要合成到一起。盡管GPU是一塊高度優化的硬件來做這種事情,但這還是會讓它非常忙碌。

為何圖片縮放會增加GPU工作量

當所有的像素是對齊的時候我們得到相對簡單的計算公式。每當 GPU 需要計算出屏幕上一個像素是什么顏色的時候,它只需要考慮在這個像素之上的所有 layer 中對應的單個像素,并把這些像素合并到一起。或者,如果最頂層的紋理是不透明的(即圖層樹的最底層),這時候 GPU 就可以簡單的拷貝它的像素到屏幕上。

當一個 layer 上所有的像素和屏幕上的像素完美的對應整齊,那這個 layer 就是像素對齊的。主要有兩個原因可能會造成不對齊。第一個便是滾動,當一個紋理上下滾動的時候,紋理的像素便不會和屏幕的像素排列對齊。另一個原因便是當紋理的起點不在一個像素的邊界上。

在這兩種情況下,GPU 需要再做額外的計算。它需要將源紋理上多個像素混合起來,生成一個用來合成的值。當所有的像素都是對齊的時候,GPU 只剩下很少的工作要做。

Core Animation 工具和模擬器有一個 Color Misaligned Images 選項,當這些在你的 CALayer 實例中發生的時候,這個功能便可向你展示。

關于iOS設備的一些尺寸限制可以看這里: iOSRes

離屏渲染

On-Screen Rendering意為當前屏幕渲染,指的是GPU的渲染操作是在當前用于顯示的屏幕緩沖區中進行。

Off-Screen Rendering意為離屏渲染,指的是GPU在當前屏幕緩沖區以外新開辟一個緩沖區進行渲染操作。

當圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制時,屏幕外渲染就被喚起了。屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU還是GPU)。

離屏渲染可以被 Core Animation 自動觸發,或者被應用程序強制觸發。屏幕外的渲染會合并/渲染圖層樹的一部分到一個新的緩沖區,然后該緩沖區被渲染到屏幕上。

特殊的“離屏渲染”:CPU渲染

如果我們重寫了drawRect方法,并且使用任何Core Graphics的技術進行了繪制操作,就涉及到了CPU渲染。

整個渲染過程由CPU在App內同步地完成,渲染得到的bitmap最后再交由GPU用于顯示。

離屏渲染的體現

相比于當前屏幕渲染,離屏渲染的代價是很高的,主要體現在兩個方面:

  • 1 創建新緩沖區
    要想進行離屏渲染,首先要創建一個新的緩沖區。
  • 2 上下文切換
    離屏渲染的整個過程,需要多次切換上下文環境:先是從當前屏幕(On-Screen)切換到離屏(Off-Screen);等到離屏渲染結束以后,將離屏緩沖區的渲染結果顯示到屏幕上,又需要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。

觸發離屏渲染

1、drawRect

2、layer.shouldRasterize = true;

3、有mask或者是陰影(layer.masksToBounds, layer.shadow*);

3.1) shouldRasterize(光柵化)

3.2) masks(遮罩)

3.3) shadows(陰影)

3.4) edge antialiasing(抗鋸齒)

3.5) group opacity(不透明)

4、Text(UILabel, CATextLayer, Core Text, etc)…

注:layer.cornerRadius,layer.borderWidth,layer.borderColor并不會Offscreen Render,因為這些不需要加入Mask。

圓角優化

前面說了那么多,這里就給上實際可行方案。圓角的優化目前考慮兩方面:一是,從圖片入手,將圖片切割成指定圓角樣式。二是,使用貝塞爾曲線,利用CALayer層繪制指定圓角樣式的mask遮蓋View。

UIImage切割:

UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);

CGFloat minSize = MIN(self.size.width, self.size.height);
if (borderWidth < minSize / 2.0) {
    UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
    CGContextSaveGState(context);
    [path addClip];
    CGContextDrawImage(context, rect, self.CGImage);
    CGContextRestoreGState(context);
}

UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
image = [image dd_imageByCornerRadius:radius borderedColor:borderColor borderWidth:borderWidth corners:corners];
UIGraphicsEndImageContext();

圖片繪制:

UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
[self drawAtPoint:CGPointZero];
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGFloat strokeInset = borderWidth / 2.0;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
path.lineWidth = borderWidth;
[borderColor setStroke];
[path stroke];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();

具體源碼可以轉至github進行star DDCornerRadius 歡迎issue。

 

來自:http://chars.tech/2017/07/03/ios-corner-radius/

 

 本文由用戶 whimflash 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!