1. 程式人生 > >繪製畫素到螢幕上

繪製畫素到螢幕上

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

圖形堆疊

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

軟體組成

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

軟體堆疊

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

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,這也需要做一些相應的操作,最終畫素顯示到螢幕上。

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

硬體參與者

挑戰

正如上面這張簡單的圖片顯示那些挑戰: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是一塊高度優化的硬體來做這種事情,但這還是會讓它非常忙碌,

不透明 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 中顯示出來的(即圖層中的部分)才會被渲染出來。

離屏渲染(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。

更多的關於合成

像往常一樣,維基百科上有更多關於透明合成的基礎公式。當我們談完畫素後,我們將更深入一點的談論紅,綠,藍和 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 能做到這一點

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 Illustrator 和 Adobe Photoshop 如何工作的繪圖模型,並且大多數工具的理念翻譯成 Core Graphics 了。終究,他是起源於 NeXTSTEP 。(原來也是喬老爺的作品)。

CGLayer

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

畫素

螢幕上的畫素是由紅,綠,藍三種顏色元件構成的。因此,點陣圖資料有時也被叫做 RGB 資料。你可能會對資料如何組織在記憶體中感到好奇。而事實是,有很多種不同的方式在記憶體中展現RGB點陣圖資料。

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

預設的畫素佈局

在 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 值。

深奧的佈局

大多數時候,當處理點陣圖資料時,我們也需要處理 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(直譯是髒的意思,想不出用什麼詞比較貼切,汙染?),但還顯示原來的內容。它實際上沒做任何工作,所以多次呼叫 -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 作為他的內容,並且渲染服務將會把圖片的資料繪製到幀的緩衝區,比如,繪製到顯示屏。

相關推薦

繪製螢幕

一個畫素是如何繪製到螢幕上去的?有很多種方式將一些東西對映到顯示屏上,他們需要呼叫不同的框架、許多功能和方法的結合體。這裡我們大概的看一下螢幕之後發生的事情。當你想要弄清楚什麼時候、怎麼去查明並解決問題時,我希望這篇文章能幫助你理解哪一個 API 可以更好的幫你解決問題。

Qt快速繪製點的處理方法

在有些情況下,我們需要對螢幕上的畫素點進行大量的繪製操作。比如我之前模擬寫的一個渲染管線開源練習,涉及到了大量的畫素點操作。而Qt本身的QPen和QPainter::drawPoint的API如果操作大量的畫素點,會非常耗時,因此我Google了這個方式: 原文連結:https://

Unity3D之如何建立正確的比在螢幕

       關於這篇文章的命名,實在不知道怎麼命名好,大概功能就是:比如一張寬高為100x100的圖片顯示在螢幕上,那2D攝像頭的Size值為多少時,螢幕上顯示出來圖片大小和圖片的實際畫素一致。 這裡涉及到一個GL座標和畫素座標值的轉換比,這個比值我們可以自己設定~之後

在type=file傳圖片限制大小、型別判斷、判斷

在專案中經常用到input標籤來上傳檔案,而這些檔案通常是圖片檔案。圖片有很多格式我們只需要其中的幾種,就需要對使用者上傳的檔案進行驗證,在HTML5中有一個新的屬性:accept檔案型別限制。但是通常我們會用javascript或jQuery編寫方法進行驗證圖片的大小限制、型別判斷、畫素判斷。

css 手機裝置適配

原文連結:github文章地址  曾幾何時為了相容IE低版本瀏覽器而頭痛,以為到Mobile時代可以跟這些麻煩說拜拜。可沒想到到了移動時代,為了處理各終端的適配而亂了手腳。對於混跡各社群的偶,時常發現大家拿手機淘寶的H5頁面做討論——手淘的H5頁面是如何實現多終端的適配? 那麼趁此

input type = file傳圖片限制大小、型別判斷、判斷

在專案中經常用到input標籤來上傳檔案,而這些檔案通常是圖片檔案。圖片有很多格式我們只需要其中的幾種,就需要對使用者上傳的檔案進行驗證,在HTML5中有一個新的屬性:accept檔案型別限制。但是通常我們會用javascript或jQuery編寫方法進行驗證圖片的大小限制、型別判斷、畫素判

岡薩雷斯:數字影象處理(二):第二章數字圖形基礎()——影象內插,相鄰,鄰接性,距離度量

1.影象內插:從根本上看,內插是用已知資料來估計未知位置的數值的處理。 例如,假設一幅大小為500500畫素的影象要放大1.5倍到75075畫素,一種簡單的放大方法是建立一個假想的750750網格,它與原始影象有相同的間隔,然後將其收縮,使它準確的與原影象匹配。顯然,收縮後的750750網格

螢幕用“*”一個空心的圓

#include <stdio.h> #include <math.h> int main() {     double y;     int x, m, r;          s

iPhone螢幕尺寸、邏輯解析度、物理解析度、密度 (2018)

新增 iPhoneXs、iPhoneXR、iPhoneXs Max的螢幕尺寸 裝置 iPhone 寬 Width 高 Hei

Android正確獲取螢幕和密度等

Android如何準確獲取螢幕寬高、密度。 需要注意在不同版本有點區別,4.2增加虛擬導航欄,導致舊的方法獲取的螢幕高度不包括導航欄。所以需要區分版本去獲取。 順便提下, 這貨WindowManager windowManager = activity.getWindo

[完] 取圖片面的顏色

只是用了好用,拿來分享,參考網址 如有侵權,請聯絡我刪除,謝謝開源的童鞋。   不熟悉的點總結: m = (char *)malloc(sizeof(char)); malloc() 動態分配記憶體,用malloc分配記憶體的首地址,然後賦值給變數m - (UICo

一種Android基於對比做基本圖案識別的簡單粗暴辦法

使用者希望通過書寫不規則的圖形之後,能夠識別出這個圖案是什麼,並輸出同等大小的規則圖形。第一步當然就是識別圖案了,我用的方法原理大致分如下幾步: 1、將使用者輸入的圖片長寬壓縮為目標大小,如100*100畫素 2、和圖案庫中預留的幾張預定大小(例如100*100畫素)的基礎圖案的畫素都進行一

Android螢幕轉換

import android.content.Context; import android.util.TypedValue; /** * Created by Administrator on 2017/6/2. */ public class DensityUtil { p

不同密度螢幕下實現1px邊框或分割線

1px邊框 //HTML <div class="one"></div> //CSS .one { position: relative; width: 100px; height: 100px; } .one::after { c

移動端web頁面知識小結之密度、解析度、螢幕尺寸

一直以來,本人對移動端解析度、畫素等概念傻傻分不清,特查閱多位牛人部落格後總結如下要點,以便日後檢視。 文件來源 螢幕解析度 是指螢幕上垂直方向和水平方向上的畫素個數,單位是px。常見取值 480X800,320X480等 IPhone手

檢查是否是圖片、限制、圖片傳時預覽、點選預覽圖在新頁面檢視原圖、傳圖片

在做網站後臺管理時,涉及到了圖片的上傳,經過多次改動後,上傳圖片的一系列步驟如下 1.在點選瀏覽按鈕時彈出框只出現圖片格式的檔案,可在input type=file中通過accept=".jpg,.png,.jpeg" 限制。 增加限制前: 增加限制後: 但在上圖示紅處選擇所有檔案時又會

3D UI 場景中如何把 X Y 平面的尺寸對映為螢幕

本次的分享總結所述的3D UI應用場景並非在遊戲中,而是注重在GUI應用上(類似QT等),即使用3D繪圖技術實現的一套類似2D UI一樣效果的引擎,由於UI系統是3D的,故能實現3D的動畫效果。把3D場景中的XY平面的尺寸對映為平面畫素一一對應的優點,是能保持並延續我們在2D開發時候的習慣,方便精準地控制U

根據判斷PC瀏覽器型別和手機螢幕自動呼叫不同CSS

php、asp、js判斷客戶端輸出對應的樣式 ------------------- 1.媒體查詢方法在 css 裡面這樣寫 -------------------- @media screen and (min-width: 320px) and (max-width: 480px){ 在這裡寫小

Android開發 螢幕適配之密度適配

由於市場上採用Android系統的裝置種類繁多,迫使Andriod開發人員不得不做煩人的適配工作。 適配工作包括對安裝不同Android版本的裝置進行適配,對不同螢幕的裝置進行適配等。 而螢幕適配又包括: 螢幕尺寸(small,normal,large,xlarge , 這

立體:Obelisk.js繪製等軸透視立體圖形(Isometric Graphics) 作者:Alex Young

(圖片說明:Obelisk.js現有的全套5種原型圖形(Brick、Cube、SideY、SideX與Pyramid)以及上色後的效果,未來將會有更多的isometric原型圖加入到引擎中。) 在建立等軸圖形(Isometric Graphics)的時候,由Max