將像素繪製到屏幕上去

轉自:http://answerhuang.duapp.com/index.php/2013/09/04/pixels-get-onto-the-screen/

原文鏈接:http://www.objc.io/issue-3/moving-pixels-onto-the-screen.html#pixels


 一個像素是怎樣繪製到屏幕上去的?有多種方式將一些東西映射到顯示屏上,他們需要調用不同的框架、許多功能和方法的結合體。這裏我們走馬觀花的看一下屏幕之後發生的一些事情。當你想要弄清楚什麼時候、怎麼去查明並解決問題時,我希望這篇文章能幫助你理解哪一個API將能更好的幫你解決問題。我們將聚焦於iOS,然而我討論的大多數問題也同樣適用於OS X。

圖形堆棧

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

軟件組成

從簡單的角度來看,軟件堆棧看起來有點像這樣:

1

Display的上一層便是圖形處理單元GPU,GPU是一個專門爲圖形高迸發計算而量身定做的處理單元。這也是爲什麼它能同時更新所有的像素,並呈現到顯示器上。它迸發的本性讓它能高效的將不同紋理合成起來。我們將有一小塊內容來更詳細的討論圖形合成。關鍵的是,GPU是非常專業的,因此在某些工作上非常高效。比如,GPU非常快,並且比CPU使用更少的電來完成工作。通常CPU都有一個普遍的目的,它可以做很多不同的事情,但是合成圖像在CPU上卻顯得比較慢。

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

OpenGL(Open Graphics Library)是一個提供了2D和3D圖形渲染的API。GPU是一塊非常特殊的硬件,OpenGL和GPU密切的工作以提高GPU的能力,並實現硬件加速渲染。對大多數人來說,OpenGL看起來非常底層,但是當它在1992年第一次發佈的時候(20多年前的事了)是第一個和圖形硬件(GPU)交流的標準化方式,這是一個重大的飛躍,程序員不再需要爲每個GPU重寫他們的應用了。

OpenGL之上擴展出很多東西。在iOS上,幾乎所有的東西都是通過Core Animation繪製出來,然而在OS X上,繞過Core Animation直接使用Core Graphics繪製的這種情況並不少見。對於一些專門的應用,尤其是遊戲,程序可能直接和OpenGL/OpenGL ES交流。事情變得使人更加困惑,因爲Core Animation使用Core Graphics來做一些渲染。像AVFoundation,Core Image框架,和其他一些混合的入口。

要記住一件事情,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),公式看起來便會像這樣:

3

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

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

不透明 VS 透明

當源紋理是完全不透明的時候,目標像素就等於源紋理。這可以省下GPU很大的工作量,這樣只需簡單的拷貝源紋理而不需要合成所有的像素值。但是沒有方法能告訴GPU紋理上的像素是透明還是不透明的。只有當你作爲一名開發者知道你放什麼到CALayer上了。這也是爲什麼CALayer有一個叫做opaque的屬性了。如果這個屬性爲YES,GPU將不會做任何合成,而是簡單從這個層拷貝,不需要考慮它下方的任何東西(因爲都被它遮擋住了)。這節省了GPU相當大的工作量。這也正是Instruments中color blended layers選項中所涉及的。(這在模擬器中的Debug菜單中也可用).它允許你看到哪一個layers(紋理)被標註爲透明的,比如GPU正在爲哪一個layers做合成。合成不透明的layers因爲需要更少的數學計算而更廉價。

所以如果你知道你的layer是不透明的,最好確定設置它的opaque爲YES。如果你加載一個沒有alpha通道的圖片,並且將它顯示在UIImageView上,這將會自動發生。但是要記住如果一個圖片沒有alpha通道和一個圖片每個地方的alpha都是100%,這將會產生很大的不同。在後一種情況下,Core Animation需要假定是否存在像素的alpha值不爲100%。在Finder中,你可以使用Get Info並且檢查More Info部分。它將告訴你這張圖片是否擁有alpha通道。

像素對齊 VS 不重合在一起

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

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

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

再次,Core Animation工具和模擬器有一個叫做color misaligned images的選項,當這些在你的CALayer實例中發生的時候,這個功能便可向你展示。

Masks

一個圖層可以有一個和它相關聯的mask(蒙板),mask是一個擁有alpha值的位圖,當像素要和它下面包含的像素合併之前都會把mask應用到圖層的像素上去。當你要設置一個圖層的圓角半徑時,你可以有效的在圖層上面設置一個mask。但是也可以指定任意一個蒙板。比如,一個字母A形狀的mask。最終只有在mask中顯示出來的(即圖層中的部分)纔會被渲染出來。

離屏渲染

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

離屏渲染合成計算是非常昂貴的, 但有時你也許希望強制這種操作。一種好的方法就是緩存合成的紋理/圖層。如果你的渲染樹非常複雜(所有的紋理,以及如何組合在一起),你可以強制離屏渲染緩存那些圖層,然後可以用緩存作爲合成的結果放到屏幕上。

如果你的程序混合了很多圖層,並且想要他們一起做動畫,GPU通常會爲每一幀(1/60s)重複合成所有的圖層。當使用離屏渲染時,GPU第一次會混合所有圖層到一個基於新的紋理的位圖緩存上,然後使用這個紋理來繪製到屏幕上。現在,當這些圖層一起移動的時候,GPU便可以複用這個位圖緩存,並且只需要做很少的工作。需要注意的是,只有當那些圖層不改變時,這纔可以用。如果那些圖層改變了,GPU需要重新創建位圖緩存。你可以通過設置shouldRasterize爲YES來觸發這個行爲。

然而,這是一個權衡。第一,這可能會使事情變得更慢。創建額外的屏幕外緩衝區是GPU需要多做的一步操作,特殊情況下這個位圖可能再也不需要被複用,這便是一個無用功了。然而,可以被複用的位圖,GPU也有可能將它卸載了。所以你需要計算GPU的利用率和幀的速率來判斷這個位圖是否有用。

離屏渲染也可能產生副作用。如果你正在直接或者間接的將mask應用到一個圖層上,Core Animation爲了應用這個mask,會強制進行屏幕外渲染。這會對GPU產生重負。通常情況下mask只能被直接渲染到幀的緩衝區中(在屏幕內)。

Instrument的Core Animation工具有一個叫做Color Offscreen-Rendered Yellow的選項,它會將已經被渲染到屏幕外緩衝區的區域標註爲黃色(這個選項在模擬器中也可以用)。同時記得檢查Color Hits Green and Misses Red選項。綠色代表無論何時一個屏幕外緩衝區被複用,而紅色代表當緩衝區被重新創建。

一般情況下,你需要避免離屏渲染,因爲這是很大的消耗。直接將圖層合成到幀的緩衝區中(在屏幕上)比先創建屏幕外緩衝區,然後渲染到紋理中,最後將結果渲染到幀的緩衝區中要廉價很多。因爲這其中涉及兩次昂貴的環境轉換(轉換環境到屏幕外緩衝區,然後轉換環境到幀緩衝區)。

所以當你打開Color Offscreen-Rendered Yellow後看到黃色,這便是一個警告,但這不一定是不好的。如果Core Animation能夠複用屏幕外渲染的結果,這便能夠提升性能。

同時還要注意,rasterized layer的空間是有限的。蘋果暗示大概有屏幕大小兩倍的空間來存儲rasterized layer/屏幕外緩衝區。

如果你使用layer的方式會通過屏幕外渲染,你最好擺脫這種方式。爲layer使用蒙板或者設置圓角半徑會造成屏幕外渲染,產生陰影也會如此。

至於mask,圓角半徑(特殊的mask)和clipsToBounds /masksToBounds,你可以簡單的爲一個已經擁有mask的layer創建內容,比如,已經應用了mask的layer使用一張圖片。如果你想根據layer的內容爲其應用一個長方形mask,你可以使用contentsRect來代替蒙板。

如果你最後設置了shouldRasterize爲YES,那也要記住設置rasterizationScale爲contentsScale。

更多的關於合成

像往常一樣,維基百科上有更多關於透明合成的基礎公式。當我們談完像素後,我們將更深入一點的談論紅,綠,藍和alpha是怎麼在內存中表現的。

OS X

如果你是在OS X上工作,你將會發現大多數debugging選項在一個叫做”Quartz Debug”的獨立程序中,而不是在Instruments中。Quartz Debug是Graphics Tools中的一部分,這可以在蘋果的developer portal中下載到。

Core Animation & OpenGL ES

正如名字所建議的那樣,Core Animation讓你在屏幕上實現動畫。我們將跳過動畫部分,而集中在繪圖上。需要注意的是,Core Animation允許你做非常高效的渲染。這也是爲什麼當你使用Core Animation時可以實現每秒60幀的動畫。

Core Animation的核心是OpenGL ES的一個抽象物,簡而言之,它讓你直接使用OpenGL ES的功能,卻不需要處理OpenGL ES做的複雜的事情。當我們上面談論合成的時候,我們把layer和texture當做等價的,但是他們不是同一物體,可又是如此的類似。

Core Animation的layer可以有子layer,所以最終你得到的是一個圖層樹。Core Animation所需要做的最繁重的任務便是判斷出哪些圖層需要被(重新)繪製,而OpenGL ES需要做的便是將圖層合併、顯示到屏幕上。

舉個例子,當你設置一個layer的內容爲CGImageRef時,Core Animation會創建一個OpenGL紋理,並確保在這個圖層中的位圖被上傳到對應的紋理中。以及當你重寫-drawInContext方法時,Core Animation會請求分配一個紋理,同時確保Core Graphics會將你所做的(即你在drawInContext中繪製的東西)放入到紋理的位圖數據中。一個圖層的性質和CALayer的子類會影響到OpenGL的渲染結果,許多低等級的OpenGL ES行爲被簡單易懂地封裝到CALayer概念中。

Core Animation通過Core Graphics的一端和OpenGL ES的另一端,精心策劃基於CPU的位圖繪製。因爲Core Animation處在渲染過程中的重要位置上,所以你如何使用Core Animation將會對性能產生極大的影響。

CPU限制 VS GPU限制

當你在屏幕上顯示東西的時候,有許多組件參與了其中的工作。其中,CPU和GPU在硬件中扮演了重要的角色。在他們命名中P和U分別代表了”處理”和”單元”,當需要在屏幕上進行繪製時,他們都需要做處理,同時他們都有資源限制(即CPU和GPU的硬件資源)。

爲了每秒達到60幀,你需要確定CPU和GPU不能過載。此外,即使你當前能達到60fps(frame per second),你還是要儘可能多的繪製工作交給GPU做,而讓CPU儘可能的來執行應用程序。通常,GPU的渲染性能要比CPU高效很多,同時對系統的負載和消耗也更低一些。

既然繪圖性能是基於CPU和GPU的,那麼你需要找出是哪一個限制你繪圖性能的。如果你用盡了GPU所有的資源,也就是說,是GPU限制了你的性能,同樣的,如果你用盡了CPU,那就是CPU限制了你的性能。

要告訴你,如果是GPU限制了你的性能,你可以使用OpenGL ES Driver instrument。點擊上面那個小的i按鈕,配置一下,同時注意查看Device Utilization %。現在,當你運行你的app時,你可以看到你GPU的負荷。如果這個值靠近100%,那麼你就需要把你工作的重心放在GPU方面了。

Core Graphics /Quartz 2D

通過Core Graphics這個框架,Quartz 2D被更爲廣泛的知道。

Quartz 2D擁有比我們這裏談到更多的裝飾。我們這裏不會過多的討論關於PDF的創建,渲染,解析,或者打印。只需要注意的是,PDF的打印、創建和在屏幕上繪製位圖的操作是差不多的。因爲他們都是基於Quartz 2D。

讓我們簡單的瞭解一下Quartz 2D主要的概念。有關詳細信息可以到蘋果的官方文檔中瞭解。

放心,當Quartz 2D涉及到2D繪製的時候,它是非常強大的。有基於路徑的繪製,反鋸齒渲染,透明圖層,分辨率,並且設備獨立,可以說出很多特色。這可能會讓人產生畏懼,主要因爲這是一個低級並且基於C的API。

主要的概念當對簡單,UIKit和AppKit都包含了Quartz 2D的一些簡單API,一旦你熟練了,一些簡單C的API也是很容易理解的。最終你學會了一個能實現Photoshop和Illustrator大部分功能的繪圖引擎。蘋果把iOS程序裏面的股票應用作爲講解Quartz 2D在代碼中實現動態渲染的一個例子。

當你的程序進行位圖繪製時,不管使用哪種方式,都是基於Quartz 2D的。也就是說,CPU部分實現的繪製是通過Quartz 2D實現的。儘管Quartz可以做其它的事情,但是我們這裏還是集中於位圖繪製,在緩衝區(一塊內存)繪製位圖會包括RGBA數據。

比方說,我們要畫一個八角形,我們通過UIKit能做到這一點

4

相對應的Core Graphics代碼:

5

需要問的問題是:這個繪製到哪兒去了?這正好引出所謂的CGContext登場。我們傳過去的ctx參數正是在那個上下文中。而這個上下文定義了我們需要繪製的地方。如果我們實現了CALayer的-drawInContext:這時已經傳過來一個上下文。繪製到這個上下文中的內容將會被繪製到圖層的備份區(圖層的緩衝區).但是我們也可以創建我們自己的上下文,叫做基於位圖的上下文,比如CGBitmapContextCreate().這個方法返回一個我們可以傳給CGContext方法來繪製的上下文。

注意UIKit版本的代碼爲何不傳入一個上下文參數到方法中?這是因爲當使用UIKit或者AppKit時,上下文是唯一的。UIkit維護着一個上下文堆棧,UIKit方法總是繪製到最頂層的上下文中。你可以使用UIGraphicsGetCurrentContext()來得到最頂層的上下文。你可以使用UIGraphicsPushContext()和UIGraphicsPopContext()在UIKit的堆棧中推進或取出上下文。

最爲突出的是,UIKit使用UIGraphicsBeginImageContextWithOptions()和UIGraphicsEndImageContext()方便的創建類似於CGBitmapContextCreate()的位圖上下文。混合調用UIKit和Core Graphics非常簡單:

6

或者另外一種方法:

7

你可以使用Core Graphics創建大量的非常酷的東西。一個很好的理由就是,蘋果的文檔有很多例子。我們不能得到所有的細節,但是Core Graphics有一個非常接近Adobe Illustrator和Adobe Photoshop如何工作的繪圖模型,並且大多數工具的理念翻譯成Core Graphics了。終究,他是起源於NeXTSTEP。(原來也是喬老爺的作品)

CGLayer

一件非常值得提起的事,便是CGLayer。它經常被忽視,並且它的名字有時會造成困惑。在Photoshop中,從圖層的意義上講,它不是一個圖層。從Core Animation的意義上講,它還不是一個圖層。

把CGLayer想象成一個子上下文。它共用父上下文的所有特性。你可以獨立於父上下文,在它自己的後備存儲中繪製。並且因爲它跟上下文緊密的聯繫在一起,CGLayer可以被高效的繪製到上下文中。

什麼時候這將變得有用?如果你用Core Graphics來繪製一些相當難懂的,並且部分內容需要被重新繪製的,你只需將那部分內容繪製到CGLayer一次,然後便可繪製/合成這個CGLayer到父上下文中。當這可行的時候,這是一個非常優雅的性能竅門。這和我們上文中提到的屏幕外緩衝區概念有點類似。你需要做出權衡的便是,是否需要爲CGLayer的後備存儲申請額外的內存,衡量這是否對你有用。

我們最初指出CGLayer可以用來提升重複繪製相同元素的速度。正如Dave Hayden指出,這些小道消息不再可靠。

像素

屏幕上的像素是由紅,綠,藍三種顏色組件構成的。因此,位圖數據有時也被叫做RGB數據。你可能會對數據如何組織在內存中感到好奇。而事實是,有很多種不同的方式在內存中展現RGB位圖數據。

稍後我們將會談到壓縮數據,這又是一個完全不同的概念。現在,我們先看一下RGB位圖數據,我們可以從顏色組件:紅,綠,藍中得到一個值。而大多數情況下,我們有第四個組件:透明度。最終我們從每個像素中得到四個單獨的值。

默認的像素佈局

在iOS和OS X上最常見的格式就是大家所熟知的32bits-per-pixel(bpp),8bits-per-componet(bpc),透明度會首先被乘以到像素值上(就像上文中提到的那個公式一樣),在內存中,像下面這樣:

8

這個格式經常被叫做ARGB。每個像素佔用4字節(32bpp),每一個顏色組件是1字節(8bpc).每個像素有一個alpha值,這個值總是最先得到的(在RGB值之前),最終紅、綠、藍的值都會被預先乘以alpha的值。預乘的意思就是alpha值被烘烤到紅、綠、藍的組件中。如果我們有一個橙色,他們各自的8bpc就像這樣:240,99,24.一個完全不透明的橙色像素擁有的ARGB值爲255,240,99,24,它在內存中的佈局就像上面圖示那樣。如果我們有一個相同顏色的像素,但是alpha值爲33%,那麼他的像素值便是84,80,33,8.

另一個常見的格式便是32bpp,8bpc,跳過第一個alpha值,看起來像下面這樣:

9

這常被叫做xRGB。像素並沒有任何alpha值(他們都被假定爲100%不透明),但是內存佈局是一樣的。你應該想知道爲什麼這種格式很流行,當我們每一個像素中都有一個不用字節時,我們將會省下25%的空間。事實證明,這種格式更容易被現代的CPU和繪圖算法消化,因爲每一個獨立的像素都對齊到32-bit的邊界。現代的CPU不喜歡裝載(讀取)不對齊的數據,特別是當將這種數據和上面沒有alpha值格式的數據混合時,算法需要做很多挪動和蒙板操作。

當處理RGB數據時,Core Graphics也需要支持把alpha值放到最後(另外還要支持跳過)。有時候也分別稱爲RGBA和RGBx,假定是8bpc,並且預乘了alpha值。

深奧的佈局

大多數時候,當處理位圖數據時,我們也需要處理Core Graphics/Quartz 2D。有一個非常詳細的列表列出了他支持的混合組合。但是讓我們首先看一下剩下的RGB格式:

另一個選擇是16bpp,5bpc,不包含alpha值。這個格式相比之前一個僅佔用50%的存儲大小(每個像素2字節),但將使你存儲它的RGB數據到內存或磁盤中變得困難。既然這種格式中,每個顏色組件只有5bits(原文中寫的是每個像素是5bits,但根據上下文可知應該是每個組件),這樣圖形(特別是平滑漸變的)會造成重疊在一起的假象。

還有一個是64bpp,16bpc,最終爲128bpp,32bpc,浮點數組件(有或沒有alpha值)。它們分別使用8字節和16字節,並且允許更高的精度。當然,這會造成更多的內存使用和昂貴的計算。

整件事件中,Core Graphics也支持一些像灰度模式和CMYK格式,這些格式類似於僅有alpha值的格式(蒙板)。

二維數據

當顏色組件(紅、綠、藍、alpha)混雜在一起的時候,大多數框架(包括Core Graphics)使用像素數據。正是這種情況下我們稱之爲二維數據,或者二維組件。這個意思是:每一個顏色組件都在它自己的內存區域,也就是說它是二維的。比如RGB數據,我們有三個獨立的內存區域,一個大的區域包含了所有像素的紅顏色的值,一個包含了所有綠顏色的值,一個包含了所有藍顏色的值。

在某些情況下,一些視頻框架便會使用二維數據。

YCbCr

當我們處理視頻數據時,YCbCr是一種常見的格式。它也是包含了三種(Y,Cb和Cr)代表顏色數據的組件。但是簡單的講,它更類似於通過人眼看到的顏色。人眼對Cb和Cr這兩種組件的色彩度不太能精確的辨認出來,但是能很準確的識別出Y的亮度。當數據使用YCbCr格式時,在同等的條件下,Cb和Cr組件比Y組件壓縮的更緊密。

出於同樣的原因,JPEG圖像有時會將像素數據從RGB轉換到YCbCr。JPEG單獨的壓縮每一個二維顏色。當壓縮基於YCbCr的平面時,Cb和Cr能比Y壓縮得更完全。

圖片格式

當你在iOS或者OS X上處理圖片時,他們大多數爲JPEG和PNG。讓我們更進一步觀察。

JPEG

每個人都知道JPEG。他是相機的產物。它代表這照片如何存儲在電腦上。甚至你嘛嘛都聽說過JPEG。

一個很好的理由,很多人都認爲JPEG文件僅是另一種像素數據的格式,就像我們剛剛談到的RGB像素佈局那樣。這樣理解離真像真是差十萬八千里了。

將JPEG數據轉換成像素數據是一個非常複雜的過程,你通過一個週末的計劃都不能完成,甚至是一個非常漫長的週末(原文的意思好像就是爲了表達這個過程非常複雜,不過老外的比喻總讓人拎不清)。對於每一個二維顏色,JPEG使用一種基於離散餘弦變換(簡稱DCT變換)的算法,將空間信息轉變到頻域.這個信息然後被量子化,排好序,並且用一種哈夫曼編碼的變種來壓縮。很多時候,首先數據會被從RGB轉換到二維YCbCr,當解碼JPEG的時候,這一切都將變得可逆。

這也是爲什麼當你通過JPEG文件創建一個UIImage並且繪製到屏幕上時,將會有一個延時,因爲CPU這時候忙於解壓這個JPEG。如果你需要爲每一個tableviewcell解壓JPEG,那麼你的滾動當然不會平滑(原來tableviewcell裏面最要不要用JPEG的圖片)。

那究竟爲什麼我們還要用JPEG呢?答案就是JPEG可以非常非常好的壓縮圖片。一個通過Iphone5拍攝的,未經壓縮的圖片佔用接近24M。但是通過默認壓縮設置,你的照片通常只會在2-3M左右。JPEG壓縮這麼好是因爲它是失真的,它去除了人眼很難察覺的信息,並且這樣做可以超出像gzip這樣壓縮算法的限制。但這僅僅在圖片上有效的,因爲JPEG依賴於圖片上有很多人類不能察覺出的數據。如果你從一個基本顯示文本的網頁上截取一張圖,JPEG將不會這麼高效。壓縮效率將會變得低下,你甚至能看出來圖片已經壓縮變形了。

PNG

PNG讀作”ping”。和JPEG相反,它的壓縮對格式是無損的。當你將一張圖片保存爲PNG,並且打開它(或解壓),所有的像素數據會和最初一模一樣,因爲這個限制,PNG不能像JPEG一樣壓縮圖片,但是對於像程序中的原圖(如buttons,icons),它工作的非常好。更重要的是,解碼PNG數據比解碼JPEG簡單的多。

在現實世界中,事情從來沒有那麼簡單,目前存在了大量不同的PNG格式。可以通過維基百科查看詳情。但是簡言之,PNG支持壓縮帶或不帶alpha通道的顏色像素(RGB),這也是爲什麼它在程序原圖中表現良好的另一個原因。

挑選一個格式

當你在你的程序中使用圖片時,你需要堅持這兩種格式:JPEG或者PNG。讀寫這種格式文件的壓縮和解壓文件能表現出很高的性能,另外,還支持並行操作。同時Apple正在改進解壓縮並可能出現在將來的新操作系統中,屆時你將會得到持續的性能提升。如果嘗試使用另一種格式,你需要注意到,這可能對你程序的性能會產生影響,同時可能會打開安全漏洞,經常,圖像解壓縮算法是黑客最喜歡的攻擊目標。

已經寫了很多關於優化PNGs,如果你想要了解更多,請到互聯網上查詢。非常重要的一點,注意Xcode優化PNG選項和優化其他引擎有很大的不同。

當Xcode優化一個PNG文件的時候,它將PNG文件變成一個從技術上講不再是有效的PNG文件。但是iOS可以讀取這種文件,並且這比解壓縮正常的PNG文件更快。Xcode改變他們,讓iOS通過一種對正常PNG不起作用的算法來對他們解壓縮。值得注意的重點是,這改變了像素的佈局。正如我們所提到的一樣,在像素之下有很多種方式來描繪RGB數據,如果這不是iOS繪製系統所需要的格式,它需要將每一個像素的數據替換,而不需要加速來做這件事。

讓我們再強調一遍,如果你可以,你需要爲原圖設置resizable images。你的文件將變得更小,因此你只需要從文件系統裝載更少的數據。

UIKit和Pixels

每一個在UIKit中的view都有它自己的CALayer。依次,這些圖層都有一個叫像素位圖的後備存儲,有點像一個圖像。這個後備存儲正是被渲染到顯示器上的。

With –drawRect:

如果你的視圖類實現了-drawRect:,他們將像這樣工作:

當你調用-setNeedsDisplay,UIKit將會在這個視圖的圖層上調用-setNeedsDisplay。這爲圖層設置了一個標識,標記爲dirty(直譯是髒的意思,想不出用什麼詞比較貼切),但還顯示原來的內容。它實際上沒做任何工作,所以在一個row中可以多次調用-setNeedsDisplay。

下面,當渲染系統準備好,它會調用視圖圖層的-display方法.此時,圖層會裝配它的後備存儲。然後建立一個Core Graphics上下文(CGContextRef),將後備存儲對應內存中的數據恢復出來,繪圖會進入對應的內存區域,並使用CGContextRef繪製。

當你使用UIKit的繪製方法,例如:UIRectFill()或者-[UIBezierPath fill]代替你的-drawRect:方法,他們將會使用這個上下文。使用方法是,UIKit將後備存儲的CGContextRef推進他的graphics context stack,也就是說,它會將那個上下文設置爲當前的。因此UIGraphicsGetCurrent()將會返回那個對應的上下文。既然UIKit使用UIGraphicsGetCurrent()繪製方法,繪圖將會進入到圖層的後備存儲。如果你想直接使用Core Graphics方法,你可以自己調用UIGraphicsGetCurrent()得到相同的上下文,並且將這個上下文傳給Core Graphics方法。

從現在開始,圖層的後備存儲將會被不斷的渲染到屏幕上。直到下次再次調用視圖的-setNeedsDisplay,將會依次將圖層的後備存儲更新到視圖上。

不使用-drawRect:

當你用一個UIImageView時,事情略有不同,這個視圖仍然有一個CALayer,但是圖層卻沒有申請一個後備存儲。取而代之的是使用一個CGImageRef作爲他的內容,並且渲染服務將會把圖片的數據繪製到幀的緩衝區,比如,繪製到顯示屏。

在這種情況下,將不會繼續重新繪製。我們只是簡單的將位圖數據以圖片的形式傳給了UIImageView,然後UIImageView傳給了Core Animation,然後輪流傳給渲染服務。

實現-drawRect:還是不實現-drawRect:

這聽起來貌似有點低俗,但是最快的繪製就是你不要做任何繪製。

大多數時間,你可以不要合成你在其他視圖(圖層)上定製的視圖(圖層),這正是我們推薦的,因爲UIKit的視圖類是非常優化的 (就是讓我們不要閒着沒事做,自己去合併視圖或圖層) 。

當你需要自定義繪圖代碼時,Apple在WWDC 2012’s session 506:Optimizing 2D Graphics and Animation Performance中展示了一個很好的例子:”finger painting”

另一個地方需要自定義繪圖的就是iOS的股票軟件。股票是直接用Core Graphics在設備上繪製的,注意,這僅僅是你需要自定義繪圖,你並不需要實現-drawRect:方法。有時,通過UIGraphicsBeginImageContextWithOptions()或者CGBitmapContextCeate()創建位圖會顯得更有意義,從位圖上面抓取圖像,並設置爲CALayer的內容。下面我們將給出一個例子來測試,檢驗。

單一顏色

如果我們看這個例子:

10

現在我們知道這爲什麼不好:我們促使Core Animation來爲我們創建一個後備存儲,並讓它使用單一顏色填充後備存儲,然後上傳給GPU。

我們跟本不需要實現-drawRect:,並節省這些代碼工作量,只需簡單的設置這個視圖圖層的背景顏色。如果這個視圖有一個CAGradientLayer作爲圖層,那麼這個技術也同樣適用於此(漸變圖層)。

可變尺寸的圖像

類似的,你可以使用可變尺寸的圖像來降低繪圖系統的壓力。讓我們假設你需要一個300×500點的按鈕插圖,這將是600×100=60k像素或者60kx4=240kB內存大小需要上傳到GPU,並且佔用VRAM。如果我們使用所謂的可變尺寸的圖像,我們只需要一個54×12點的圖像,這將佔用低於2.6k的像素或者10kB的內存,這樣就變得更快了。

Core Animation可以通過CALayer的contentsCenter屬性來改變圖像,大多數情況下,你可能更傾向於使用,-[UIImage resizableImageWithCapInsets:resizingMode:]。

同時注意,在第一次渲染這個按鈕之前,我們並不需要從文件系統讀取一個60k像素的PNG並解碼,解碼一個小的PNG將會更快。通過這種方式,你的程序在每一步的調用中都將做更少的工作,並且你的視圖將會加載的更快。

同時發生的繪圖

最後一個objc.io的問題是關於同時發生繪圖的討論。正如你所知道的一樣,UIKit的線程模型是非常簡單的:你僅可以從主隊列(比如主線程)中調用UIKit類(比如視圖),那麼同時繪圖又是什麼呢?

如果你必須實現-drawRect:,並且你必須繪製大量的東西,這將佔用時間。由於你希望動畫變得更平滑,除了在主隊列中,你還希望在其他隊列中做一些工作。同時發生的繪圖是複雜的,但是除了幾個警告,同時發生的繪圖還是比較容易實現的。

我們除了在主隊列中可以向CALayer的後備存儲中繪製一些東西,其他方法都將不可行。可怕的事情將會發生。我們能做的就是向一個完全斷開鏈接的位圖上下文中進行繪製。

正如我們上面所提到的一樣,在Core Graphics下,所有Core Graphics繪製方法都需要一個上下文參數來指定繪製到那個上下文中。UIKit有一個當前上下文的概念(也就是繪製到哪兒去)。這個當前的上下文就是per-thread.

爲了同時繪製,我們需要做下面的操作。我們需要在另一個隊列創建一個圖像,一旦我們擁有了圖像,我們可以切換回主隊列,並且設置這個圖像爲UIImageView的圖像。這個技術在WWDC 2012 session 211中討論過。(異步下載圖片經常用到這個)

增加一個你可以在其中繪製的新方法:

11

這個方法通過UIGraphicsBeginImageContextWithOptions()方法,並根據給定的大小創建一個新的CGContextRef位圖。這個方法也會將這個上下文設置爲當前UIKit的上下文。現在你可以在這裏做你想在-drawRect:中做的事了。然後我們可以通過UIGraphicsGetImageFromCurrentImageContext(),將獲得的這個上下文位圖數據作爲一個UIImage,最終移除這個上下文。

很重要的一點就是,你在這個方法中所做的所有繪圖的代碼都是線程安全的,也就是說,當你訪問屬性等等,他們需要線程安全。因爲你是在另一個隊列中調用這個方法的。如果這個方法在你的視圖類中,那就需要注意一點了。另一個選擇就是創建一個單獨的渲染類,並設置所有需要的屬性,然後通過觸發來渲染圖片。如果這樣,你可以通過使用簡單的UIImageView或者UITableViewCell。

要知道,所有UIKit的繪製API在使用另一個隊列時,都是安全的。只需要確定是在同一個操作中調用他們的,這個操作需要以UIGraphicsBeginImageContextWithOptions()開始,以UIGraphicsEndIamgeContext()結束。

你需要像下面這樣觸發渲染代碼:

12

要注意,我們是在主隊列中調用view.image = image.這是一個非常重要的細節。你不可以在任何其他隊列中調用這個代碼。

像往常一樣,同時繪製會伴隨很多問題,你現在需要取消後臺渲染。並且在渲染隊列中設置合理的同時繪製的最大限度。

爲了支持這一切,最簡單的就是在一個NSOperation子類內部實現-renderInImageOfSize:

最終,需要指出,設置UITableViewCell內容爲異步是非常困難的。單元格很有可能在完成異步渲染前已經被複用了。儘管單元格已經被其他地方複用,但你只需要設置內容就行了。

CALayer

到現在爲止,你需要知道在GPU內,一個CALayer在某種方式上和一個紋理類似。圖層有一個後備存儲,這便是被用來繪製到屏幕上的位圖。

通常,當你使用CALayer時,你會設置它的內容爲一個圖片。這到底做了什麼?這樣做會告訴Core Animation使用圖片的位圖數據作爲紋理。如果這個圖片(JPEG或PNG)被壓縮了,Core Animation將會這個圖片解壓縮,然後上傳像素數據到GPU。

儘管還有很多其他中圖層,如果你是用一個簡單的沒有設置上下文的CALayer,併爲這個CALayer設置一個背景顏色,Core Animation並不會上傳任何數據到GPU,但卻能夠不用任何像素數據而在GPU上完成所有的工作,類似的,對於漸變的圖層,GPU是能創建漸變的,而且不需要CPU做任何工作,並且不需要上傳任何數據到GPU。

自定義繪製的圖層

如果一個CALayer的子類實現了-drawInContext:或者它的代理,類似於-drawLayer:inContest:,Core Animation將會爲這個圖層申請一個後備存儲,用來保存那些方法繪製進來的位圖。那些方法內的代碼將會運行在CPU上,結果將會被上傳到GPU。

形狀和文本圖層

形狀和文本圖層還是有些不同的。開始時,Core Animation爲這些圖層申請一個後備存儲來保存那些需要爲上下文生成的位圖數據。然後Core Animation會講這些圖形或文本繪製到後備存儲上。這在概念上非常類似於,當你實現-drawInContext:方法,然後在方法內繪製形狀或文本,他們的性能也很接近。

在某種程度上,當你需要改變形狀或者文本圖層時,這需要更新它的後備存儲,Core Animation將會重新渲染後備存儲。例如,當動態改變形狀圖層的大小時,Core Animation需要爲動畫中的每一幀重新繪製形狀。

異步繪圖

CALayer有一個叫做drawsAsynchronously的屬性,這似乎是一個解決所有問題的高招。注意,儘管這可能提升性能,但也可能讓事情變慢。

當你設置drawsAsynchronously爲YES時,發生了什麼?你的-drawRect:/-drawInContext:方法仍然會被在主線程上調用。但是所有調用Core Graphics的操作都不會被執行。取而代之的是,繪製命令被推遲,並且在後臺線程中異步執行。

這種方式就是先記錄繪圖命令,然後在後臺線程中重現。爲了這個過程的順利進行,更多的工作需要被做,更多的內存需要被申請。但是主隊列中的一些工作便被移出來了(大概意思就是讓我們把一些能在後臺實現的工作放到後臺實現,讓主線程更順暢)。

對於昂貴的繪圖方法,這是最有可能提升性能的,但對於那些繪圖方法來說,也不會節省太多資源。

翻譯到這兒終於結束了,迫不及待的發出來了,整片文章不記空格的話,一共16,828個字符,第一次翻譯這麼長的文章,由於這篇文章相對來說有點底層,所以翻譯時,很擔心有的地方翻譯錯了,這樣會對大家造成誤解,所有如果讀者發現有錯誤的地方,請在下面留言,一經發現,立馬改正。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章