從iOS的圖片圓角想到渲染

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

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

那麼離屏渲染會帶來什麼?固然後資源損耗,可能產生卡頓。由於在iPhone設備的硬件資源有差別,當離屏渲染很少時,並非很明顯感受到它的缺點。github

什麼是像素

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

顯示機制

一個像素是如何繪製到屏幕上去的?有不少種方式將一些東西映射到顯示屏上,他們須要調用不一樣的框架、許多功能和方法的結合體。這裏咱們大概看一下屏幕以後發生的事情。框架

圖像想顯示到屏幕上令人肉眼可見都需藉助像素的力量。它們密集的排布在手機屏幕上,將任何圖形經過不一樣的色值表現出來。計算機顯示的流程大體能夠描述爲將圖像轉化爲一系列像素點的排列而後打印在屏幕上,由圖像轉化爲像素點的過程又能夠稱之爲光柵化,就是從矢量的點線面的描述,變成像素的描述。ide

display screen

回溯歷史,能夠從過去的 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的顯示機制大體如此:
pixels software stack

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,最終像素顯示到屏幕上。

pixels hardware

正如上圖顯示,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);等到離屏渲染結束之後,將離屏緩衝區的渲染結果顯示到屏幕上,又須要將上下文環境從離屏切換到當前屏幕。而上下文環境的切換是要付出很大代價的。

觸發離屏渲染

一、drawRect
二、layer.shouldRasterize = true;
三、有mask或者是陰影(layer.masksToBounds, layer.shadow*);
3.1) shouldRasterize(光柵化)
3.2) masks(遮罩)
3.3) shadows(陰影)
3.4) edge antialiasing(抗鋸齒)
3.5) group opacity(不透明)
四、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。

相關文章
相關標籤/搜索