鍍金池/ 教程/ iOS/ 繪制像素到屏幕上
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測(cè)試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動(dòng)開發(fā)
Collection View 動(dòng)畫
截圖測(cè)試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動(dòng)布局工具箱
動(dòng)畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動(dòng)畫解釋
響應(yīng)式 Android 應(yīng)用
初識(shí) TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測(cè)試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動(dòng)游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動(dòng)畫
常見的后臺(tái)實(shí)踐
糟糕的測(cè)試
避免濫用單例
數(shù)據(jù)模型和模型對(duì)象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場(chǎng)
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測(cè)試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測(cè)試
值對(duì)象
活動(dòng)追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測(cè)試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場(chǎng)
游戲
調(diào)試核對(duì)清單
View Controller 容器
學(xué)無止境
XCTest 測(cè)試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動(dòng)畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲(chǔ)
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫(kù)支持
Fetch 請(qǐng)求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識(shí)別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

繪制像素到屏幕上

一個(gè)像素是如何繪制到屏幕上去的?有很多種方式將一些東西映射到顯示屏上,他們需要調(diào)用不同的框架、許多功能和方法的結(jié)合體。這里我們大概的看一下屏幕之后發(fā)生的事情。當(dāng)你想要弄清楚什么時(shí)候、怎么去查明并解決問題時(shí),我希望這篇文章能幫助你理解哪一個(gè) API 可以更好的幫你解決問題。我們將聚焦于 iOS,然而我討論的大多數(shù)問題也同樣適用于 OS X。

圖形堆棧

當(dāng)像素映射到屏幕上的時(shí)候,后臺(tái)發(fā)生了很多事情。但一旦他們顯示到屏幕上,每一個(gè)像素均由三個(gè)顏色組件構(gòu)成:紅,綠,藍(lán)。三個(gè)獨(dú)立的顏色單元會(huì)根據(jù)給定的顏色顯示到一個(gè)像素上。在 iPhone5 的液晶顯示器上有1,136×640=727,040個(gè)像素,因此有2,181,120個(gè)顏色單元。在15寸視網(wǎng)膜屏的 MacBook Pro 上,這一數(shù)字達(dá)到15.5百萬以上。所有的圖形堆棧一起工作以確保每次正確的顯示。當(dāng)你滾動(dòng)整個(gè)屏幕的時(shí)候,數(shù)以百萬計(jì)的顏色單元必須以每秒60次的速度刷新,這是一個(gè)很大的工作量。

軟件組成

從簡(jiǎn)單的角度來看,軟件堆??雌饋碛悬c(diǎn)像這樣:

http://wiki.jikexueyuan.com/project/objc/images/3-1.png" alt="軟件堆棧" />

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

GPU Driver 是直接和 GPU 交流的代碼塊。不同的GPU是不同的性能怪獸,但是驅(qū)動(dòng)使他們?cè)谙乱粋€(gè)層級(jí)上顯示的更為統(tǒng)一,典型的驅(qū)動(dòng)有 OpenGL/OpenGL ES.

OpenGL(Open Graphics Library) 是一個(gè)提供了 2D 和 3D 圖形渲染的 API。GPU 是一塊非常特殊的硬件,OpenGL 和 GPU 密切的工作以提高GPU的能力,并實(shí)現(xiàn)硬件加速渲染。對(duì)大多數(shù)人來說,OpenGL 看起來非常底層,但是當(dāng)它在1992年第一次發(fā)布的時(shí)候(20多年前的事了)是第一個(gè)和圖形硬件(GPU)交流的標(biāo)準(zhǔn)化方式,這是一個(gè)重大的飛躍,程序員不再需要為每個(gè)GPU重寫他們的應(yīng)用了。

OpenGL 之上擴(kuò)展出很多東西。在 iOS 上,幾乎所有的東西都是通過 Core Animation 繪制出來,然而在 OS X 上,繞過 Core Animation 直接使用 Core Graphics 繪制的情況并不少見。對(duì)于一些專門的應(yīng)用,尤其是游戲,程序可能直接和 OpenGL/OpenGL ES 交流。事情變得使人更加困惑,因?yàn)?Core Animation 使用 Core Graphics 來做一些渲染。像 AVFoundation,Core Image 框架,和其他一些混合的入口。

要記住一件事情,GPU 是一個(gè)非常強(qiáng)大的圖形硬件,并且在顯示像素方面起著核心作用。它連接到 CPU。從硬件上講兩者之間存在某種類型的總線,并且有像 OpenGL,Core Animation 和 Core Graphics 這樣的框架來在 GPU 和 CPU 之間精心安排數(shù)據(jù)的傳輸。為了將像素顯示到屏幕上,一些處理將在 CPU 上進(jìn)行。然后數(shù)據(jù)將會(huì)傳送到 GPU,這也需要做一些相應(yīng)的操作,最終像素顯示到屏幕上。

這個(gè)過程的每一部分都有各自的挑戰(zhàn),并且許多時(shí)候需要做出折中的選擇。

硬件參與者

http://wiki.jikexueyuan.com/project/objc/images/3-2.png" alt="挑戰(zhàn)" />

正如上面這張簡(jiǎn)單的圖片顯示那些挑戰(zhàn):GPU 需要將每一個(gè) frame 的紋理(位圖)合成在一起(一秒60次)。每一個(gè)紋理會(huì)占用 VRAM(video RAM),所以需要給 GPU 同時(shí)保持紋理的數(shù)量做一個(gè)限制。GPU 在合成方面非常高效,但是某些合成任務(wù)卻比其他更復(fù)雜,并且 GPU在 16.7ms(1/60s)內(nèi)能做的工作也是有限的。

下一個(gè)挑戰(zhàn)就是將數(shù)據(jù)傳輸?shù)?GPU 上。為了讓 GPU 訪問數(shù)據(jù),需要將數(shù)據(jù)從 RAM 移動(dòng)到 VRAM 上。這就是提及到的上傳數(shù)據(jù)到 GPU。這看起來貌似微不足道,但是一些大型的紋理卻會(huì)非常耗時(shí)。

最終,CPU 開始運(yùn)行你的程序。你可能會(huì)讓 CPU 從 bundle 加載一張 PNG 的圖片并且解壓它。這所有的事情都在 CPU 上進(jìn)行。然后當(dāng)你需要顯示解壓縮后的圖片時(shí),它需要以某種方式上傳到 GPU。一些看似平凡的,比如顯示文本,對(duì) CPU 來說卻是一件非常復(fù)雜的事情,這會(huì)促使 Core Text 和 Core Graphics 框架更緊密的集成來根據(jù)文本生成一個(gè)位圖。一旦準(zhǔn)備好,它將會(huì)被作為一個(gè)紋理上傳到 GPU 并準(zhǔn)備顯示出來。當(dāng)你滾動(dòng)或者在屏幕上移動(dòng)文本時(shí),不管怎么樣,同樣的紋理能夠被復(fù)用,CPU 只需簡(jiǎn)單的告訴 GPU 新的位置就行了,所以 GPU 就可以重用存在的紋理了。CPU 并不需要重新渲染文本,并且位圖也不需要重新上傳到 GPU。

這張圖涉及到一些錯(cuò)綜復(fù)雜的方面,我們將會(huì)把這些方面提取出來并深一步了解。

合成

在圖形世界中,合成是一個(gè)描述不同位圖如何放到一起來創(chuàng)建你最終在屏幕上看到圖像的過程。在許多方面顯得顯而易見,而讓人忘了背后錯(cuò)綜復(fù)雜的計(jì)算。

讓我們忽略一些難懂的事例并且假定屏幕上一切事物皆紋理。一個(gè)紋理就是一個(gè)包含 RGBA 值的長(zhǎng)方形,比如,每一個(gè)像素里面都包含紅、綠、藍(lán)和透明度的值。在 Core Animation 世界中這就相當(dāng)于一個(gè) CALayer。

在這個(gè)簡(jiǎn)化的設(shè)置中,每一個(gè) layer 是一個(gè)紋理,所有的紋理都以某種方式堆疊在彼此的頂部。對(duì)于屏幕上的每一個(gè)像素,GPU 需要算出怎么混合這些紋理來得到像素 RGB 的值。這就是合成大概的意思。

如果我們所擁有的是一個(gè)和屏幕大小一樣并且和屏幕像素對(duì)齊的單一紋理,那么屏幕上每一個(gè)像素相當(dāng)于紋理中的一個(gè)像素,紋理的最后一個(gè)像素也就是屏幕的最后一個(gè)像素。

如果我們有第二個(gè)紋理放在第一個(gè)紋理之上,然后GPU將會(huì)把第二個(gè)紋理合成到第一個(gè)紋理中。有很多種不同的合成方法,但是如果我們假定兩個(gè)紋理的像素對(duì)齊,并且使用正常的混合模式,我們便可以用下面這個(gè)公式來計(jì)算每一個(gè)像素:

R = S + D * ( 1 – Sa )

結(jié)果的顏色是源色彩(頂端紋理)+目標(biāo)顏色(低一層的紋理)*(1-源顏色的透明度)。在這個(gè)公式中所有的顏色都假定已經(jīng)預(yù)先乘以了他們的透明度。

顯然相當(dāng)多的事情在這發(fā)生了。讓我們進(jìn)行第二個(gè)假定,兩個(gè)紋理都完全不透明,比如 alpha=1.如果目標(biāo)紋理(低一層的紋理)是藍(lán)色(RGB=0,0,1),并且源紋理(頂層的紋理)顏色是紅色(RGB=1,0,0),因?yàn)?Sa 為1,所以結(jié)果為:

R = S

結(jié)果是源顏色的紅色。這正是我們所期待的(紅色覆蓋了藍(lán)色)。

如果源顏色層為50%的透明,比如 alpha=0.5,既然 alpha 組成部分需要預(yù)先乘進(jìn) RGB 的值中,那么 S 的 RGB 值為(0.5, 0, 0),公式看起來便會(huì)像這樣:

                       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),是一個(gè)紫色。這正是我們所期望將透明紅色合成到藍(lán)色背景上所得到的。

記住我們剛剛只是將紋理中的一個(gè)像素合成到另一個(gè)紋理的像素上。當(dāng)兩個(gè)紋理覆蓋在一起的時(shí)候,GPU需要為所有像素做這種操作。正如你所知道的一樣,許多程序都有很多層,因此所有的紋理都需要合成到一起。盡管GPU是一塊高度優(yōu)化的硬件來做這種事情,但這還是會(huì)讓它非常忙碌,

不透明 VS 透明

當(dāng)源紋理是完全不透明的時(shí)候,目標(biāo)像素就等于源紋理。這可以省下 GPU 很大的工作量,這樣只需簡(jiǎn)單的拷貝源紋理而不需要合成所有的像素值。但是沒有方法能告訴 GPU 紋理上的像素是透明還是不透明的。只有當(dāng)你作為一名開發(fā)者知道你放什么到 CALayer 上了。這也是為什么 CALayer 有一個(gè)叫做 opaque 的屬性了。如果這個(gè)屬性為 YES,GPU 將不會(huì)做任何合成,而是簡(jiǎn)單從這個(gè)層拷貝,不需要考慮它下方的任何東西(因?yàn)槎急凰趽踝×?。這節(jié)省了 GPU 相當(dāng)大的工作量。這也正是 Instruments 中 color blended layers 選項(xiàng)中所涉及的。(這在模擬器中的Debug菜單中也可用).它允許你看到哪一個(gè) layers(紋理) 被標(biāo)注為透明的,比如 GPU 正在為哪一個(gè) layers 做合成。合成不透明的 layers 因?yàn)樾枰俚臄?shù)學(xué)計(jì)算而更廉價(jià)。

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

像素對(duì)齊 VS 不重合在一起

到現(xiàn)在我們都在考慮像素完美重合在一起的 layers。當(dāng)所有的像素是對(duì)齊的時(shí)候我們得到相對(duì)簡(jiǎn)單的計(jì)算公式。每當(dāng) GPU 需要計(jì)算出屏幕上一個(gè)像素是什么顏色的時(shí)候,它只需要考慮在這個(gè)像素之上的所有 layer 中對(duì)應(yīng)的單個(gè)像素,并把這些像素合并到一起?;蛘?,如果最頂層的紋理是不透明的(即圖層樹的最底層),這時(shí)候 GPU 就可以簡(jiǎn)單的拷貝它的像素到屏幕上。

當(dāng)一個(gè) layer 上所有的像素和屏幕上的像素完美的對(duì)應(yīng)整齊,那這個(gè) layer 就是像素對(duì)齊的。主要有兩個(gè)原因可能會(huì)造成不對(duì)齊。第一個(gè)便是滾動(dòng);當(dāng)一個(gè)紋理上下滾動(dòng)的時(shí)候,紋理的像素便不會(huì)和屏幕的像素排列對(duì)齊。另一個(gè)原因便是當(dāng)紋理的起點(diǎn)不在一個(gè)像素的邊界上。

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

Core Animation 工具和模擬器有一個(gè)叫做 color misaligned images 的選項(xiàng),當(dāng)這些在你的 CALayer 實(shí)例中發(fā)生的時(shí)候,這個(gè)功能便可向你展示。

Masks

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

離屏渲染(Offscreen Rendering)

離屏渲染可以被 Core Animation 自動(dòng)觸發(fā),或者被應(yīng)用程序強(qiáng)制觸發(fā)。屏幕外的渲染會(huì)合并/渲染圖層樹的一部分到一個(gè)新的緩沖區(qū),然后該緩沖區(qū)被渲染到屏幕上。

離屏渲染合成計(jì)算是非常昂貴的, 但有時(shí)你也許希望強(qiáng)制這種操作。一種好的方法就是緩存合成的紋理/圖層。如果你的渲染樹非常復(fù)雜(所有的紋理,以及如何組合在一起),你可以強(qiáng)制離屏渲染緩存那些圖層,然后可以用緩存作為合成的結(jié)果放到屏幕上。

如果你的程序混合了很多圖層,并且想要他們一起做動(dòng)畫,GPU 通常會(huì)為每一幀(1/60s)重復(fù)合成所有的圖層。當(dāng)使用離屏渲染時(shí),GPU 第一次會(huì)混合所有圖層到一個(gè)基于新的紋理的位圖緩存上,然后使用這個(gè)紋理來繪制到屏幕上?,F(xiàn)在,當(dāng)這些圖層一起移動(dòng)的時(shí)候,GPU 便可以復(fù)用這個(gè)位圖緩存,并且只需要做很少的工作。需要注意的是,只有當(dāng)那些圖層不改變時(shí),這才可以用。如果那些圖層改變了,GPU 需要重新創(chuàng)建位圖緩存。你可以通過設(shè)置 shouldRasterize 為 YES 來觸發(fā)這個(gè)行為。

然而,這是一個(gè)權(quán)衡。第一,這可能會(huì)使事情變得更慢。創(chuàng)建額外的屏幕外緩沖區(qū)是 GPU 需要多做的一步操作,特殊情況下這個(gè)位圖可能再也不需要被復(fù)用,這便是一個(gè)無用功了。然而,可以被復(fù)用的位圖,GPU 也有可能將它卸載了。所以你需要計(jì)算 GPU 的利用率和幀的速率來判斷這個(gè)位圖是否有用。

離屏渲染也可能產(chǎn)生副作用。如果你正在直接或者間接的將mask應(yīng)用到一個(gè)圖層上,Core Animation 為了應(yīng)用這個(gè) mask,會(huì)強(qiáng)制進(jìn)行屏幕外渲染。這會(huì)對(duì) GPU 產(chǎn)生重負(fù)。通常情況下 mask 只能被直接渲染到幀的緩沖區(qū)中(在屏幕內(nèi))。

Instrument 的 Core Animation 工具有一個(gè)叫做 Color Offscreen-Rendered Yellow 的選項(xiàng),它會(huì)將已經(jīng)被渲染到屏幕外緩沖區(qū)的區(qū)域標(biāo)注為黃色(這個(gè)選項(xiàng)在模擬器中也可以用)。同時(shí)記得檢查 Color Hits Green and Misses Red 選項(xiàng)。綠色代表無論何時(shí)一個(gè)屏幕外緩沖區(qū)被復(fù)用,而紅色代表當(dāng)緩沖區(qū)被重新創(chuàng)建。

一般情況下,你需要避免離屏渲染,因?yàn)檫@是很大的消耗。直接將圖層合成到幀的緩沖區(qū)中(在屏幕上)比先創(chuàng)建屏幕外緩沖區(qū),然后渲染到紋理中,最后將結(jié)果渲染到幀的緩沖區(qū)中要廉價(jià)很多。因?yàn)檫@其中涉及兩次昂貴的環(huán)境轉(zhuǎn)換(轉(zhuǎn)換環(huán)境到屏幕外緩沖區(qū),然后轉(zhuǎn)換環(huán)境到幀緩沖區(qū))。

所以當(dāng)你打開 Color Offscreen-Rendered Yellow 后看到黃色,這便是一個(gè)警告,但這不一定是不好的。如果 Core Animation 能夠復(fù)用屏幕外渲染的結(jié)果,這便能夠提升性能。

同時(shí)還要注意,rasterized layer 的空間是有限的。蘋果暗示大概有屏幕大小兩倍的空間來存儲(chǔ) rasterized layer/屏幕外緩沖區(qū)。

如果你使用 layer 的方式會(huì)通過屏幕外渲染,你最好擺脫這種方式。為 layer 使用蒙板或者設(shè)置圓角半徑會(huì)造成屏幕外渲染,產(chǎn)生陰影也會(huì)如此。

至于 mask,圓角半徑(特殊的mask)和 clipsToBounds/masksToBounds,你可以簡(jiǎn)單的為一個(gè)已經(jīng)擁有 mask 的 layer 創(chuàng)建內(nèi)容,比如,已經(jīng)應(yīng)用了 mask 的 layer 使用一張圖片。如果你想根據(jù) layer 的內(nèi)容為其應(yīng)用一個(gè)長(zhǎng)方形 mask,你可以使用 contentsRect 來代替蒙板。

如果你最后設(shè)置了 shouldRasterize 為 YES,那也要記住設(shè)置 rasterizationScale 為 contentsScale。

更多的關(guān)于合成

像往常一樣,維基百科上有更多關(guān)于透明合成的基礎(chǔ)公式。當(dāng)我們談完像素后,我們將更深入一點(diǎn)的談?wù)摷t,綠,藍(lán)和 alpha 是怎么在內(nèi)存中表現(xiàn)的。

OS X

如果你是在 OS X 上工作,你將會(huì)發(fā)現(xiàn)大多數(shù) debugging 選項(xiàng)在一個(gè)叫做 Quartz Debug 的獨(dú)立程序中,而不是在 Instruments 中。Quartz Debug 是 Graphics Tools 中的一部分,這可以在蘋果的 developer portal 中下載到。

Core Animation OpenGL ES

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

Core Animation 的核心是 OpenGL ES 的一個(gè)抽象物,簡(jiǎn)而言之,它讓你直接使用 OpenGL ES 的功能,卻不需要處理 OpenGL ES 做的復(fù)雜的事情。當(dāng)我們上面談?wù)摵铣傻臅r(shí)候,我們把 layer 和 texture 當(dāng)做等價(jià)的,但是他們不是同一物體,可又是如此的類似。

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

舉個(gè)例子,當(dāng)你設(shè)置一個(gè) layer 的內(nèi)容為 CGImageRef 時(shí),Core Animation 會(huì)創(chuàng)建一個(gè) OpenGL 紋理,并確保在這個(gè)圖層中的位圖被上傳到對(duì)應(yīng)的紋理中。以及當(dāng)你重寫 -drawInContext 方法時(shí),Core Animation 會(huì)請(qǐng)求分配一個(gè)紋理,同時(shí)確保 Core Graphics 會(huì)將你所做的(即你在drawInContext中繪制的東西)放入到紋理的位圖數(shù)據(jù)中。一個(gè)圖層的性質(zhì)和 CALayer 的子類會(huì)影響到 OpenGL 的渲染結(jié)果,許多低等級(jí)的 OpenGL ES 行為被簡(jiǎn)單易懂地封裝到 CALayer 概念中。

Core Animation 通過 Core Graphics 的一端和 OpenGL ES 的另一端,精心策劃基于 CPU 的位圖繪制。因?yàn)?Core Animation 處在渲染過程中的重要位置上,所以你如何使用 Core Animation 將會(huì)對(duì)性能產(chǎn)生極大的影響。

CPU限制 VS GPU限制

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

為了每秒達(dá)到 60 幀,你需要確定 CPU 和 GPU 不能過載。此外,即使你當(dāng)前能達(dá)到 60fps(frame per second),你還是要盡可能多的繪制工作交給 GPU 做,而讓 CPU 盡可能的來執(zhí)行應(yīng)用程序。通常,GPU 的渲染性能要比 CPU 高效很多,同時(shí)對(duì)系統(tǒng)的負(fù)載和消耗也更低一些。

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

要告訴你,如果是 GPU 限制了你的性能,你可以使用 OpenGL ES Driver instrument。點(diǎn)擊上面那個(gè)小的 i 按鈕,配置一下,同時(shí)注意查看 Device Utilization %?,F(xiàn)在,當(dāng)你運(yùn)行你的 app 時(shí),你可以看到你 GPU 的負(fù)荷。如果這個(gè)值靠近 100%,那么你就需要把你工作的重心放在GPU方面了。

Core Graphics / Quartz 2D

通過 Core Graphics 這個(gè)框架,Quartz 2D 被更為廣泛的知道。

Quartz 2D 擁有比我們這里談到更多的裝飾。我們這里不會(huì)過多的討論關(guān)于 PDF 的創(chuàng)建,渲染,解析,或者打印。只需要注意的是,PDF 的打印、創(chuàng)建和在屏幕上繪制位圖的操作是差不多的。因?yàn)樗麄兌际腔?Quartz 2D。

讓我們簡(jiǎn)單的了解一下 Quartz 2D 主要的概念。有關(guān)詳細(xì)信息可以到蘋果的官方文檔中了解。

放心,當(dāng)Quartz 2D 涉及到 2D 繪制的時(shí)候,它是非常強(qiáng)大的。有基于路徑的繪制,反鋸齒渲染,透明圖層,分辨率,并且設(shè)備獨(dú)立,可以說出很多特色。這可能會(huì)讓人產(chǎn)生畏懼,主要因?yàn)檫@是一個(gè)低級(jí)并且基于 C 的 API。

主要的概念當(dāng)對(duì)簡(jiǎn)單,UIKit 和 AppKit 都包含了 Quartz 2D 的一些簡(jiǎn)單 API,一旦你熟練了,一些簡(jiǎn)單 C 的 API 也是很容易理解的。最終你學(xué)會(huì)了一個(gè)能實(shí)現(xiàn) Photoshop 和 Illustrator 大部分功能的繪圖引擎。蘋果把 iOS 程序里面的股票應(yīng)用作為講解 Quartz 2D 在代碼中實(shí)現(xiàn)動(dòng)態(tài)渲染的一個(gè)例子。

當(dāng)你的程序進(jìn)行位圖繪制時(shí),不管使用哪種方式,都是基于 Quartz 2D 的。也就是說,CPU 部分實(shí)現(xiàn)的繪制是通過 Quartz 2D 實(shí)現(xiàn)的。盡管 Quartz 可以做其它的事情,但是我們這里還是集中于位圖繪制,在緩沖區(qū)(一塊內(nèi)存)繪制位圖會(huì)包括 RGBA 數(shù)據(jù)。

比方說,我們要畫一個(gè)八角形,我們通過 UIKit 能做到這一點(diǎn)

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];

相對(duì)應(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);

需要問的問題是:這個(gè)繪制到哪兒去了?這正好引出所謂的 CGContext 登場(chǎng)。我們傳過去的ctx參數(shù)正是在那個(gè)上下文中。而這個(gè)上下文定義了我們需要繪制的地方。如果我們實(shí)現(xiàn)了 CALayer 的 -drawInContext: 這時(shí)已經(jīng)傳過來一個(gè)上下文。繪制到這個(gè)上下文中的內(nèi)容將會(huì)被繪制到圖層的備份區(qū)(圖層的緩沖區(qū)).但是我們也可以創(chuàng)建我們自己的上下文,叫做基于位圖的上下文,比如 CGBitmapContextCreate().這個(gè)方法返回一個(gè)我們可以傳給 CGContext 方法來繪制的上下文。

注意 UIKit 版本的代碼為何不傳入一個(gè)上下文參數(shù)到方法中?這是因?yàn)楫?dāng)使用 UIKit 或者 AppKit 時(shí),上下文是唯一的。UIkit 維護(hù)著一個(gè)上下文堆棧,UIKit 方法總是繪制到最頂層的上下文中。你可以使用 UIGraphicsGetCurrentContext() 來得到最頂層的上下文。你可以使用 UIGraphicsPushContext()UIGraphicsPopContext() 在 UIKit 的堆棧中推進(jìn)或取出上下文。

最為突出的是,UIKit 使用 UIGraphicsBeginImageContextWithOptions()UIGraphicsEndImageContext() 方便的創(chuàng)建類似于 CGBitmapContextCreate() 的位圖上下文?;旌险{(diào)用 UIKit 和 Core Graphics 非常簡(jiǎn)單:

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)建大量的非??岬臇|西。一個(gè)很好的理由就是,蘋果的文檔有很多例子。我們不能得到所有的細(xì)節(jié),但是 Core Graphics 有一個(gè)非常接近 Adobe IllustratorAdobe Photoshop 如何工作的繪圖模型,并且大多數(shù)工具的理念翻譯成 Core Graphics 了。終究,他是起源于 NeXTSTEP 。(原來也是喬老爺?shù)淖髌?。

CGLayer

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

像素

屏幕上的像素是由紅,綠,藍(lán)三種顏色組件構(gòu)成的。因此,位圖數(shù)據(jù)有時(shí)也被叫做 RGB 數(shù)據(jù)。你可能會(huì)對(duì)數(shù)據(jù)如何組織在內(nèi)存中感到好奇。而事實(shí)是,有很多種不同的方式在內(nèi)存中展現(xiàn)RGB位圖數(shù)據(jù)。

稍后我們將會(huì)談到壓縮數(shù)據(jù),這又是一個(gè)完全不同的概念。現(xiàn)在,我們先看一下RGB位圖數(shù)據(jù),我們可以從顏色組件:紅,綠,藍(lán)中得到一個(gè)值。而大多數(shù)情況下,我們有第四個(gè)組件:透明度。最終我們從每個(gè)像素中得到四個(gè)單獨(dú)的值。

默認(rèn)的像素布局

在 iOS 和 OS X 上最常見的格式就是大家所熟知的 32bits-per-pixel(bpp), 8bits-per-componet(bpc),透明度會(huì)首先被乘以到像素值上(就像上文中提到的那個(gè)公式一樣),在內(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 ...

這個(gè)格式經(jīng)常被叫做 ARGB。每個(gè)像素占用 4 字節(jié)(32bpp),每一個(gè)顏色組件是1字節(jié)(8bpc).每個(gè)像素有一個(gè) alpha 值,這個(gè)值總是最先得到的(在RGB值之前),最終紅、綠、藍(lán)的值都會(huì)被預(yù)先乘以 alpha 的值。預(yù)乘的意思就是 alpha 值被烘烤到紅、綠、藍(lán)的組件中。如果我們有一個(gè)橙色,他們各自的 8bpc 就像這樣: 240,99,24.一個(gè)完全不透明的橙色像素?fù)碛械?ARGB 值為: 255,240,99,24,它在內(nèi)存中的布局就像上面圖示那樣。如果我們有一個(gè)相同顏色的像素,但是 alpha 值為 33%,那么他的像素值便是:84,80,33,8.

另一個(gè)常見的格式便是 32bpp,8bpc,跳過第一個(gè) 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)該想知道為什么這種格式很流行,當(dāng)我們每一個(gè)像素中都有一個(gè)不用字節(jié)時(shí),我們將會(huì)省下 25% 的空間。事實(shí)證明,這種格式更容易被現(xiàn)代的 CPU 和繪圖算法消化,因?yàn)槊恳粋€(gè)獨(dú)立的像素都對(duì)齊到 32-bit 的邊界?,F(xiàn)代的 CPU 不喜歡裝載(讀取)不對(duì)齊的數(shù)據(jù),特別是當(dāng)將這種數(shù)據(jù)和上面沒有 alpha 值格式的數(shù)據(jù)混合時(shí),算法需要做很多挪動(dòng)和蒙板操作。

當(dāng)處理 RGB 數(shù)據(jù)時(shí),Core Graphics 也需要支持把a(bǔ)lpha 值放到最后(另外還要支持跳過)。有時(shí)候也分別稱為 RGBA 和 RGBx,假定是 8bpc,并且預(yù)乘了 alpha 值。

深?yuàn)W的布局

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

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

還有一個(gè)是 64bpp,16bpc,最終為 128bpp,32bpc,浮點(diǎn)數(shù)組件(有或沒有 alpha 值)。它們分別使用 8 字節(jié)和 16 字節(jié),并且允許更高的精度。當(dāng)然,這會(huì)造成更多的內(nèi)存使用和昂貴的計(jì)算。

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

二維數(shù)據(jù)

當(dāng)顏色組件(紅、綠、藍(lán)、alpha)混雜在一起的時(shí)候,大多數(shù)框架(包括 Core Graphics )使用像素?cái)?shù)據(jù)。正是這種情況下我們稱之為二維數(shù)據(jù),或者二維組件。這個(gè)意思是:每一個(gè)顏色組件都在它自己的內(nèi)存區(qū)域,也就是說它是二維的。比如 RGB 數(shù)據(jù),我們有三個(gè)獨(dú)立的內(nèi)存區(qū)域,一個(gè)大的區(qū)域包含了所有像素的紅顏色的值,一個(gè)包含了所有綠顏色的值,一個(gè)包含了所有藍(lán)顏色的值。

在某些情況下,一些視頻框架便會(huì)使用二維數(shù)據(jù)。

YCbCr

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

出于同樣的原因,JPEG 圖像有時(shí)會(huì)將像素?cái)?shù)據(jù)從 RGB 轉(zhuǎn)換到 YCbCr。JPEG 單獨(dú)的壓縮每一個(gè)二維顏色。當(dāng)壓縮基于 YCbCr 的平面時(shí),Cb 和 Cr 能比 Y 壓縮得更完全。

圖片格式

當(dāng)你在 iOS 或者 OS X 上處理圖片時(shí),他們大多數(shù)為 JPEG 和 PNG。讓我們更進(jìn)一步觀察。

JPEG

每個(gè)人都知道 JPEG。他是相機(jī)的產(chǎn)物。它代表這照片如何存儲(chǔ)在電腦上。甚至你嘛嘛都聽說過 JPEG。

一個(gè)很好的理由,很多人都認(rèn)為 JPEG 文件僅是另一種像素?cái)?shù)據(jù)的格式,就像我們剛剛談到的 RGB 像素布局那樣。這樣理解離真像真是差十萬八千里了。

將 JPEG 數(shù)據(jù)轉(zhuǎn)換成像素?cái)?shù)據(jù)是一個(gè)非常復(fù)雜的過程,你通過一個(gè)周末的計(jì)劃都不能完成,甚至是一個(gè)非常漫長(zhǎng)的周末(原文的意思好像就是為了表達(dá)這個(gè)過程非常復(fù)雜,不過老外的比喻總讓人拎不清)。對(duì)于每一個(gè)二維顏色,JPEG 使用一種基于離散余弦變換(簡(jiǎn)稱 DCT 變換)的算法,將空間信息轉(zhuǎn)變到頻域.這個(gè)信息然后被量子化,排好序,并且用一種哈夫曼編碼的變種來壓縮。很多時(shí)候,首先數(shù)據(jù)會(huì)被從 RGB 轉(zhuǎn)換到二維 YCbCr,當(dāng)解碼 JPEG 的時(shí)候,這一切都將變得可逆。

這也是為什么當(dāng)你通過 JPEG 文件創(chuàng)建一個(gè) UIImage 并且繪制到屏幕上時(shí),將會(huì)有一個(gè)延時(shí),因?yàn)?CPU 這時(shí)候忙于解壓這個(gè) JPEG。如果你需要為每一個(gè) tableviewcell 解壓 JPEG,那么你的滾動(dòng)當(dāng)然不會(huì)平滑(原來 tableviewcell 里面最要不要用 JPEG 的圖片)。

那究竟為什么我們還要用 JPEG 呢?答案就是 JPEG 可以非常非常好的壓縮圖片。一個(gè)通過 iPhone5 拍攝的,未經(jīng)壓縮的圖片占用接近 24M。但是通過默認(rèn)壓縮設(shè)置,你的照片通常只會(huì)在 2-3M 左右。JPEG 壓縮這么好是因?yàn)樗鞘д娴?,它去除了人眼很難察覺的信息,并且這樣做可以超出像 gzip 這樣壓縮算法的限制。但這僅僅在圖片上有效的,因?yàn)?JPEG 依賴于圖片上有很多人類不能察覺出的數(shù)據(jù)。如果你從一個(gè)基本顯示文本的網(wǎng)頁(yè)上截取一張圖,JPEG 將不會(huì)這么高效。壓縮效率將會(huì)變得低下,你甚至能看出來圖片已經(jīng)壓縮變形了。

PNG

PNG讀作”ping”。和 JPEG 相反,它的壓縮對(duì)格式是無損的。當(dāng)你將一張圖片保存為 PNG,并且打開它(或解壓),所有的像素?cái)?shù)據(jù)會(huì)和最初一模一樣,因?yàn)檫@個(gè)限制,PNG 不能像 JPEG 一樣壓縮圖片,但是對(duì)于像程序中的原圖(如buttons,icons),它工作的非常好。更重要的是,解碼 PNG 數(shù)據(jù)比解碼 JPEG 簡(jiǎn)單的多。

在現(xiàn)實(shí)世界中,事情從來沒有那么簡(jiǎn)單,目前存在了大量不同的 PNG 格式??梢酝ㄟ^維基百科查看詳情。但是簡(jiǎn)言之,PNG 支持壓縮帶或不帶 alpha 通道的顏色像素(RGB),這也是為什么它在程序原圖中表現(xiàn)良好的另一個(gè)原因。

挑選一個(gè)格式

當(dāng)你在你的程序中使用圖片時(shí),你需要堅(jiān)持這兩種格式: JPEG 或者 PNG。讀寫這種格式文件的壓縮和解壓文件能表現(xiàn)出很高的性能,另外,還支持并行操作。同時(shí) Apple 正在改進(jìn)解壓縮并可能出現(xiàn)在將來的新操作系統(tǒng)中,屆時(shí)你將會(huì)得到持續(xù)的性能提升。如果嘗試使用另一種格式,你需要注意到,這可能對(duì)你程序的性能會(huì)產(chǎn)生影響,同時(shí)可能會(huì)打開安全漏洞,經(jīng)常,圖像解壓縮算法是黑客最喜歡的攻擊目標(biāo)。

已經(jīng)寫了很多關(guān)于優(yōu)化 PNGs,如果你想要了解更多,請(qǐng)到互聯(lián)網(wǎng)上查詢。非常重要的一點(diǎn),注意 Xcode 優(yōu)化 PNG 選項(xiàng)和優(yōu)化其他引擎有很大的不同。

當(dāng) Xcode 優(yōu)化一個(gè) PNG 文件的時(shí)候,它將 PNG 文件變成一個(gè)從技術(shù)上講不再是有效的PNG文件。但是 iOS 可以讀取這種文件,并且這比解壓縮正常的 PNG 文件更快。Xcode 改變他們,讓 iOS 通過一種對(duì)正常 PNG 不起作用的算法來對(duì)他們解壓縮。值得注意的重點(diǎn)是,這改變了像素的布局。正如我們所提到的一樣,在像素之下有很多種方式來描繪 RGB 數(shù)據(jù),如果這不是 iOS 繪制系統(tǒng)所需要的格式,它需要將每一個(gè)像素的數(shù)據(jù)替換,而不需要加速來做這件事。

讓我們?cè)購(gòu)?qiáng)調(diào)一遍,如果你可以,你需要為原圖設(shè)置 resizable images。你的文件將變得更小,因此你只需要從文件系統(tǒng)裝載更少的數(shù)據(jù)。

UIKit 和 Pixels

每一個(gè)在 UIKit 中的 view 都有它自己的 CALayer。依次,這些圖層都有一個(gè)叫像素位圖的后備存儲(chǔ),有點(diǎn)像一個(gè)圖像。這個(gè)后備存儲(chǔ)正是被渲染到顯示器上的。

With –drawRect:

如果你的視圖類實(shí)現(xiàn)了 -drawRect:,他們將像這樣工作:

當(dāng)你調(diào)用 -setNeedsDisplay,UIKit 將會(huì)在這個(gè)視圖的圖層上調(diào)用 -setNeedsDisplay。這為圖層設(shè)置了一個(gè)標(biāo)識(shí),標(biāo)記為 dirty(直譯是臟的意思,想不出用什么詞比較貼切,污染?),但還顯示原來的內(nèi)容。它實(shí)際上沒做任何工作,所以多次調(diào)用 -setNeedsDisplay并不會(huì)造成性能損失。

下面,當(dāng)渲染系統(tǒng)準(zhǔn)備好,它會(huì)調(diào)用視圖圖層的-display方法.此時(shí),圖層會(huì)裝配它的后備存儲(chǔ)。然后建立一個(gè) Core Graphics 上下文(CGContextRef),將后備存儲(chǔ)對(duì)應(yīng)內(nèi)存中的數(shù)據(jù)恢復(fù)出來,繪圖會(huì)進(jìn)入對(duì)應(yīng)的內(nèi)存區(qū)域,并使用 CGContextRef 繪制。

當(dāng)你使用 UIKit 的繪制方法,例如: UIRectFill() 或者 -[UIBezierPath fill] 代替你的 -drawRect: 方法,他們將會(huì)使用這個(gè)上下文。使用方法是,UIKit 將后備存儲(chǔ)的 CGContextRef 推進(jìn)他的 graphics context stack,也就是說,它會(huì)將那個(gè)上下文設(shè)置為當(dāng)前的。因此 UIGraphicsGetCurrent() 將會(huì)返回那個(gè)對(duì)應(yīng)的上下文。既然 UIKit 使用 UIGraphicsGetCurrent() 繪制方法,繪圖將會(huì)進(jìn)入到圖層的后備存儲(chǔ)。如果你想直接使用 Core Graphics 方法,你可以自己調(diào)用 UIGraphicsGetCurrent() 得到相同的上下文,并且將這個(gè)上下文傳給 Core Graphics 方法。

從現(xiàn)在開始,圖層的后備存儲(chǔ)將會(huì)被不斷的渲染到屏幕上。直到下次再次調(diào)用視圖的 -setNeedsDisplay ,將會(huì)依次將圖層的后備存儲(chǔ)更新到視圖上。

不使用 -drawRect:

當(dāng)你用一個(gè) UIImageView 時(shí),事情略有不同,這個(gè)視圖仍然有一個(gè) CALayer,但是圖層卻沒有申請(qǐng)一個(gè)后備存儲(chǔ)。取而代之的是使用一個(gè) CGImageRef 作為他的內(nèi)容,并且渲染服務(wù)將會(huì)把圖片的數(shù)據(jù)繪制到幀的緩沖區(qū),比如,繪制到顯示屏。

在這種情況下,將不會(huì)繼續(xù)重新繪制。我們只是簡(jiǎn)單的將位圖數(shù)據(jù)以圖片的形式傳給了 UIImageView,然后 UIImageView 傳給了 Core Animation,然后輪流傳給渲染服務(wù)。

實(shí)現(xiàn)-drawRect: 還是不實(shí)現(xiàn) -drawRect:

這聽起來貌似有點(diǎn)低俗,但是最快的繪制就是你不要做任何繪制。

大多數(shù)時(shí)間,你可以不要合成你在其他視圖(圖層)上定制的視圖(圖層),這正是我們推薦的,因?yàn)?UIKit 的視圖類是非常優(yōu)化的 (就是讓我們不要閑著沒事做,自己去合并視圖或圖層) 。

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

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

單一顏色

如果我們看這個(gè)例子:

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

現(xiàn)在我們知道這為什么不好:我們促使 Core Animation 來為我們創(chuàng)建一個(gè)后備存儲(chǔ),并讓它使用單一顏色填充后備存儲(chǔ),然后上傳給 GPU。

我們跟本不需要實(shí)現(xiàn) -drawRect:,并節(jié)省這些代碼工作量,只需簡(jiǎn)單的設(shè)置這個(gè)視圖圖層的背景顏色。如果這個(gè)視圖有一個(gè) CAGradientLayer 作為圖層,那么這個(gè)技術(shù)也同樣適用于此(漸變圖層)。

可變尺寸的圖像

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

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

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

并發(fā)繪圖

上一次 objc.io 的話題是關(guān)于并發(fā)的討論。正如你所知道的一樣,UIKit 的線程模型是非常簡(jiǎn)單的:你僅可以從主隊(duì)列(比如主線程)中調(diào)用 UIKit 類(比如視圖),那么并發(fā)繪圖又是什么呢?

如果你必須實(shí)現(xiàn) -drawRect:,并且你必須繪制大量的東西,這將占用時(shí)間。由于你希望動(dòng)畫變得更平滑,除了在主隊(duì)列中,你還希望在其他隊(duì)列中做一些工作。同時(shí)發(fā)生的繪圖是復(fù)雜的,但是除了幾個(gè)警告,同時(shí)發(fā)生的繪圖還是比較容易實(shí)現(xiàn)的。

我們除了在主隊(duì)列中可以向 CALayer 的后備存儲(chǔ)中繪制一些東西,其他方法都將不可行。可怕的事情將會(huì)發(fā)生。我們能做的就是向一個(gè)完全斷開鏈接的位圖上下文中進(jìn)行繪制。

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

為了同時(shí)繪制,我們需要做下面的操作。我們需要在另一個(gè)隊(duì)列創(chuàng)建一個(gè)圖像,一旦我們擁有了圖像,我們可以切換回主隊(duì)列,并且設(shè)置這個(gè)圖像為 UIImageView 的圖像。這個(gè)技術(shù)在 WWDC 2012 session 211 中討論過。(異步下載圖片經(jīng)常用到這個(gè))

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

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

    // do drawing here

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

這個(gè)方法通過 UIGraphicsBeginImageContextWithOptions() 方法,并根據(jù)給定的大小創(chuàng)建一個(gè)新的 CGContextRef 位圖。這個(gè)方法也會(huì)將這個(gè)上下文設(shè)置為當(dāng)前UIKit的上下文?,F(xiàn)在你可以在這里做你想在 -drawRect: 中做的事了。然后我們可以通過 UIGraphicsGetImageFromCurrentImageContext(),將獲得的這個(gè)上下文位圖數(shù)據(jù)作為一個(gè) UIImage,最終移除這個(gè)上下文。

很重要的一點(diǎn)就是,你在這個(gè)方法中所做的所有繪圖的代碼都是線程安全的,也就是說,當(dāng)你訪問屬性等等,他們需要線程安全。因?yàn)槟闶窃诹硪粋€(gè)隊(duì)列中調(diào)用這個(gè)方法的。如果這個(gè)方法在你的視圖類中,那就需要注意一點(diǎn)了。另一個(gè)選擇就是創(chuàng)建一個(gè)單獨(dú)的渲染類,并設(shè)置所有需要的屬性,然后通過觸發(fā)來渲染圖片。如果這樣,你可以通過使用簡(jiǎn)單的 UIImageView 或者 UITableViewCell。

要知道,所有 UIKit 的繪制 API 在使用另一個(gè)隊(duì)列時(shí),都是安全的。只需要確定是在同一個(gè)操作中調(diào)用他們的,這個(gè)操作需要以 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;
        }];
}];

要注意,我們是在主隊(duì)列中調(diào)用 view.image = image.這是一個(gè)非常重要的細(xì)節(jié)。你不可以在任何其他隊(duì)列中調(diào)用這個(gè)代碼。

像往常一樣,同時(shí)繪制會(huì)伴隨很多問題,你現(xiàn)在需要取消后臺(tái)渲染。并且在渲染隊(duì)列中設(shè)置合理的同時(shí)繪制的最大限度。

為了支持這一切,最簡(jiǎn)單的就是在一個(gè) NSOperation 子類內(nèi)部實(shí)現(xiàn) -renderInImageOfSize:。

最終,需要指出,設(shè)置 UITableViewCell 內(nèi)容為異步是非常困難的。單元格很有可能在完成異步渲染前已經(jīng)被復(fù)用了。盡管單元格已經(jīng)被其他地方復(fù)用,但你只需要設(shè)置內(nèi)容就行了。

CALayer

到現(xiàn)在為止,你需要知道在 GPU 內(nèi),一個(gè) CALayer 在某種方式上和一個(gè)紋理類似。圖層有一個(gè)后備存儲(chǔ),這便是被用來繪制到屏幕上的位圖。

通常,當(dāng)你使用 CALayer 時(shí),你會(huì)設(shè)置它的內(nèi)容為一個(gè)圖片。這到底做了什么?這樣做會(huì)告訴 Core Animation 使用圖片的位圖數(shù)據(jù)作為紋理。如果這個(gè)圖片(JPEG或PNG)被壓縮了,Core Animation 將會(huì)這個(gè)圖片解壓縮,然后上傳像素?cái)?shù)據(jù)到 GPU。

盡管還有很多其他中圖層,如果你是用一個(gè)簡(jiǎn)單的沒有設(shè)置上下文的 CALayer,并為這個(gè) CALayer 設(shè)置一個(gè)背景顏色,Core Animation 并不會(huì)上傳任何數(shù)據(jù)到 GPU,但卻能夠不用任何像素?cái)?shù)據(jù)而在 GPU 上完成所有的工作,類似的,對(duì)于漸變的圖層,GPU 是能創(chuàng)建漸變的,而且不需要 CPU 做任何工作,并且不需要上傳任何數(shù)據(jù)到 GPU。

自定義繪制的圖層

如果一個(gè) CALayer 的子類實(shí)現(xiàn)了 -drawInContext: 或者它的代理,類似于 -drawLayer:inContest:, Core Animation 將會(huì)為這個(gè)圖層申請(qǐng)一個(gè)后備存儲(chǔ),用來保存那些方法繪制進(jìn)來的位圖。那些方法內(nèi)的代碼將會(huì)運(yùn)行在 CPU 上,結(jié)果將會(huì)被上傳到 GPU。

形狀和文本圖層

形狀和文本圖層還是有些不同的。開始時(shí),Core Animation 為這些圖層申請(qǐng)一個(gè)后備存儲(chǔ)來保存那些需要為上下文生成的位圖數(shù)據(jù)。然后 Core Animation 會(huì)講這些圖形或文本繪制到后備存儲(chǔ)上。這在概念上非常類似于,當(dāng)你實(shí)現(xiàn) -drawInContext: 方法,然后在方法內(nèi)繪制形狀或文本,他們的性能也很接近。

在某種程度上,當(dāng)你需要改變形狀或者文本圖層時(shí),這需要更新它的后備存儲(chǔ),Core Animation 將會(huì)重新渲染后備存儲(chǔ)。例如,當(dāng)動(dòng)態(tài)改變形狀圖層的大小時(shí),Core Animation 需要為動(dòng)畫中的每一幀重新繪制形狀。

異步繪圖

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

當(dāng)你設(shè)置 drawsAsynchronously 為 YES 時(shí),發(fā)生了什么?你的 -drawRect:/-drawInContext: 方法仍然會(huì)被在主線程上調(diào)用。但是所有調(diào)用 Core Graphics 的操作都不會(huì)被執(zhí)行。取而代之的是,繪制命令被推遲,并且在后臺(tái)線程中異步執(zhí)行。

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

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