在第14章『圖像IO』討論如何高效地載入和顯示圖像,通過視圖來避免可能引起 動畫幀率下降的性能問題。在最后一章,我們將著重圖層樹本身,以發掘最好的性 能。
隱式繪制
寄宿圖可以通過Core Graphics
直接繪制,也可以直接載入一個圖片文件并賦值給contents
屬性,或事先繪制一個屏幕之外的 CGContext
上下文。在之前的兩 章中我們討論了這些場景下的優化。但是除了常見的顯式創建寄宿圖,你也可以通過以下三種方式創建隱式的:1,使用特性的圖層屬性。2,特定的視圖。3,特定 的圖層子類。
了解這個情況為什么發生何時發生是很重要的,它能夠讓你避免引入不必要的軟件繪制行為。
文本
CATextLayer
和 UILabel
都是直接將文本繪制在圖層的寄宿圖中。事實上這兩種方式用了完全不同的渲染方式:在iOS 6
及之前, UILabel
用WebKit
的HTML
渲染引擎來繪制文本,而 CATextLayer
用的是Core Text
.后者渲染更迅速,所以 在所有需要繪制大量文本的情形下都優先使用它吧。但是這兩種方法都用了軟件的 方式繪制,因此他們實際上要比硬件加速合成方式要慢。
不論如何,盡可能地避免改變那些包含文本的視圖的frame
,因為這樣做的話文本就需要重繪。例如,如果你想在圖層的角落里顯示一段靜態的文本,但是這個圖 層經常改動,你就應該把文本放在一個子圖層中。
光柵化
在第四章『視覺效果』中我們提到了 CALayer
的shouldRasterize
屬性,它可以解決重疊透明圖層的混合失靈問題。同樣在第12章『速度的曲調』
中,它也是作為繪制復雜圖層樹結構的優化方法。
啟用 shouldRasterize
屬性會將圖層繪制到一個屏幕之外的圖像。然后這個圖 像將會被緩存起來并繪制到實際圖層的 contents
和子圖層。如果有很多的子圖層 或者有復雜的效果應用,這樣做就會比重繪所有事務的所有幀劃得來得多。但是光 柵化原始圖像需要時間,而且還會消耗額外的內存。
當我們使用得當時,光柵化可以提供很大的性能優勢(如你在第12章所見),但 是一定要避免作用在內容不斷變動的圖層上,否則它緩存方面的好處就會消失,而且會讓性能變的更糟。
為了檢測你是否正確地使用了光柵化方式,用Instrument
查看一下Color Hits Green
和Misses Red
項目,是否已光柵化圖像被頻繁地刷新(這樣就說明圖層并不 是光柵化的好選擇,或則你無意間觸發了不必要的改變導致了重繪行為)。
離屏渲染
當圖層屬性的混合體被指定為在未預合成之前不能直接在屏幕中繪制時,屏幕外渲染就被喚起了。屏幕外渲染并不意味著軟件繪制,但是它意味著圖層必須在被顯示之前在一個屏幕外上下文中被渲染(不論CPU
還是GPU
)。圖層的以下屬性將會 觸發屏幕外繪制:
- 圓角(當和 maskToBounds 一起使用時)
- 圖層蒙板
- 陰影
屏幕外渲染和我們啟用光柵化時相似,除了它并沒有像光柵化圖層那么消耗大,子圖層并沒有被影響到,而且結果也沒有被緩存,所以不會有長期的內存占用。但是,如果太多圖層在屏幕外渲染依然會影響到性能。
有時候我們可以把那些需要屏幕外繪制的圖層開啟光柵化以作為一個優化方式,前提是這些圖層并不會被頻繁地重繪。
對于那些需要動畫而且要在屏幕外渲染的圖層來說,你可以用CAShapeLayer
, contentsCenter
或者shadowPath
來獲得同樣的表現而且 較少地影響到性能。
CAShapeLayer
cornerRadius
和 maskToBounds
獨立作用的時候都不會有太大的性能問題, 但是當他倆結合在一起,就觸發了屏幕外渲染。有時候你想顯示圓角并沿著圖層裁 切子圖層的時候,你可能會發現你并不需要沿著圓角裁切,這個情況下用 CAShapeLayer
就可以避免這個問題了。
你想要的只是圓角且沿著矩形邊界裁切,同時還不希望引起性能問題。其實你可以用現成的UIBezierPath
的構造器 + bezierPathWithRoundedRect:cornerRadius:
這樣做并不會比直接用cornerRadius
更快,但是它避免了性能問題。
可伸縮圖片
另一個創建圓角矩形的方法就是用一個圓形內容圖片并結合第二章『寄宿圖』提 到的 contensCenter
屬性去創建一個可伸縮圖片(見清單15.2).理論上來說,這 個應該比用CAShapeLayer
要快,因為一個可拉伸圖片只需要18
個三角形(一個 圖片是由一個3*3
網格渲染而成),然而,許多都需要渲染成一個順滑的曲線。在 實際應用上,二者并沒有太大的區別。
使用可伸縮圖片的優勢在于它可以繪制成任意邊框效果而不需要額外的性能消耗。舉個例子,可伸縮圖片甚至還可以顯示出矩形陰影的效果。
shadowPath
在第2
章我們有提到 shadowPath
屬性。如果圖層是一個簡單幾何圖形如矩形或 者圓角矩形(假設不包含任何透明部分或者子圖層),創建出一個對應形狀的陰影 路徑就比較容易,而且Core Animation
繪制這個陰影也相當簡單,避免了屏幕外的 圖層部分的預排版需求。這對性能來說很有幫助。
如果你的圖層是一個更復雜的圖形,生成正確的陰影路徑可能就比較難了,這樣子的話你可以考慮用繪圖軟件預先生成一個陰影背景圖。
混合和過度繪制
在第12
章有提到,GPU
每一幀可以繪制的像素有一個最大限制(就是所謂的fill rate
),這個情況下可以輕易地繪制整個屏幕的所有像素。但是如果由于重疊圖層的關系需要不停地重繪同一區域的話,掉幀就可能發生了。
GPU
會放棄繪制那些完全被其他圖層遮擋的像素,但是要計算出一個圖層是否被 遮擋也是相當復雜并且會消耗處理器資源。同樣,合并不同圖層的透明重疊像素 (即混合)消耗的資源也是相當客觀的。所以為了加速處理進程,不到必須時刻不 要使用透明圖層。任何情況下,你應該這樣做:
- 給視圖的 屬性設置一個固定的,不透明的顏色
- 設置
opaque
屬性為YES
這樣做減少了混合行為(因為編譯器知道在圖層之后的東西都不會對最終的像素 顏色產生影響)并且計算得到了加速,避免了過度繪制行為因為Core Animation
可以舍棄所有被完全遮蓋住的圖層,而不用每個像素都去計算一遍。
如果用到了圖像,盡量避免透明除非非常必要。如果圖像要顯示在一個固定的背景顏色或是固定的背景圖之前,你沒必要相對前景移動,你只需要預填充背景圖片就可以避免運行時混色了。
如果是文本的話,一個白色背景的 UILabel
(或者其他顏色)會比透明背景要 更高效。
最后,明智地使用 shouldRasterize
屬性,可以將一個固定的圖層體系折疊成 單張圖片,這樣就不需要每一幀重新合成了,也就不會有因為子圖層之間的混合和過度繪制的性能問題了。
減少圖層數量
初始化圖層,處理圖層,打包通過IPC
發給渲染引擎,轉化成OpenGL
幾何圖 形,這些是一個圖層的大致資源開銷。事實上,一次性能夠在屏幕上顯示的最大圖 層數量也是有限的。
確切的限制數量取決于iOS
設備,圖層類型,圖層內容和屬性等。但是總得說來 可以容納上百或上千個,下面我們將演示即使圖層本身并沒有做什么也會遇到的性 能問題。
裁切
在對圖層做任何優化之前,你需要確定你不是在創建一些不可見的圖層,圖層在以下幾種情況下回事不可見的:
- 圖層在屏幕邊界之外,或是在父圖層邊界之外。
- 完全在一個不透明圖層之后。
- 完全透明
Core Animation
非常擅長處理對視覺效果無意義的圖層。但是經常性地,你自己 的代碼會比Core Animation更早地想知道一個圖層是否是有用的。理想狀況下,在圖層對象在創建之前就想知道,以避免創建和配置不必要圖層的額外工作。
對象回收
處理巨大數量的相似視圖或圖層時還有一個技巧就是回收他們。對象回收在iOS 頗為常見; UITableView
和UICollectionView
都有用到, MKMapView
中的動畫pin
碼也有用到,還有其他很多例子。
對象回收的基礎原則就是你需要創建一個相似對象池。當一個對象的指定實例(本例子中指的是圖層)結束了使命,你把它添加到對象池中。每次當你需要一個實例時,你就從池中取出一個。當且僅當池中為空時再創建一個新的。
這樣做的好處在于避免了不斷創建和釋放對象(相當消耗資源,因為涉及到內存的分配和銷毀)而且也不必給相似實例重復賦值。
Core Graphics繪制
當排除掉對屏幕顯示沒有任何貢獻的圖層或者視圖之后,長遠看來,你可能仍然 需要減少圖層的數量。例如,如果你正在使用多個UILabel
或者UIImageView
實例去顯示固定內容,你可以把他們全部替換成一個單獨的視 圖,然后用- drawRect:
方法繪制出那些復雜的視圖層級。
這個提議看上去并不合理因為大家都知道軟件繪制行為要比GPU
合成要慢而且還 需要更多的內存空間,但是在因為圖層數量而使得性能受限的情況下,軟件繪制很 可能提高性能呢,因為它避免了圖層分配和操作問題。
你可以自己實驗一下這個情況,它包含了性能和柵格化的權衡,但是意味著你可 以從圖層樹上去掉子圖層(用shouldRasterize
,與完全遮擋圖層相反)。
-renderInContext: 方法
用Core Graphics
去繪制一個靜態布局有時候會比用層級的 UIView
實例來得快,但是使用UIView
實例要簡單得多而且比用手寫代碼寫出相同效果要可靠得 多,更邊說Interface Builder
來得直接明了。為了性能而舍棄這些便利實在是不應 該。
幸好,你不必這樣,如果大量的視圖或者圖層真的關聯到了屏幕上將會是一個大問題。沒有與圖層樹相關聯的圖層不會被送到渲染引擎,也沒有性能問題(在他們被創建和配置之后)。
使用 CALayer
的 -renderInContext:
方法,你可以將圖層及其子圖層快照進 一個Core Graphics
上下文然后得到一個圖片,它可以直接顯示在UIImageView
中,或者作為另一個圖層的contents
。不同于shouldRasterize
—— 要求圖層與圖層樹相關聯 —— ,這個方法沒有持續的 性能消耗。
當圖層內容改變時,刷新這張圖片的機會取決于你(不同于 shouldRasterize
,它自動地處理緩存和緩存驗證),但是一旦圖片被生成, 相比于讓Core Animation
處理一個復雜的圖層樹,你節省了相當客觀的性能。
總結
本章學習了使用Core Animation
圖層可能遇到的性能瓶頸,并討論了如何避免或 減小壓力。你學習了如何管理包含上千虛擬圖層的場景(事實上只創建了幾百 個)。同時也學習了一些有用的技巧,選擇性地選取光柵化或者繪制圖層內容在合 適的時候重新分配給CPU
和GPU
。這些就是我們要講的關于Core Animation
的全部 了(至少可以等到蘋果發明什么新的玩意兒)。