圓角是一種很常見(jiàn)的視圖效果属提,相比于直角,它更加柔和優(yōu)美病往,易于接受捣染。設(shè)置圓角會(huì)帶來(lái)一定的性能損耗,如何提高性能是一個(gè)需要重點(diǎn)討論的話題停巷。
大家常見(jiàn)的圓角代碼x.layer.cornerRadius = xx; x.clipsToBounds = YES;
這兩行確實(shí)實(shí)現(xiàn)了圓角視覺(jué)效果耍攘。其實(shí)使用x.layer.cornerRadius = xx;
已經(jīng)實(shí)現(xiàn)了圓角,只不過(guò)在某些控件是不生效的畔勤,因?yàn)槟承﹫D層在被切割圓角圖層之上而被顯示出來(lái)了蕾各。而x.clipsToBounds = YES;
帶來(lái)的后果就是產(chǎn)生離屏渲染
∏炀荆可以使用instruments中的CoreAnimation工具式曲,打開(kāi)Color Offscren-Rednered Yellow
選項(xiàng),可見(jiàn)黃色區(qū)域部分即是離屏渲染部分缸榛。
那么離屏渲染會(huì)帶來(lái)什么吝羞?當(dāng)然后資源損耗,可能產(chǎn)生卡頓内颗。因?yàn)樵趇Phone設(shè)備的硬件資源有差異钧排,當(dāng)離屏渲染不多時(shí),并不是很明顯感覺(jué)到它的缺點(diǎn)均澳。
文章中說(shuō)到的具體源碼可以轉(zhuǎn)至github進(jìn)行star DDCornerRadius 歡迎issue卖氨。
什么是像素
像素会烙,為視頻顯示的基本單位,譯自英文“pixel”筒捺,pix是英語(yǔ)單詞picture的常用簡(jiǎn)寫(xiě)柏腻,加上英語(yǔ)單詞“元素”element,就得到pixel系吭,故“像素”表示“畫(huà)像元素”之意五嫂,有時(shí)亦被稱(chēng)為pel(picture element)。每個(gè)這樣的消息元素不是一個(gè)點(diǎn)或者一個(gè)方塊肯尺,而是一個(gè)抽象的取樣沃缘。像素是由紅,綠则吟,藍(lán)三種顏色組件構(gòu)成的槐臀。因此,位圖數(shù)據(jù)有時(shí)也被叫做 RGB 數(shù)據(jù)氓仲。
顯示機(jī)制
一個(gè)像素是如何繪制到屏幕上去的水慨?有很多種方式將一些東西映射到顯示屏上,他們需要調(diào)用不同的框架敬扛、許多功能和方法的結(jié)合體晰洒。這里我們大概看一下屏幕之后發(fā)生的事情。
圖像想顯示到屏幕上使人肉眼可見(jiàn)都需借助像素的力量啥箭。它們密集的排布在手機(jī)屏幕上谍珊,將任何圖形通過(guò)不同的色值表現(xiàn)出來(lái)。計(jì)算機(jī)顯示的流程大致可以描述為將圖像轉(zhuǎn)化為一系列像素點(diǎn)的排列然后打印在屏幕上急侥,由圖像轉(zhuǎn)化為像素點(diǎn)的過(guò)程又可以稱(chēng)之為光柵化砌滞,就是從矢量的點(diǎn)線面的描述,變成像素的描述坏怪。
回溯歷史布持,可以從過(guò)去的 CRT 顯示器原理說(shuō)起。CRT 的電子槍按照上面方式陕悬,從上到下一行行掃描题暖,掃描完成后顯示器就呈現(xiàn)一幀畫(huà)面,隨后電子槍回到初始位置繼續(xù)下一次掃描捉超。為了把顯示器的顯示過(guò)程和系統(tǒng)的視頻控制器進(jìn)行同步胧卤,顯示器(或者其他硬件)會(huì)用硬件時(shí)鐘產(chǎn)生一系列的定時(shí)信號(hào)。當(dāng)電子槍換到新的一行拼岳,準(zhǔn)備進(jìn)行掃描時(shí)枝誊,顯示器會(huì)發(fā)出一個(gè)水平同步信號(hào)(horizonal synchronization),簡(jiǎn)稱(chēng) HSync惜纸;而當(dāng)一幀畫(huà)面繪制完成后叶撒,電子槍回復(fù)到原位绝骚,準(zhǔn)備畫(huà)下一幀前,顯示器會(huì)發(fā)出一個(gè)垂直同步信號(hào)(vertical synchronization)祠够,簡(jiǎn)稱(chēng) VSync压汪。顯示器通常以固定頻率進(jìn)行刷新,這個(gè)刷新率就是 VSync 信號(hào)產(chǎn)生的頻率古瓤。盡管現(xiàn)在的設(shè)備大都是液晶顯示屏了止剖,但原理仍然沒(méi)有變。
關(guān)于卡頓的簡(jiǎn)單原理解釋
在 VSync 信號(hào)到來(lái)后落君,系統(tǒng)圖形服務(wù)會(huì)通過(guò) CADisplayLink 等機(jī)制通知 App穿香,App 主線程開(kāi)始在 CPU 中計(jì)算顯示內(nèi)容,比如視圖的創(chuàng)建绎速、布局計(jì)算皮获、圖片解碼、文本繪制等纹冤。隨后 CPU 會(huì)將計(jì)算好的內(nèi)容提交到 GPU 去洒宝,由 GPU 進(jìn)行變換、合成赵哲、渲染。隨后 GPU 會(huì)把渲染結(jié)果提交到幀緩沖區(qū)去君丁,等待下一次 VSync 信號(hào)到來(lái)時(shí)顯示到屏幕上枫夺。由于垂直同步的機(jī)制,如果在一個(gè) VSync 時(shí)間內(nèi)绘闷,CPU 或者 GPU 沒(méi)有完成內(nèi)容提交橡庞,則那一幀就會(huì)被丟棄,等待下一次機(jī)會(huì)再顯示印蔗,而這時(shí)顯示屏?xí)A糁暗膬?nèi)容不變扒最。這就是界面卡頓的原因。
CPU 和 GPU 不論哪個(gè)阻礙了顯示流程华嘹,都會(huì)造成掉幀現(xiàn)象吧趣。所以開(kāi)發(fā)時(shí),也需要分別對(duì) CPU 和 GPU 壓力進(jìn)行評(píng)估和優(yōu)化耙厚。
渲染機(jī)制
當(dāng)像素映射到屏幕上的時(shí)候强挫,后臺(tái)發(fā)生了很多事情。但一旦它們顯示到屏幕上薛躬,每一個(gè)像素均由三個(gè)顏色組件構(gòu)成:紅俯渤,綠,藍(lán)型宝。三個(gè)獨(dú)立的顏色單元會(huì)根據(jù)給定的顏色顯示到一個(gè)像素上八匠。在 iPhoneSE 的顯示器上有1,136×640=727,040個(gè)像素絮爷,因此有2,181,120個(gè)顏色單元。在一些Retina屏幕上梨树,這一數(shù)字將達(dá)到百萬(wàn)以上坑夯。所有的圖形堆棧一起工作以確保每次正確的顯示。當(dāng)你滾動(dòng)整個(gè)屏幕的時(shí)候劝萤,數(shù)以百萬(wàn)計(jì)的顏色單元必須以每秒60次的速度刷新渊涝,這就是一個(gè)很大的工作量。
簡(jiǎn)單來(lái)說(shuō)床嫌,iOS的顯示機(jī)制大致如此:
Display 的上一層便是圖形處理單元 GPU跨释,GPU 是一個(gè)專(zhuān)門(mén)為圖形高并發(fā)計(jì)算而量身定做的處理單元。這也是為什么它能同時(shí)更新所有的像素厌处,并呈現(xiàn)到顯示器上鳖谈。它的并發(fā)本性讓它能高效的將不同紋理合成起來(lái)。所以阔涉,開(kāi)發(fā)中我們應(yīng)該盡量讓CPU負(fù)責(zé)主線程的UI調(diào)動(dòng)缆娃,把圖形顯示相關(guān)的工作交給GPU來(lái)處理。
GPU Driver 是直接和 GPU 交流的代碼塊瑰排。不同的GPU是不同的性能怪獸贯要,但是驅(qū)動(dòng)使它們?cè)谙乱粋€(gè)層級(jí)上顯示的更為統(tǒng)一,典型的下一層級(jí)有 OpenGL/OpenGL ES.
OpenGL(Open Graphics Library) 是一個(gè)提供了 2D 和 3D 圖形渲染的 API椭住。GPU 是一塊非常特殊的硬件崇渗,OpenGL 和 GPU 密切的工作以提高GPU的能力,并實(shí)現(xiàn)硬件加速渲染京郑。
OpenGL 之上擴(kuò)展出很多東西宅广。在 iOS 上,幾乎所有的東西都是通過(guò) Core Animation 繪制出來(lái)些举,然而在 OS X 上跟狱,繞過(guò) Core Animation 直接使用 Core Graphics 繪制的情況并不少見(jiàn)。對(duì)于一些專(zhuān)門(mén)的應(yīng)用户魏,尤其是游戲驶臊,程序可能直接和 OpenGL/OpenGL ES 交流。
需要強(qiáng)調(diào)的是叼丑,GPU 是一個(gè)非常強(qiáng)大的圖形硬件资铡,并且在顯示像素方面起著核心作用。它連接到 CPU幢码。從硬件上講兩者之間存在某種類(lèi)型的總線笤休,并且有像 OpenGL,Core Animation 和 Core Graphics 這樣的框架來(lái)在 GPU 和 CPU 之間精心安排數(shù)據(jù)的傳輸症副。為了將像素顯示到屏幕上店雅,一些處理將在 CPU 上進(jìn)行政基。然后數(shù)據(jù)將會(huì)傳送到 GPU,最終像素顯示到屏幕上闹啦。
正如上圖顯示沮明,GPU 需要將每一個(gè) frame 的紋理(位圖)合成在一起(一秒60次)。每一個(gè)紋理會(huì)占用 VRAM(video RAM)窍奋,所以需要給 GPU 同時(shí)保持紋理的數(shù)量做一個(gè)限制荐健。GPU 在合成方面非常高效,但是某些合成任務(wù)卻比其他更復(fù)雜琳袄,并且 GPU在 16.7ms(1/60s)內(nèi)能做的工作也是有限的江场。
另外一個(gè)問(wèn)題就是將數(shù)據(jù)傳輸?shù)?GPU 上。為了讓 GPU 訪問(wèn)數(shù)據(jù)窖逗,需要將數(shù)據(jù)從 RAM 移動(dòng)到 VRAM 上址否。這就是提及到的上傳數(shù)據(jù)到 GPU。這些看起來(lái)貌似微不足道碎紊,但是一些大型的紋理卻會(huì)非常耗時(shí)佑附。
最終,CPU 開(kāi)始運(yùn)行程序仗考。你可能會(huì)讓 CPU 從 bundle 加載一張 PNG 的圖片并且解壓它音同。這所有的事情都在 CPU 上進(jìn)行。然后當(dāng)你需要顯示解壓縮后的圖片時(shí)秃嗜,它需要以某種方式上傳到 GPU权均。一些看似平凡的,比如顯示文本痪寻,對(duì) CPU 來(lái)說(shuō)卻是一件非常復(fù)雜的事情螺句,這會(huì)促使 Core Text 和 Core Graphics 框架更緊密的集成來(lái)根據(jù)文本生成一個(gè)位圖虽惭。一旦準(zhǔn)備好橡类,它將會(huì)被作為一個(gè)紋理上傳到 GPU 并準(zhǔn)備顯示出來(lái)。當(dāng)你滾動(dòng)或者在屏幕上移動(dòng)文本時(shí)芽唇,同樣的紋理能夠被復(fù)用顾画,CPU 只需簡(jiǎn)單的告訴 GPU 新的位置就行了,所以 GPU 就可以重用存在的紋理了。CPU 并不需要重新渲染文本匆笤,并且位圖也不需要重新上傳到 GPU研侣。
在圖形世界中,合成是一個(gè)描述不同位圖如何放到一起來(lái)創(chuàng)建你最終在屏幕上看到圖像的過(guò)程炮捧。屏幕上一切事物皆紋理庶诡。一個(gè)紋理就是一個(gè)包含 RGBA 值的長(zhǎng)方形,比如咆课,每一個(gè)像素里面都包含紅末誓、綠扯俱、藍(lán)和透明度的值。在 Core Animation 世界中這就相當(dāng)于一個(gè) CALayer喇澡。
每一個(gè) layer 是一個(gè)紋理迅栅,所有的紋理都以某種方式堆疊在彼此的頂部。對(duì)于屏幕上的每一個(gè)像素晴玖,GPU 需要算出怎么混合這些紋理來(lái)得到像素 RGB 的值读存。這就是合成。
如果我們所擁有的是一個(gè)和屏幕大小一樣并且和屏幕像素對(duì)齊的單一紋理呕屎,那么屏幕上每一個(gè)像素相當(dāng)于紋理中的一個(gè)像素让簿,紋理的最后一個(gè)像素也就是屏幕的最后一個(gè)像素。
如果我們有第二個(gè)紋理放在第一個(gè)紋理之上榨惰,然后GPU將會(huì)把第二個(gè)紋理合成到第一個(gè)紋理中拜英。有很多種不同的合成方法,但是如果我們假定兩個(gè)紋理的像素對(duì)齊琅催,并且使用正常的混合模式居凶,我們便可以用公式來(lái)計(jì)算每一個(gè)像素:R = S + D * ( 1 – Sa )
結(jié)果的顏色是源色彩(頂端紋理)+目標(biāo)顏色(低一層的紋理)*(1-源顏色的透明度)。在這個(gè)公式中所有的顏色都假定已經(jīng)預(yù)先乘以了它們的透明度藤抡。
接著我們進(jìn)行第二個(gè)假定侠碧,兩個(gè)紋理都完全不透明,比如 alpha=1缠黍。如果目標(biāo)紋理(低一層的紋理)是藍(lán)色(RGB=0,0,1)弄兜,并且源紋理(頂層的紋理)顏色是紅色(RGB=1,0,0),因?yàn)?Sa 為1瓷式,所以結(jié)果為:R = S
結(jié)果是源顏色的紅色替饿。這正是我們所期待的(紅色覆蓋了藍(lán)色)。如果源顏色層為50%的透明贸典,比如 alpha=0.5视卢,既然 alpha 組成部分需要預(yù)先乘進(jìn) RGB 的值中,那么 S 的 RGB 值為(0.5, 0, 0)廊驼,公式看起來(lái)便會(huì)像這樣:
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),是一個(gè)紫色据过。這正是我們所期望將透明紅色合成到藍(lán)色背景上所得到的。
記住我們剛剛只是將紋理中的一個(gè)像素合成到另一個(gè)紋理的像素上妒挎。當(dāng)兩個(gè)紋理覆蓋在一起的時(shí)候绳锅,GPU需要為所有像素做這種操作。正如你所知道的一樣酝掩,許多程序都有很多層很魂,因此所有的紋理都需要合成到一起致开。盡管GPU是一塊高度優(yōu)化的硬件來(lái)做這種事情丹弱,但這還是會(huì)讓它非常忙碌桥胞。
為何圖片縮放會(huì)增加GPU工作量
當(dāng)所有的像素是對(duì)齊的時(shí)候我們得到相對(duì)簡(jiǎn)單的計(jì)算公式。每當(dāng) GPU 需要計(jì)算出屏幕上一個(gè)像素是什么顏色的時(shí)候,它只需要考慮在這個(gè)像素之上的所有 layer 中對(duì)應(yīng)的單個(gè)像素,并把這些像素合并到一起≈胬或者,如果最頂層的紋理是不透明的(即圖層樹(shù)的最底層)丙笋,這時(shí)候 GPU 就可以簡(jiǎn)單的拷貝它的像素到屏幕上谢澈。
當(dāng)一個(gè) layer 上所有的像素和屏幕上的像素完美的對(duì)應(yīng)整齊,那這個(gè) layer 就是像素對(duì)齊的御板。主要有兩個(gè)原因可能會(huì)造成不對(duì)齊锥忿。第一個(gè)便是滾動(dòng),當(dāng)一個(gè)紋理上下滾動(dòng)的時(shí)候怠肋,紋理的像素便不會(huì)和屏幕的像素排列對(duì)齊敬鬓。另一個(gè)原因便是當(dāng)紋理的起點(diǎn)不在一個(gè)像素的邊界上。
在這兩種情況下笙各,GPU 需要再做額外的計(jì)算钉答。它需要將源紋理上多個(gè)像素混合起來(lái),生成一個(gè)用來(lái)合成的值杈抢。當(dāng)所有的像素都是對(duì)齊的時(shí)候数尿,GPU 只剩下很少的工作要做。
Core Animation 工具和模擬器有一個(gè)Color Misaligned Images
選項(xiàng)惶楼,當(dāng)這些在你的 CALayer 實(shí)例中發(fā)生的時(shí)候右蹦,這個(gè)功能便可向你展示。
關(guān)于iOS設(shè)備的一些尺寸限制可以看這里:iOSRes
離屏渲染
On-Screen Rendering意為當(dāng)前屏幕渲染歼捐,指的是GPU的渲染操作是在當(dāng)前用于顯示的屏幕緩沖區(qū)中進(jìn)行何陆。
Off-Screen Rendering意為離屏渲染,指的是GPU在當(dāng)前屏幕緩沖區(qū)以外新開(kāi)辟一個(gè)緩沖區(qū)進(jìn)行渲染操作豹储。
當(dāng)圖層屬性的混合體被指定為在未預(yù)合成之前不能直接在屏幕中繪制時(shí)贷盲,屏幕外渲染就被喚起了。屏幕外渲染并不意味著軟件繪制颂翼,但是它意味著圖層必須在被顯示之前在一個(gè)屏幕外上下文中被渲染(不論CPU還是GPU)晃洒。
離屏渲染可以被 Core Animation 自動(dòng)觸發(fā)慨灭,或者被應(yīng)用程序強(qiáng)制觸發(fā)朦乏。屏幕外的渲染會(huì)合并/渲染圖層樹(shù)的一部分到一個(gè)新的緩沖區(qū),然后該緩沖區(qū)被渲染到屏幕上氧骤。
特殊的“離屏渲染”:CPU渲染
如果我們重寫(xiě)了drawRect方法呻疹,并且使用任何Core Graphics的技術(shù)進(jìn)行了繪制操作,就涉及到了CPU渲染筹陵。
整個(gè)渲染過(guò)程由CPU在App內(nèi)同步地完成刽锤,渲染得到的bitmap最后再交由GPU用于顯示镊尺。
離屏渲染的體現(xiàn)
相比于當(dāng)前屏幕渲染,離屏渲染的代價(jià)是很高的并思,主要體現(xiàn)在兩個(gè)方面:
- 1 創(chuàng)建新緩沖區(qū)
要想進(jìn)行離屏渲染庐氮,首先要?jiǎng)?chuàng)建一個(gè)新的緩沖區(qū)。 - 2 上下文切換
離屏渲染的整個(gè)過(guò)程宋彼,需要多次切換上下文環(huán)境:先是從當(dāng)前屏幕(On-Screen)切換到離屏(Off-Screen)弄砍;等到離屏渲染結(jié)束以后,將離屏緩沖區(qū)的渲染結(jié)果顯示到屏幕上输涕,又需要將上下文環(huán)境從離屏切換到當(dāng)前屏幕音婶。而上下文環(huán)境的切換是要付出很大代價(jià)的。
觸發(fā)離屏渲染
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并不會(huì)Offscreen Render螟深,因?yàn)檫@些不需要加入Mask。
圓角優(yōu)化
前面說(shuō)了那么多烫葬,這里就給上實(shí)際可行方案界弧。圓角的優(yōu)化目前考慮兩方面:一是,從圖片入手搭综,將圖片切割成指定圓角樣式垢箕。二是,使用貝塞爾曲線兑巾,利用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();