繪制像素到屏幕上

繪制像素到屏幕上

answer-huang22 Mar 2014

分享文章

一個像素是如何繪制到屏幕上去的?有很多種方式將一些東西映射到顯示屏上,他們需要調用不同的框架、許多功能和方法的結合體。這里我們大概的看一下屏幕之后發生的事情。當你想要弄清楚什么時候、怎么去查明并解決問題時,我希望這篇文章能幫助你理解哪一個 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.5R = S + D* (1 - Sa) = 0? + 0 *(1 - 0.5) = 00? ? 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;[[UIColorredColor] 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, [UIColorredColor].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。(原來也是喬老爺的作品)。

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 作為他的內容,并且渲染服務將會把圖片的數據繪制到幀的緩沖區,比如,繪制到顯示屏。

在這種情況下,將不會繼續重新繪制。我們只是簡單的將位圖數據以圖片的形式傳給了 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的內容。下面我們將給出一個例子來測試,檢驗。

單一顏色

如果我們看這個例子:

// Don't do this- (void)drawRect:(CGRect)rect{? ? [[UIColorredColor] setFill];? ? UIRectFill([selfbounds]);}

現在我們知道這為什么不好:我們促使 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中討論過。(異步下載圖片經常用到這個)

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

- (UIImage*)renderInImageOfSize:(CGSize)size{? ? UIGraphicsBeginImageContextWithOptions(size,NO,0);// do drawing hereUIImage*result = UIGraphicsGetImageFromCurrentImageContext();? ? UIGraphicsEndImageContext();returnresult;}

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

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

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

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

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

要注意,我們是在主隊列中調用 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 的操作都不會被執行。取而代之的是,繪制命令被推遲,并且在后臺線程中異步執行。

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

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

話題 #3 下的更多文章

原文Getting Pixels onto the Screen

譯文將像素繪制到屏幕上去 - answer-huang

關于譯者

answer-huang

iOS開發者,Python愛好者,創業公司創業中(康大預診)。個人博客:

http://answerhuang.duapp.com/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,923評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,740評論 3 420
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,856評論 0 380
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,175評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,931評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,321評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,383評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,533評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,082評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,891評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,618評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,319評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,732評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,987評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,794評論 3 394
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,076評論 2 375

推薦閱讀更多精彩內容

  • 1.繪制像素到屏幕上 軟件的組成 GPU :圖形處理單元。更多圖像的繪制CPU:圖形高迸發計算而量身定做的處理單元...
    曲年閱讀 448評論 0 0
  • 卷首語 歡迎來到 objc.io 的第三期! 這一期都是關于視圖層的。當然視圖層有很多方面,我們需要把它們縮小到幾...
    評評分分閱讀 1,793評論 0 18
  • 像素是如何顯示在屏幕上的呢? 當然這里有很多種方式將某些東西顯示到顯示器上面,并且它們可能涉及到許多不同的fram...
    樗同學閱讀 1,796評論 1 3
  • 有很多種framework以及很多種方法的組合可以在屏幕上渲染UI元素,我們在這里討論這個過程中發生的事情,希望這...
    縱橫而樂閱讀 4,504評論 4 25
  • 王朗明白,自己并不是梁甜的最佳選擇! 婚禮,原本應該是充滿甜蜜的場合,梁甜卻一直是一張冰霜臉,偶爾為應付場面露出的...
    天黎琉璃閱讀 668評論 6 7