一個像素是如何繪制到屏幕上去的?有很多種方式將一些東西映射到顯示屏上,他們需要調(diào)用不同的框架、許多功能和方法的結(jié)合體。這里我們大概的看一下屏幕之后發(fā)生的事情。當你想要弄清楚什么時候、怎么去查明并解決問題時,我希望這篇文章能幫助你理解哪一個 API 可以更好的幫你解決問題。我們將聚焦于 iOS,然而我討論的大多數(shù)問題也同樣適用于 OS X。
當像素映射到屏幕上的時候,后臺發(fā)生了很多事情。但一旦他們顯示到屏幕上,每一個像素均由三個顏色組件構(gòu)成:紅,綠,藍。三個獨立的顏色單元會根據(jù)給定的顏色顯示到一個像素上。在 iPhone5 的液晶顯示器上有1,136×640=727,040個像素,因此有2,181,120個顏色單元。在15寸視網(wǎng)膜屏的 MacBook Pro 上,這一數(shù)字達到15.5百萬以上。所有的圖形堆棧一起工作以確保每次正確的顯示。當你滾動整個屏幕的時候,數(shù)以百萬計的顏色單元必須以每秒60次的速度刷新,這是一個很大的工作量。
從簡單的角度來看,軟件堆棧看起來有點像這樣:
http://wiki.jikexueyuan.com/project/objc/images/3-1.png" alt="軟件堆棧" />
Display 的上一層便是圖形處理單元 GPU,GPU 是一個專門為圖形高迸發(fā)計算而量身定做的處理單元。這也是為什么它能同時更新所有的像素,并呈現(xiàn)到顯示器上。它迸發(fā)的本性讓它能高效的將不同紋理合成起來。我們將有一小塊內(nèi)容來更詳細的討論圖形合成。關(guān)鍵的是,GPU 是非常專業(yè)的,因此在某些工作上非常高效。比如,GPU 非常快,并且比 CPU 使用更少的電來完成工作。通常 CPU 都有一個普遍的目的,它可以做很多不同的事情,但是合成圖像在 CPU 上卻顯得比較慢。
GPU Driver 是直接和 GPU 交流的代碼塊。不同的GPU是不同的性能怪獸,但是驅(qū)動使他們在下一個層級上顯示的更為統(tǒng)一,典型的驅(qū)動有 OpenGL/OpenGL ES.
OpenGL(Open Graphics Library) 是一個提供了 2D 和 3D 圖形渲染的 API。GPU 是一塊非常特殊的硬件,OpenGL 和 GPU 密切的工作以提高GPU的能力,并實現(xiàn)硬件加速渲染。對大多數(shù)人來說,OpenGL 看起來非常底層,但是當它在1992年第一次發(fā)布的時候(20多年前的事了)是第一個和圖形硬件(GPU)交流的標準化方式,這是一個重大的飛躍,程序員不再需要為每個GPU重寫他們的應(yīng)用了。
OpenGL 之上擴展出很多東西。在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,然而在 OS X 上,繞過 Core Animation 直接使用 Core Graphics 繪制的情況并不少見。對于一些專門的應(yīng)用,尤其是游戲,程序可能直接和 OpenGL/OpenGL ES 交流。事情變得使人更加困惑,因為 Core Animation 使用 Core Graphics 來做一些渲染。像 AVFoundation,Core Image 框架,和其他一些混合的入口。
要記住一件事情,GPU 是一個非常強大的圖形硬件,并且在顯示像素方面起著核心作用。它連接到 CPU。從硬件上講兩者之間存在某種類型的總線,并且有像 OpenGL,Core Animation 和 Core Graphics 這樣的框架來在 GPU 和 CPU 之間精心安排數(shù)據(jù)的傳輸。為了將像素顯示到屏幕上,一些處理將在 CPU 上進行。然后數(shù)據(jù)將會傳送到 GPU,這也需要做一些相應(yīng)的操作,最終像素顯示到屏幕上。
這個過程的每一部分都有各自的挑戰(zhàn),并且許多時候需要做出折中的選擇。
http://wiki.jikexueyuan.com/project/objc/images/3-2.png" alt="挑戰(zhàn)" />
正如上面這張簡單的圖片顯示那些挑戰(zhàn):GPU 需要將每一個 frame 的紋理(位圖)合成在一起(一秒60次)。每一個紋理會占用 VRAM(video RAM),所以需要給 GPU 同時保持紋理的數(shù)量做一個限制。GPU 在合成方面非常高效,但是某些合成任務(wù)卻比其他更復雜,并且 GPU在 16.7ms(1/60s)內(nèi)能做的工作也是有限的。
下一個挑戰(zhàn)就是將數(shù)據(jù)傳輸?shù)?GPU 上。為了讓 GPU 訪問數(shù)據(jù),需要將數(shù)據(jù)從 RAM 移動到 VRAM 上。這就是提及到的上傳數(shù)據(jù)到 GPU。這看起來貌似微不足道,但是一些大型的紋理卻會非常耗時。
最終,CPU 開始運行你的程序。你可能會讓 CPU 從 bundle 加載一張 PNG 的圖片并且解壓它。這所有的事情都在 CPU 上進行。然后當你需要顯示解壓縮后的圖片時,它需要以某種方式上傳到 GPU。一些看似平凡的,比如顯示文本,對 CPU 來說卻是一件非常復雜的事情,這會促使 Core Text 和 Core Graphics 框架更緊密的集成來根據(jù)文本生成一個位圖。一旦準備好,它將會被作為一個紋理上傳到 GPU 并準備顯示出來。當你滾動或者在屏幕上移動文本時,不管怎么樣,同樣的紋理能夠被復用,CPU 只需簡單的告訴 GPU 新的位置就行了,所以 GPU 就可以重用存在的紋理了。CPU 并不需要重新渲染文本,并且位圖也不需要重新上傳到 GPU。
這張圖涉及到一些錯綜復雜的方面,我們將會把這些方面提取出來并深一步了解。
在圖形世界中,合成是一個描述不同位圖如何放到一起來創(chuàng)建你最終在屏幕上看到圖像的過程。在許多方面顯得顯而易見,而讓人忘了背后錯綜復雜的計算。
讓我們忽略一些難懂的事例并且假定屏幕上一切事物皆紋理。一個紋理就是一個包含 RGBA 值的長方形,比如,每一個像素里面都包含紅、綠、藍和透明度的值。在 Core Animation 世界中這就相當于一個 CALayer。
在這個簡化的設(shè)置中,每一個 layer 是一個紋理,所有的紋理都以某種方式堆疊在彼此的頂部。對于屏幕上的每一個像素,GPU 需要算出怎么混合這些紋理來得到像素 RGB 的值。這就是合成大概的意思。
如果我們所擁有的是一個和屏幕大小一樣并且和屏幕像素對齊的單一紋理,那么屏幕上每一個像素相當于紋理中的一個像素,紋理的最后一個像素也就是屏幕的最后一個像素。
如果我們有第二個紋理放在第一個紋理之上,然后GPU將會把第二個紋理合成到第一個紋理中。有很多種不同的合成方法,但是如果我們假定兩個紋理的像素對齊,并且使用正常的混合模式,我們便可以用下面這個公式來計算每一個像素:
R = S + D * ( 1 – Sa )
結(jié)果的顏色是源色彩(頂端紋理)+目標顏色(低一層的紋理)*(1-源顏色的透明度)。在這個公式中所有的顏色都假定已經(jīng)預先乘以了他們的透明度。
顯然相當多的事情在這發(fā)生了。讓我們進行第二個假定,兩個紋理都完全不透明,比如 alpha=1.如果目標紋理(低一層的紋理)是藍色(RGB=0,0,1),并且源紋理(頂層的紋理)顏色是紅色(RGB=1,0,0),因為 Sa 為1,所以結(jié)果為:
R = S
結(jié)果是源顏色的紅色。這正是我們所期待的(紅色覆蓋了藍色)。
如果源顏色層為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是一塊高度優(yōu)化的硬件來做這種事情,但這還是會讓它非常忙碌,
當源紋理是完全不透明的時候,目標像素就等于源紋理。這可以省下 GPU 很大的工作量,這樣只需簡單的拷貝源紋理而不需要合成所有的像素值。但是沒有方法能告訴 GPU 紋理上的像素是透明還是不透明的。只有當你作為一名開發(fā)者知道你放什么到 CALayer 上了。這也是為什么 CALayer 有一個叫做 opaque 的屬性了。如果這個屬性為 YES,GPU 將不會做任何合成,而是簡單從這個層拷貝,不需要考慮它下方的任何東西(因為都被它遮擋住了)。這節(jié)省了 GPU 相當大的工作量。這也正是 Instruments 中 color blended layers 選項中所涉及的。(這在模擬器中的Debug菜單中也可用).它允許你看到哪一個 layers(紋理) 被標注為透明的,比如 GPU 正在為哪一個 layers 做合成。合成不透明的 layers 因為需要更少的數(shù)學計算而更廉價。
所以如果你知道你的 layer 是不透明的,最好確定設(shè)置它的 opaque 為 YES。如果你加載一個沒有 alpha 通道的圖片,并且將它顯示在 UIImageView 上,這將會自動發(fā)生。但是要記住如果一個圖片沒有 alpha 通道和一個圖片每個地方的 alpha 都是100%,這將會產(chǎn)生很大的不同。在后一種情況下,Core Animation 需要假定是否存在像素的 alpha 值不為100%。在 Finder 中,你可以使用 Get Info 并且檢查 More Info 部分。它將告訴你這張圖片是否擁有 alpha 通道。
到現(xiàn)在我們都在考慮像素完美重合在一起的 layers。當所有的像素是對齊的時候我們得到相對簡單的計算公式。每當 GPU 需要計算出屏幕上一個像素是什么顏色的時候,它只需要考慮在這個像素之上的所有 layer 中對應(yīng)的單個像素,并把這些像素合并到一起?;蛘?,如果最頂層的紋理是不透明的(即圖層樹的最底層),這時候 GPU 就可以簡單的拷貝它的像素到屏幕上。
當一個 layer 上所有的像素和屏幕上的像素完美的對應(yīng)整齊,那這個 layer 就是像素對齊的。主要有兩個原因可能會造成不對齊。第一個便是滾動;當一個紋理上下滾動的時候,紋理的像素便不會和屏幕的像素排列對齊。另一個原因便是當紋理的起點不在一個像素的邊界上。
在這兩種情況下,GPU 需要再做額外的計算。它需要將源紋理上多個像素混合起來,生成一個用來合成的值。當所有的像素都是對齊的時候,GPU 只剩下很少的工作要做。
Core Animation 工具和模擬器有一個叫做 color misaligned images 的選項,當這些在你的 CALayer 實例中發(fā)生的時候,這個功能便可向你展示。
一個圖層可以有一個和它相關(guān)聯(lián)的 mask(蒙板),mask 是一個擁有 alpha 值的位圖,當像素要和它下面包含的像素合并之前都會把 mask 應(yīng)用到圖層的像素上去。當你要設(shè)置一個圖層的圓角半徑時,你可以有效的在圖層上面設(shè)置一個 mask。但是也可以指定任意一個蒙板。比如,一個字母 A 形狀的 mask。最終只有在 mask 中顯示出來的(即圖層中的部分)才會被渲染出來。
離屏渲染可以被 Core Animation 自動觸發(fā),或者被應(yīng)用程序強制觸發(fā)。屏幕外的渲染會合并/渲染圖層樹的一部分到一個新的緩沖區(qū),然后該緩沖區(qū)被渲染到屏幕上。
離屏渲染合成計算是非常昂貴的, 但有時你也許希望強制這種操作。一種好的方法就是緩存合成的紋理/圖層。如果你的渲染樹非常復雜(所有的紋理,以及如何組合在一起),你可以強制離屏渲染緩存那些圖層,然后可以用緩存作為合成的結(jié)果放到屏幕上。
如果你的程序混合了很多圖層,并且想要他們一起做動畫,GPU 通常會為每一幀(1/60s)重復合成所有的圖層。當使用離屏渲染時,GPU 第一次會混合所有圖層到一個基于新的紋理的位圖緩存上,然后使用這個紋理來繪制到屏幕上。現(xiàn)在,當這些圖層一起移動的時候,GPU 便可以復用這個位圖緩存,并且只需要做很少的工作。需要注意的是,只有當那些圖層不改變時,這才可以用。如果那些圖層改變了,GPU 需要重新創(chuàng)建位圖緩存。你可以通過設(shè)置 shouldRasterize 為 YES 來觸發(fā)這個行為。
然而,這是一個權(quán)衡。第一,這可能會使事情變得更慢。創(chuàng)建額外的屏幕外緩沖區(qū)是 GPU 需要多做的一步操作,特殊情況下這個位圖可能再也不需要被復用,這便是一個無用功了。然而,可以被復用的位圖,GPU 也有可能將它卸載了。所以你需要計算 GPU 的利用率和幀的速率來判斷這個位圖是否有用。
離屏渲染也可能產(chǎn)生副作用。如果你正在直接或者間接的將mask應(yīng)用到一個圖層上,Core Animation 為了應(yīng)用這個 mask,會強制進行屏幕外渲染。這會對 GPU 產(chǎn)生重負。通常情況下 mask 只能被直接渲染到幀的緩沖區(qū)中(在屏幕內(nèi))。
Instrument 的 Core Animation 工具有一個叫做 Color Offscreen-Rendered Yellow 的選項,它會將已經(jīng)被渲染到屏幕外緩沖區(qū)的區(qū)域標注為黃色(這個選項在模擬器中也可以用)。同時記得檢查 Color Hits Green and Misses Red 選項。綠色代表無論何時一個屏幕外緩沖區(qū)被復用,而紅色代表當緩沖區(qū)被重新創(chuàng)建。
一般情況下,你需要避免離屏渲染,因為這是很大的消耗。直接將圖層合成到幀的緩沖區(qū)中(在屏幕上)比先創(chuàng)建屏幕外緩沖區(qū),然后渲染到紋理中,最后將結(jié)果渲染到幀的緩沖區(qū)中要廉價很多。因為這其中涉及兩次昂貴的環(huán)境轉(zhuǎn)換(轉(zhuǎn)換環(huán)境到屏幕外緩沖區(qū),然后轉(zhuǎn)換環(huán)境到幀緩沖區(qū))。
所以當你打開 Color Offscreen-Rendered Yellow 后看到黃色,這便是一個警告,但這不一定是不好的。如果 Core Animation 能夠復用屏幕外渲染的結(jié)果,這便能夠提升性能。
同時還要注意,rasterized layer 的空間是有限的。蘋果暗示大概有屏幕大小兩倍的空間來存儲 rasterized layer/屏幕外緩沖區(qū)。
如果你使用 layer 的方式會通過屏幕外渲染,你最好擺脫這種方式。為 layer 使用蒙板或者設(shè)置圓角半徑會造成屏幕外渲染,產(chǎn)生陰影也會如此。
至于 mask,圓角半徑(特殊的mask)和 clipsToBounds/masksToBounds,你可以簡單的為一個已經(jīng)擁有 mask 的 layer 創(chuàng)建內(nèi)容,比如,已經(jīng)應(yīng)用了 mask 的 layer 使用一張圖片。如果你想根據(jù) layer 的內(nèi)容為其應(yīng)用一個長方形 mask,你可以使用 contentsRect 來代替蒙板。
如果你最后設(shè)置了 shouldRasterize 為 YES,那也要記住設(shè)置 rasterizationScale 為 contentsScale。
像往常一樣,維基百科上有更多關(guān)于透明合成的基礎(chǔ)公式。當我們談完像素后,我們將更深入一點的談?wù)摷t,綠,藍和 alpha 是怎么在內(nèi)存中表現(xiàn)的。
如果你是在 OS X 上工作,你將會發(fā)現(xiàn)大多數(shù) debugging 選項在一個叫做 Quartz Debug 的獨立程序中,而不是在 Instruments 中。Quartz Debug 是 Graphics Tools 中的一部分,這可以在蘋果的 developer portal 中下載到。
正如名字所建議的那樣,Core Animation 讓你在屏幕上實現(xiàn)動畫。我們將跳過動畫部分,而集中在繪圖上。需要注意的是,Core Animation 允許你做非常高效的渲染。這也是為什么當你使用 Core Animation 時可以實現(xiàn)每秒 60 幀的動畫。
Core Animation 的核心是 OpenGL ES 的一個抽象物,簡而言之,它讓你直接使用 OpenGL ES 的功能,卻不需要處理 OpenGL ES 做的復雜的事情。當我們上面談?wù)摵铣傻臅r候,我們把 layer 和 texture 當做等價的,但是他們不是同一物體,可又是如此的類似。
Core Animation 的 layer 可以有子 layer,所以最終你得到的是一個圖層樹。Core Animation 所需要做的最繁重的任務(wù)便是判斷出哪些圖層需要被(重新)繪制,而 OpenGL ES 需要做的便是將圖層合并、顯示到屏幕上。
舉個例子,當你設(shè)置一個 layer 的內(nèi)容為 CGImageRef 時,Core Animation 會創(chuàng)建一個 OpenGL 紋理,并確保在這個圖層中的位圖被上傳到對應(yīng)的紋理中。以及當你重寫 -drawInContext 方法時,Core Animation 會請求分配一個紋理,同時確保 Core Graphics 會將你所做的(即你在drawInContext中繪制的東西)放入到紋理的位圖數(shù)據(jù)中。一個圖層的性質(zhì)和 CALayer 的子類會影響到 OpenGL 的渲染結(jié)果,許多低等級的 OpenGL ES 行為被簡單易懂地封裝到 CALayer 概念中。
Core Animation 通過 Core Graphics 的一端和 OpenGL ES 的另一端,精心策劃基于 CPU 的位圖繪制。因為 Core Animation 處在渲染過程中的重要位置上,所以你如何使用 Core Animation 將會對性能產(chǎn)生極大的影響。
當你在屏幕上顯示東西的時候,有許多組件參與了其中的工作。其中,CPU 和 GPU 在硬件中扮演了重要的角色。在他們命名中 P 和 U 分別代表了”處理”和”單元”,當需要在屏幕上進行繪制時,他們都需要做處理,同時他們都有資源限制(即 CPU 和 GPU 的硬件資源)。
為了每秒達到 60 幀,你需要確定 CPU 和 GPU 不能過載。此外,即使你當前能達到 60fps(frame per second),你還是要盡可能多的繪制工作交給 GPU 做,而讓 CPU 盡可能的來執(zhí)行應(yīng)用程序。通常,GPU 的渲染性能要比 CPU 高效很多,同時對系統(tǒng)的負載和消耗也更低一些。
既然繪圖性能是基于 CPU 和 GPU 的,那么你需要找出是哪一個限制你繪圖性能的。如果你用盡了 GPU 所有的資源,也就是說,是 GPU 限制了你的性能,同樣的,如果你用盡了 CPU,那就是 CPU 限制了你的性能。
要告訴你,如果是 GPU 限制了你的性能,你可以使用 OpenGL ES Driver instrument。點擊上面那個小的 i 按鈕,配置一下,同時注意查看 Device Utilization %。現(xiàn)在,當你運行你的 app 時,你可以看到你 GPU 的負荷。如果這個值靠近 100%,那么你就需要把你工作的重心放在GPU方面了。
通過 Core Graphics 這個框架,Quartz 2D 被更為廣泛的知道。
Quartz 2D 擁有比我們這里談到更多的裝飾。我們這里不會過多的討論關(guān)于 PDF 的創(chuàng)建,渲染,解析,或者打印。只需要注意的是,PDF 的打印、創(chuàng)建和在屏幕上繪制位圖的操作是差不多的。因為他們都是基于 Quartz 2D。
讓我們簡單的了解一下 Quartz 2D 主要的概念。有關(guān)詳細信息可以到蘋果的官方文檔中了解。
放心,當Quartz 2D 涉及到 2D 繪制的時候,它是非常強大的。有基于路徑的繪制,反鋸齒渲染,透明圖層,分辨率,并且設(shè)備獨立,可以說出很多特色。這可能會讓人產(chǎn)生畏懼,主要因為這是一個低級并且基于 C 的 API。
主要的概念當對簡單,UIKit 和 AppKit 都包含了 Quartz 2D 的一些簡單 API,一旦你熟練了,一些簡單 C 的 API 也是很容易理解的。最終你學會了一個能實現(xiàn) Photoshop 和 Illustrator 大部分功能的繪圖引擎。蘋果把 iOS 程序里面的股票應(yīng)用作為講解 Quartz 2D 在代碼中實現(xiàn)動態(tài)渲染的一個例子。
當你的程序進行位圖繪制時,不管使用哪種方式,都是基于 Quartz 2D 的。也就是說,CPU 部分實現(xiàn)的繪制是通過 Quartz 2D 實現(xiàn)的。盡管 Quartz 可以做其它的事情,但是我們這里還是集中于位圖繪制,在緩沖區(qū)(一塊內(nèi)存)繪制位圖會包括 RGBA 數(shù)據(jù)。
比方說,我們要畫一個八角形,我們通過 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];
相對應(yīng)的 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參數(shù)正是在那個上下文中。而這個上下文定義了我們需要繪制的地方。如果我們實現(xiàn)了 CALayer 的 -drawInContext: 這時已經(jīng)傳過來一個上下文。繪制到這個上下文中的內(nèi)容將會被繪制到圖層的備份區(qū)(圖層的緩沖區(qū)).但是我們也可以創(chuàng)建我們自己的上下文,叫做基于位圖的上下文,比如 CGBitmapContextCreate().這個方法返回一個我們可以傳給 CGContext 方法來繪制的上下文。
注意 UIKit 版本的代碼為何不傳入一個上下文參數(shù)到方法中?這是因為當使用 UIKit 或者 AppKit 時,上下文是唯一的。UIkit 維護著一個上下文堆棧,UIKit 方法總是繪制到最頂層的上下文中。你可以使用 UIGraphicsGetCurrentContext() 來得到最頂層的上下文。你可以使用 UIGraphicsPushContext() 和 UIGraphicsPopContext() 在 UIKit 的堆棧中推進或取出上下文。
最為突出的是,UIKit 使用 UIGraphicsBeginImageContextWithOptions() 和 UIGraphicsEndImageContext() 方便的創(chuàng)建類似于 CGBitmapContextCreate() 的位圖上下文。混合調(diào)用 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 創(chuàng)建大量的非??岬臇|西。一個很好的理由就是,蘋果的文檔有很多例子。我們不能得到所有的細節(jié),但是 Core Graphics 有一個非常接近 Adobe Illustrator 和 Adobe Photoshop 如何工作的繪圖模型,并且大多數(shù)工具的理念翻譯成 Core Graphics 了。終究,他是起源于 NeXTSTEP 。(原來也是喬老爺?shù)淖髌?。
我們最初指出 CGLayer 可以用來提升重復繪制相同元素的速度。正如 Dave Hayden指出,這些小道消息不再可靠。
屏幕上的像素是由紅,綠,藍三種顏色組件構(gòu)成的。因此,位圖數(shù)據(jù)有時也被叫做 RGB 數(shù)據(jù)。你可能會對數(shù)據(jù)如何組織在內(nèi)存中感到好奇。而事實是,有很多種不同的方式在內(nèi)存中展現(xiàn)RGB位圖數(shù)據(jù)。
稍后我們將會談到壓縮數(shù)據(jù),這又是一個完全不同的概念?,F(xiàn)在,我們先看一下RGB位圖數(shù)據(jù),我們可以從顏色組件:紅,綠,藍中得到一個值。而大多數(shù)情況下,我們有第四個組件:透明度。最終我們從每個像素中得到四個單獨的值。
在 iOS 和 OS X 上最常見的格式就是大家所熟知的 32bits-per-pixel(bpp), 8bits-per-componet(bpc),透明度會首先被乘以到像素值上(就像上文中提到的那個公式一樣),在內(nèi)存中,像下面這樣:
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 ...
這個格式經(jīng)常被叫做 ARGB。每個像素占用 4 字節(jié)(32bpp),每一個顏色組件是1字節(jié)(8bpc).每個像素有一個 alpha 值,這個值總是最先得到的(在RGB值之前),最終紅、綠、藍的值都會被預先乘以 alpha 的值。預乘的意思就是 alpha 值被烘烤到紅、綠、藍的組件中。如果我們有一個橙色,他們各自的 8bpc 就像這樣: 240,99,24.一個完全不透明的橙色像素擁有的 ARGB 值為: 255,240,99,24,它在內(nèi)存中的布局就像上面圖示那樣。如果我們有一個相同顏色的像素,但是 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%不透明),但是內(nèi)存布局是一樣的。你應(yīng)該想知道為什么這種格式很流行,當我們每一個像素中都有一個不用字節(jié)時,我們將會省下 25% 的空間。事實證明,這種格式更容易被現(xiàn)代的 CPU 和繪圖算法消化,因為每一個獨立的像素都對齊到 32-bit 的邊界?,F(xiàn)代的 CPU 不喜歡裝載(讀取)不對齊的數(shù)據(jù),特別是當將這種數(shù)據(jù)和上面沒有 alpha 值格式的數(shù)據(jù)混合時,算法需要做很多挪動和蒙板操作。
當處理 RGB 數(shù)據(jù)時,Core Graphics 也需要支持把alpha 值放到最后(另外還要支持跳過)。有時候也分別稱為 RGBA 和 RGBx,假定是 8bpc,并且預乘了 alpha 值。
大多數(shù)時候,當處理位圖數(shù)據(jù)時,我們也需要處理 Core Graphics/Quartz 2D。有一個非常詳細的列表列出了他支持的混合組合。但是讓我們首先看一下剩下的 RGB 格式:
另一個選擇是 16bpp,5bpc,不包含 alpha 值。這個格式相比之前一個僅占用 50% 的存儲大小(每個像素2字節(jié)),但將使你存儲它的 RGB 數(shù)據(jù)到內(nèi)存或磁盤中變得困難。既然這種格式中,每個顏色組件只有 5bits(原文中寫的是每個像素是5bits,但根據(jù)上下文可知應(yīng)該是每個組件),這樣圖形(特別是平滑漸變的)會造成重疊在一起的假象。
還有一個是 64bpp,16bpc,最終為 128bpp,32bpc,浮點數(shù)組件(有或沒有 alpha 值)。它們分別使用 8 字節(jié)和 16 字節(jié),并且允許更高的精度。當然,這會造成更多的內(nèi)存使用和昂貴的計算。
整件事件中,Core Graphics 也支持一些像灰度模式和 CMYK 格式,這些格式類似于僅有 alpha 值的格式(蒙板)。
當顏色組件(紅、綠、藍、alpha)混雜在一起的時候,大多數(shù)框架(包括 Core Graphics )使用像素數(shù)據(jù)。正是這種情況下我們稱之為二維數(shù)據(jù),或者二維組件。這個意思是:每一個顏色組件都在它自己的內(nèi)存區(qū)域,也就是說它是二維的。比如 RGB 數(shù)據(jù),我們有三個獨立的內(nèi)存區(qū)域,一個大的區(qū)域包含了所有像素的紅顏色的值,一個包含了所有綠顏色的值,一個包含了所有藍顏色的值。
在某些情況下,一些視頻框架便會使用二維數(shù)據(jù)。
當我們處理視頻數(shù)據(jù)時,YCbCr 是一種常見的格式。它也是包含了三種(Y,Cb和Cr)代表顏色數(shù)據(jù)的組件。但是簡單的講,它更類似于通過人眼看到的顏色。人眼對 Cb 和 Cr 這兩種組件的色彩度不太能精確的辨認出來,但是能很準確的識別出 Y 的亮度。當數(shù)據(jù)使用 YCbCr 格式時,在同等的條件下,Cb 和 Cr 組件比 Y 組件壓縮的更緊密。
出于同樣的原因,JPEG 圖像有時會將像素數(shù)據(jù)從 RGB 轉(zhuǎn)換到 YCbCr。JPEG 單獨的壓縮每一個二維顏色。當壓縮基于 YCbCr 的平面時,Cb 和 Cr 能比 Y 壓縮得更完全。
當你在 iOS 或者 OS X 上處理圖片時,他們大多數(shù)為 JPEG 和 PNG。讓我們更進一步觀察。
每個人都知道 JPEG。他是相機的產(chǎn)物。它代表這照片如何存儲在電腦上。甚至你嘛嘛都聽說過 JPEG。
一個很好的理由,很多人都認為 JPEG 文件僅是另一種像素數(shù)據(jù)的格式,就像我們剛剛談到的 RGB 像素布局那樣。這樣理解離真像真是差十萬八千里了。
將 JPEG 數(shù)據(jù)轉(zhuǎn)換成像素數(shù)據(jù)是一個非常復雜的過程,你通過一個周末的計劃都不能完成,甚至是一個非常漫長的周末(原文的意思好像就是為了表達這個過程非常復雜,不過老外的比喻總讓人拎不清)。對于每一個二維顏色,JPEG 使用一種基于離散余弦變換(簡稱 DCT 變換)的算法,將空間信息轉(zhuǎn)變到頻域.這個信息然后被量子化,排好序,并且用一種哈夫曼編碼的變種來壓縮。很多時候,首先數(shù)據(jù)會被從 RGB 轉(zhuǎn)換到二維 YCbCr,當解碼 JPEG 的時候,這一切都將變得可逆。
這也是為什么當你通過 JPEG 文件創(chuàng)建一個 UIImage 并且繪制到屏幕上時,將會有一個延時,因為 CPU 這時候忙于解壓這個 JPEG。如果你需要為每一個 tableviewcell 解壓 JPEG,那么你的滾動當然不會平滑(原來 tableviewcell 里面最要不要用 JPEG 的圖片)。
那究竟為什么我們還要用 JPEG 呢?答案就是 JPEG 可以非常非常好的壓縮圖片。一個通過 iPhone5 拍攝的,未經(jīng)壓縮的圖片占用接近 24M。但是通過默認壓縮設(shè)置,你的照片通常只會在 2-3M 左右。JPEG 壓縮這么好是因為它是失真的,它去除了人眼很難察覺的信息,并且這樣做可以超出像 gzip 這樣壓縮算法的限制。但這僅僅在圖片上有效的,因為 JPEG 依賴于圖片上有很多人類不能察覺出的數(shù)據(jù)。如果你從一個基本顯示文本的網(wǎng)頁上截取一張圖,JPEG 將不會這么高效。壓縮效率將會變得低下,你甚至能看出來圖片已經(jīng)壓縮變形了。
PNG讀作”ping”。和 JPEG 相反,它的壓縮對格式是無損的。當你將一張圖片保存為 PNG,并且打開它(或解壓),所有的像素數(shù)據(jù)會和最初一模一樣,因為這個限制,PNG 不能像 JPEG 一樣壓縮圖片,但是對于像程序中的原圖(如buttons,icons),它工作的非常好。更重要的是,解碼 PNG 數(shù)據(jù)比解碼 JPEG 簡單的多。
在現(xiàn)實世界中,事情從來沒有那么簡單,目前存在了大量不同的 PNG 格式。可以通過維基百科查看詳情。但是簡言之,PNG 支持壓縮帶或不帶 alpha 通道的顏色像素(RGB),這也是為什么它在程序原圖中表現(xiàn)良好的另一個原因。
當你在你的程序中使用圖片時,你需要堅持這兩種格式: JPEG 或者 PNG。讀寫這種格式文件的壓縮和解壓文件能表現(xiàn)出很高的性能,另外,還支持并行操作。同時 Apple 正在改進解壓縮并可能出現(xiàn)在將來的新操作系統(tǒng)中,屆時你將會得到持續(xù)的性能提升。如果嘗試使用另一種格式,你需要注意到,這可能對你程序的性能會產(chǎn)生影響,同時可能會打開安全漏洞,經(jīng)常,圖像解壓縮算法是黑客最喜歡的攻擊目標。
已經(jīng)寫了很多關(guān)于優(yōu)化 PNGs,如果你想要了解更多,請到互聯(lián)網(wǎng)上查詢。非常重要的一點,注意 Xcode 優(yōu)化 PNG 選項和優(yōu)化其他引擎有很大的不同。
當 Xcode 優(yōu)化一個 PNG 文件的時候,它將 PNG 文件變成一個從技術(shù)上講不再是有效的PNG文件。但是 iOS 可以讀取這種文件,并且這比解壓縮正常的 PNG 文件更快。Xcode 改變他們,讓 iOS 通過一種對正常 PNG 不起作用的算法來對他們解壓縮。值得注意的重點是,這改變了像素的布局。正如我們所提到的一樣,在像素之下有很多種方式來描繪 RGB 數(shù)據(jù),如果這不是 iOS 繪制系統(tǒng)所需要的格式,它需要將每一個像素的數(shù)據(jù)替換,而不需要加速來做這件事。
讓我們再強調(diào)一遍,如果你可以,你需要為原圖設(shè)置 resizable images。你的文件將變得更小,因此你只需要從文件系統(tǒng)裝載更少的數(shù)據(jù)。
每一個在 UIKit 中的 view 都有它自己的 CALayer。依次,這些圖層都有一個叫像素位圖的后備存儲,有點像一個圖像。這個后備存儲正是被渲染到顯示器上的。
如果你的視圖類實現(xiàn)了 -drawRect:,他們將像這樣工作:
當你調(diào)用 -setNeedsDisplay,UIKit 將會在這個視圖的圖層上調(diào)用 -setNeedsDisplay。這為圖層設(shè)置了一個標識,標記為 dirty(直譯是臟的意思,想不出用什么詞比較貼切,污染?),但還顯示原來的內(nèi)容。它實際上沒做任何工作,所以多次調(diào)用 -setNeedsDisplay并不會造成性能損失。
下面,當渲染系統(tǒng)準備好,它會調(diào)用視圖圖層的-display方法.此時,圖層會裝配它的后備存儲。然后建立一個 Core Graphics 上下文(CGContextRef),將后備存儲對應(yīng)內(nèi)存中的數(shù)據(jù)恢復出來,繪圖會進入對應(yīng)的內(nèi)存區(qū)域,并使用 CGContextRef 繪制。
當你使用 UIKit 的繪制方法,例如: UIRectFill() 或者 -[UIBezierPath fill] 代替你的 -drawRect: 方法,他們將會使用這個上下文。使用方法是,UIKit 將后備存儲的 CGContextRef 推進他的 graphics context stack,也就是說,它會將那個上下文設(shè)置為當前的。因此 UIGraphicsGetCurrent() 將會返回那個對應(yīng)的上下文。既然 UIKit 使用 UIGraphicsGetCurrent() 繪制方法,繪圖將會進入到圖層的后備存儲。如果你想直接使用 Core Graphics 方法,你可以自己調(diào)用 UIGraphicsGetCurrent() 得到相同的上下文,并且將這個上下文傳給 Core Graphics 方法。
從現(xiàn)在開始,圖層的后備存儲將會被不斷的渲染到屏幕上。直到下次再次調(diào)用視圖的 -setNeedsDisplay ,將會依次將圖層的后備存儲更新到視圖上。
當你用一個 UIImageView 時,事情略有不同,這個視圖仍然有一個 CALayer,但是圖層卻沒有申請一個后備存儲。取而代之的是使用一個 CGImageRef 作為他的內(nèi)容,并且渲染服務(wù)將會把圖片的數(shù)據(jù)繪制到幀的緩沖區(qū),比如,繪制到顯示屏。
在這種情況下,將不會繼續(xù)重新繪制。我們只是簡單的將位圖數(shù)據(jù)以圖片的形式傳給了 UIImageView,然后 UIImageView 傳給了 Core Animation,然后輪流傳給渲染服務(wù)。
這聽起來貌似有點低俗,但是最快的繪制就是你不要做任何繪制。
大多數(shù)時間,你可以不要合成你在其他視圖(圖層)上定制的視圖(圖層),這正是我們推薦的,因為 UIKit 的視圖類是非常優(yōu)化的 (就是讓我們不要閑著沒事做,自己去合并視圖或圖層) 。
當你需要自定義繪圖代碼時,Apple 在WWDC 2012’s session 506:Optimizing 2D Graphics and Animation Performance 中展示了一個很好的例子:”finger painting”。
另一個地方需要自定義繪圖的就是 iOS 的股票軟件。股票是直接用 Core Graphics 在設(shè)備上繪制的,注意,這僅僅是你需要自定義繪圖,你并不需要實現(xiàn) -drawRect: 方法。有時,通過 UIGraphicsBeginImageContextWithOptions() 或者 CGBitmapContextCeate() 創(chuàng)建位圖會顯得更有意義,從位圖上面抓取圖像,并設(shè)置為 CALayer 的內(nèi)容。下面我們將給出一個例子來測試,檢驗。
如果我們看這個例子:
// Don't do this
- (void)drawRect:(CGRect)rect
{
[[UIColor redColor] setFill];
UIRectFill([self bounds]);
}
現(xiàn)在我們知道這為什么不好:我們促使 Core Animation 來為我們創(chuàng)建一個后備存儲,并讓它使用單一顏色填充后備存儲,然后上傳給 GPU。
我們跟本不需要實現(xiàn) -drawRect:,并節(jié)省這些代碼工作量,只需簡單的設(shè)置這個視圖圖層的背景顏色。如果這個視圖有一個 CAGradientLayer 作為圖層,那么這個技術(shù)也同樣適用于此(漸變圖層)。
類似的,你可以使用可變尺寸的圖像來降低繪圖系統(tǒng)的壓力。讓我們假設(shè)你需要一個 300×500 點的按鈕插圖,這將是 600×100=60k 像素或者 60kx4=240kB 內(nèi)存大小需要上傳到 GPU,并且占用 VRAM。如果我們使用所謂的可變尺寸的圖像,我們只需要一個 54×12 點的圖像,這將占用低于 2.6k 的像素或者 10kB 的內(nèi)存,這樣就變得更快了。
Core Animation 可以通過 CALayer 的 contentsCenter 屬性來改變圖像,大多數(shù)情況下,你可能更傾向于使用,-[UIImage resizableImageWithCapInsets:resizingMode:]。
同時注意,在第一次渲染這個按鈕之前,我們并不需要從文件系統(tǒng)讀取一個 60k 像素的 PNG 并解碼,解碼一個小的 PNG 將會更快。通過這種方式,你的程序在每一步的調(diào)用中都將做更少的工作,并且你的視圖將會加載的更快。
上一次 objc.io 的話題是關(guān)于并發(fā)的討論。正如你所知道的一樣,UIKit 的線程模型是非常簡單的:你僅可以從主隊列(比如主線程)中調(diào)用 UIKit 類(比如視圖),那么并發(fā)繪圖又是什么呢?
如果你必須實現(xiàn) -drawRect:,并且你必須繪制大量的東西,這將占用時間。由于你希望動畫變得更平滑,除了在主隊列中,你還希望在其他隊列中做一些工作。同時發(fā)生的繪圖是復雜的,但是除了幾個警告,同時發(fā)生的繪圖還是比較容易實現(xiàn)的。
我們除了在主隊列中可以向 CALayer 的后備存儲中繪制一些東西,其他方法都將不可行??膳碌氖虑閷l(fā)生。我們能做的就是向一個完全斷開鏈接的位圖上下文中進行繪制。
正如我們上面所提到的一樣,在 Core Graphics 下,所有 Core Graphics 繪制方法都需要一個上下文參數(shù)來指定繪制到那個上下文中。UIKit 有一個當前上下文的概念(也就是繪制到哪兒去)。這個當前的上下文就是 per-thread.
為了同時繪制,我們需要做下面的操作。我們需要在另一個隊列創(chuàng)建一個圖像,一旦我們擁有了圖像,我們可以切換回主隊列,并且設(shè)置這個圖像為 UIImageView 的圖像。這個技術(shù)在 WWDC 2012 session 211 中討論過。(異步下載圖片經(jīng)常用到這個)
增加一個你可以在其中繪制的新方法:
- (UIImage *)renderInImageOfSize:(CGSize)size;
{
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
// do drawing here
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
這個方法通過 UIGraphicsBeginImageContextWithOptions() 方法,并根據(jù)給定的大小創(chuàng)建一個新的 CGContextRef 位圖。這個方法也會將這個上下文設(shè)置為當前UIKit的上下文?,F(xiàn)在你可以在這里做你想在 -drawRect: 中做的事了。然后我們可以通過 UIGraphicsGetImageFromCurrentImageContext(),將獲得的這個上下文位圖數(shù)據(jù)作為一個 UIImage,最終移除這個上下文。
很重要的一點就是,你在這個方法中所做的所有繪圖的代碼都是線程安全的,也就是說,當你訪問屬性等等,他們需要線程安全。因為你是在另一個隊列中調(diào)用這個方法的。如果這個方法在你的視圖類中,那就需要注意一點了。另一個選擇就是創(chuàng)建一個單獨的渲染類,并設(shè)置所有需要的屬性,然后通過觸發(fā)來渲染圖片。如果這樣,你可以通過使用簡單的 UIImageView 或者 UITableViewCell。
要知道,所有 UIKit 的繪制 API 在使用另一個隊列時,都是安全的。只需要確定是在同一個操作中調(diào)用他們的,這個操作需要以 UIGraphicsBeginImageContextWithOptions() 開始,以 UIGraphicsEndIamgeContext() 結(jié)束。
你需要像下面這樣觸發(fā)渲染代碼:
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;
}];
}];
要注意,我們是在主隊列中調(diào)用 view.image = image.這是一個非常重要的細節(jié)。你不可以在任何其他隊列中調(diào)用這個代碼。
像往常一樣,同時繪制會伴隨很多問題,你現(xiàn)在需要取消后臺渲染。并且在渲染隊列中設(shè)置合理的同時繪制的最大限度。
為了支持這一切,最簡單的就是在一個 NSOperation 子類內(nèi)部實現(xiàn) -renderInImageOfSize:。
最終,需要指出,設(shè)置 UITableViewCell 內(nèi)容為異步是非常困難的。單元格很有可能在完成異步渲染前已經(jīng)被復用了。盡管單元格已經(jīng)被其他地方復用,但你只需要設(shè)置內(nèi)容就行了。
到現(xiàn)在為止,你需要知道在 GPU 內(nèi),一個 CALayer 在某種方式上和一個紋理類似。圖層有一個后備存儲,這便是被用來繪制到屏幕上的位圖。
通常,當你使用 CALayer 時,你會設(shè)置它的內(nèi)容為一個圖片。這到底做了什么?這樣做會告訴 Core Animation 使用圖片的位圖數(shù)據(jù)作為紋理。如果這個圖片(JPEG或PNG)被壓縮了,Core Animation 將會這個圖片解壓縮,然后上傳像素數(shù)據(jù)到 GPU。
盡管還有很多其他中圖層,如果你是用一個簡單的沒有設(shè)置上下文的 CALayer,并為這個 CALayer 設(shè)置一個背景顏色,Core Animation 并不會上傳任何數(shù)據(jù)到 GPU,但卻能夠不用任何像素數(shù)據(jù)而在 GPU 上完成所有的工作,類似的,對于漸變的圖層,GPU 是能創(chuàng)建漸變的,而且不需要 CPU 做任何工作,并且不需要上傳任何數(shù)據(jù)到 GPU。
如果一個 CALayer 的子類實現(xiàn)了 -drawInContext: 或者它的代理,類似于 -drawLayer:inContest:, Core Animation 將會為這個圖層申請一個后備存儲,用來保存那些方法繪制進來的位圖。那些方法內(nèi)的代碼將會運行在 CPU 上,結(jié)果將會被上傳到 GPU。
形狀和文本圖層還是有些不同的。開始時,Core Animation 為這些圖層申請一個后備存儲來保存那些需要為上下文生成的位圖數(shù)據(jù)。然后 Core Animation 會講這些圖形或文本繪制到后備存儲上。這在概念上非常類似于,當你實現(xiàn) -drawInContext: 方法,然后在方法內(nèi)繪制形狀或文本,他們的性能也很接近。
在某種程度上,當你需要改變形狀或者文本圖層時,這需要更新它的后備存儲,Core Animation 將會重新渲染后備存儲。例如,當動態(tài)改變形狀圖層的大小時,Core Animation 需要為動畫中的每一幀重新繪制形狀。
CALayer 有一個叫做 drawsAsynchronously 的屬性,這似乎是一個解決所有問題的高招。注意,盡管這可能提升性能,但也可能讓事情變慢。
當你設(shè)置 drawsAsynchronously 為 YES 時,發(fā)生了什么?你的 -drawRect:/-drawInContext: 方法仍然會被在主線程上調(diào)用。但是所有調(diào)用 Core Graphics 的操作都不會被執(zhí)行。取而代之的是,繪制命令被推遲,并且在后臺線程中異步執(zhí)行。
這種方式就是先記錄繪圖命令,然后在后臺線程中重現(xiàn)。為了這個過程的順利進行,更多的工作需要被做,更多的內(nèi)存需要被申請。但是主隊列中的一些工作便被移出來了(大概意思就是讓我們把一些能在后臺實現(xiàn)的工作放到后臺實現(xiàn),讓主線程更順暢)。
對于昂貴的繪圖方法,這是最有可能提升性能的,但對于那些繪圖方法來說,也不會節(jié)省太多資源。