IOS UIView 01-View開始深刻 繪製像素到屏幕上

注:本人是翻譯過來,而且加上本人的一點看法。html

 

前言ios

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

目錄算法

1. 圖形堆棧
2. 軟件組成
3. 硬件參與者
4. 合成
5. 不透明 VS 透明
6. 像素對齊 VS 不重合在一塊兒
7. Masks
8. 離屏渲染(Offscreen Rendering)
9. 更多的關於合成
10. Core Animation OpenGL ES
11. CPU限制 VS GPU限制
12. Core Graphics / Quartz 2D
13. CGLayer
14. 像素
15. 默認的像素佈局
16. 深奧的佈局
17. 二維數據
18. YCbCr
19. 圖片格式
20. UIKit 和 Pixels
21. 繪圖與圖層CALayer數組

 

1. 圖形堆棧緩存

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

 

2. 軟件組成session

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

image

 

  Display 的上一層即是圖形處理單元 GPU,GPU 是一個專門爲圖形高併發計算而量身定作的處理單元。這也是爲何它能同時更新全部的像素,並呈現到顯示器上。它併發的本性讓它能高效的將不一樣紋理合成起來。咱們將有一小塊內容來更詳細的討論圖形合成。關鍵的是,GPU 是很是專業的,所以在某些工做上很是高效。好比,GPU 很是快,而且比 CPU 使用更少的電來完成工做。一般 CPU 都有一個廣泛的目的,它能夠作不少不一樣的事情,可是合成圖像在 CPU 上卻顯得比較慢。app

  GPU Driver 是直接和 GPU 交流的代碼塊。不一樣的GPU是不一樣的性能怪獸,可是驅動使他們在下一個層級上顯示的更爲統一,典型的下一層級有 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,這也須要作一些相應的操做,最終像素顯示到屏幕上。

  這個過程的每一部分都有各自的挑戰,而且許多時候須要作出折中的選擇。

 

3. 硬件參與者

 

image

  正如上面這張簡單的圖片顯示那些挑戰: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。

  這張圖涉及到一些錯綜複雜的方面,咱們將會把這些方面提取出來並深一步瞭解。

 

4. 合成

  在圖形世界中,合成是一個描述不一樣位圖如何放到一塊兒來建立你最終在屏幕上看到圖像的過程。在許多方面顯得顯而易見,而讓人忘了背後錯綜複雜的計算。

  讓咱們忽略一些難懂的事例而且假定屏幕上一切事物皆紋理。一個紋理就是一個包含 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是一塊高度優化的硬件來作這種事情,但這仍是會讓它很是忙碌。

 

5. 不透明 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 通道。

 

6. 像素對齊 VS 不重合在一塊兒

  到如今咱們都在考慮像素完美重合在一塊兒的 layers。當全部的像素是對齊的時候咱們獲得相對簡單的計算公式。每當 GPU 須要計算出屏幕上一個像素是什麼顏色的時候,它只須要考慮在這個像素之上的全部 layer 中對應的單個像素,並把這些像素合併到一塊兒。或者,若是最頂層的紋理是不透明的(即圖層樹的最底層),這時候 GPU 就能夠簡單的拷貝它的像素到屏幕上。

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

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

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

 

7. Masks

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

 

8. 離屏渲染(Offscreen Rendering)

  離屏渲染能夠被 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。

 

9. 更多的關於合成

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

 

10. 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 將會對性能產生極大的影響。

 

11. 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方面了。

 

12. 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 能作到這一點

UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
[path addLineToPoint:CGPointMake(0.4, 18.05)];
[path addLineToPoint:CGPointMake(18.8, -0.47)];
[path addLineToPoint:CGPointMake(37.21, 18.05)];
[path addLineToPoint:CGPointMake(34.31, 20.83)];
[path addLineToPoint:CGPointMake(20.88, 7.22)];
[path addLineToPoint:CGPointMake(20.88, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 42.18)];
[path addLineToPoint:CGPointMake(16.72, 7.22)];
[path closePath];
path.lineWidth = 1;
[[UIColor redColor] setStroke];
[path stroke];

相對應的 Core Graphics 代碼:

CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
CGContextAddLineToPoint(ctx, 0.4, 18.05);
CGContextAddLineToPoint(ctx, 18.8, -0.47);
CGContextAddLineToPoint(ctx, 37.21, 18.05);
CGContextAddLineToPoint(ctx, 34.31, 20.83);
CGContextAddLineToPoint(ctx, 20.88, 7.22);
CGContextAddLineToPoint(ctx, 20.88, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 42.18);
CGContextAddLineToPoint(ctx, 16.72, 7.22);
CGContextClosePath(ctx);
CGContextSetLineWidth(ctx, 1);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextStrokePath(ctx);

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

  注意 UIKit 版本的代碼爲什麼不傳入一個上下文參數到方法中?這是由於當使用 UIKit 或者 AppKit 時,上下文是惟一的。UIkit 維護着一個上下文堆棧,UIKit 方法老是繪製到最頂層的上下文中。你可使用 UIGraphicsGetCurrentContext() 來獲得最頂層的上下文。你可使用 UIGraphicsPushContext()UIGraphicsPopContext() 在 UIKit 的堆棧中推動或取出上下文。

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

UIGraphicsBeginImageContextWithOptions(CGSizeMake(45, 45), YES, 2);
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, 16.72, 7.22);
CGContextAddLineToPoint(ctx, 3.29, 20.83);
...
CGContextStrokePath(ctx);
UIGraphicsEndImageContext();

或者另一種方法:

CGContextRef ctx = CGBitmapContextCreate(NULL, 90, 90, 8, 90 * 4, space, bitmapInfo);
CGContextScaleCTM(ctx, 0.5, 0.5);
UIGraphicsPushContext(ctx);
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(16.72, 7.22)];
[path addLineToPoint:CGPointMake(3.29, 20.83)];
...
[path stroke];
UIGraphicsPopContext(ctx);
CGContextRelease(ctx);

  你可使用 Core Graphics 建立大量的很是酷的東西。一個很好的理由就是,蘋果的文檔有不少例子。咱們不能獲得全部的細節,可是 Core Graphics 有一個很是接近 Adobe IllustratorAdobe Photoshop 如何工做的繪圖模型,而且大多數工具的理念翻譯成 Core Graphics 了。終究,他是起源於 NeXTSTEP 。(原來也是喬老爺的做品)。

 

13. CGLayer

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

 

14. 像素

  屏幕上的像素是由紅,綠,藍三種顏色組件構成的。所以,位圖數據有時也被叫作 RGB 數據。你可能會對數據如何組織在內存中感到好奇。而事實是,有不少種不一樣的方式在內存中展示RGB位圖數據。

  稍後咱們將會談到壓縮數據,這又是一個徹底不一樣的概念。如今,咱們先看一下RGB位圖數據,咱們能夠從顏色組件:紅,綠,藍中獲得一個值。而大多數狀況下,咱們有第四個組件:透明度。最終咱們從每一個像素中獲得四個單獨的值。

 

15. 默認的像素佈局

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

A   R   G   B   A   R   G   B   A   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

  這個格式常常被叫作 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 值,看起來像下面這樣:

x   R   G   B   x   R   G   B   x   R   G   B  
| pixel 0       | pixel 1       | pixel 2   
  0   1   2   3   4   5   6   7   8   9   10  11 ...

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

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

 

16. 深奧的佈局

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

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

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

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

 

17. 二維數據

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

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

 

18. YCbCr

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

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

 

19. 圖片格式

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

1) 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 將不會這麼高效。壓縮效率將會變得低下,你甚至能看出來圖片已經壓縮變形了。

2) PNG

  PNG讀做」ping」。和 JPEG 相反,它的壓縮對格式是無損的。當你將一張圖片保存爲 PNG,而且打開它(或解壓),全部的像素數據會和最初如出一轍,由於這個限制,PNG 不能像 JPEG 同樣壓縮圖片,可是對於像程序中的原圖(如buttons,icons),它工做的很是好。更重要的是,解碼 PNG 數據比解碼 JPEG 簡單的多。

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

3) 挑選一個格式

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

  已經寫了不少關於優化 PNGs,若是你想要了解更多,請到互聯網上查詢。很是重要的一點,注意 Xcode 優化 PNG 選項和優化其餘引擎有很大的不一樣。

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

  讓咱們再強調一遍,若是你能夠,你須要爲原圖設置 resizable images。你的文件將變得更小,所以你只須要從文件系統裝載更少的數據。

 

20. UIKit 和 Pixels

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

1) 使用 –drawRect:

  若是你的視圖類實現了 -drawRect:,他們將像這樣工做:

  當你調用 -setNeedsDisplay,UIKit 將會在這個視圖的圖層上調用 -setNeedsDisplay。這爲圖層設置了一個標識,標記爲 dirty(直譯是髒的意思,想不出用什麼詞比較貼切,污染?),但還顯示原來的內容。它實際上沒作任何工做,因此屢次調用 -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 ,將會依次將圖層的後備存儲更新到視圖上。

2) 不使用 -drawRect:

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

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

3) 實現-drawRect: 仍是不實現 -drawRect:

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

  大多數時間,你能夠不要合成你在其餘視圖(圖層)上定製的視圖(圖層),這正是咱們推薦的,由於 UIKit 的視圖類是很是優化的 (就是讓咱們不要閒着沒事作,本身去合併視圖或圖層) 。

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

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

4) 單一顏色

  若是咱們看這個例子:

// Don't do this
- (void)drawRect:(CGRect)rect
{
    [[UIColor redColor] setFill];
    UIRectFill([self bounds]);
}

  如今咱們知道這爲何很差:咱們促使 Core Animation 來爲咱們建立一個後備存儲,並讓它使用單一顏色填充後備存儲,而後上傳給 GPU。

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

5) 可變尺寸的圖像

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

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

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

 

21. 繪圖與圖層CALayer

1) 併發繪圖

  UIKit 的線程模型是很是簡單的:你僅能夠從主隊列(好比主線程)中調用 UIKit 類(好比視圖),那麼併發繪圖又是什麼呢?

若是你必須實現 -drawRect:,而且你必須繪製大量的東西,這將佔用時間。因爲你但願動畫變得更平滑,除了在主隊列中,你還但願在其餘隊列中作一些工做。同時發生的繪圖是複雜的,可是除了幾個警告,同時發生的繪圖仍是比較容易實現的。

  咱們除了在主隊列中能夠向 CALayer 的後備存儲中繪製一些東西,其餘方法都將不可行。可怕的事情將會發生。咱們能作的就是向一個徹底斷開連接的位圖上下文中進行繪製。

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

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

增長一個你能夠在其中繪製的新方法:

- (UIImage *)renderInImageOfSize:(CGSize)size
{
    UIGraphicsBeginImageContextWithOptions(size, NO, 0);

    // do drawing here

    UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return result;
}

  這個方法經過 UIGraphicsBeginImageContextWithOptions() 方法,並根據給定的大小建立一個新的 CGContextRef 位圖。這個方法也會將這個上下文設置爲當前UIKit的上下文。如今你能夠在這裏作你想在 -drawRect: 中作的事了。而後咱們能夠經過UIGraphicsGetImageFromCurrentImageContext(),將得到的這個上下文位圖數據做爲一個 UIImage,最終移除這個上下文。

  很重要的一點就是,你在這個方法中所作的全部繪圖的代碼都是線程安全的,也就是說,當你訪問屬性等等,他們須要線程安全。由於你是在另外一個隊列中調用這個方法的。若是這個方法在你的視圖類中,那就須要注意一點了。另外一個選擇就是建立一個單獨的渲染類,並設置全部須要的屬性,而後經過觸發來渲染圖片。若是這樣,你能夠經過使用簡單的 UIImageView 或者 UITableViewCell。

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

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

UIImageView *view; // assume we have this
NSOperationQueue *renderQueue; // assume we have this
CGSize size = view.bounds.size;
[renderQueue addOperationWithBlock:^(){
        UIImage *image = [renderer renderInImageOfSize:size];
        [[NSOperationQueue mainQueue] addOperationWithBlock:^(){
            view.image = image;
        }];
}];

  要注意,咱們是在主隊列中調用 view.image = image.這是一個很是重要的細節。你不能夠在任何其餘隊列中調用這個代碼。

  像往常同樣,同時繪製會伴隨不少問題,你如今須要取消後臺渲染。而且在渲染隊列中設置合理的同時繪製的最大限度。

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

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

2)CALayer

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

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

  儘管還有不少其餘種類的圖層,若是你是用一個簡單的沒有設置上下文的 CALayer,併爲這個 CALayer 設置一個背景顏色,Core Animation 並不會上傳任何數據到 GPU,但卻可以不用任何像素數據而在 GPU 上完成全部的工做,相似的,對於漸變的圖層,GPU 是能建立漸變的,並且不須要 CPU 作任何工做,而且不須要上傳任何數據到 GPU。

3)自定義繪製的圖層

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

4)形狀和文本圖層

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

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

5)異步繪圖

  CALayer 有一個叫作 drawsAsynchronously 的屬性,這彷佛是一個解決全部問題的高招。注意,儘管這可能提高性能,但也可能讓事情變慢。

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

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

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

 

能夠關注本人的公衆號,多年經驗的原創文章共享給你們。

相關文章
相關標籤/搜索