要更快性能,也要作對正確的事情。 ——Stephen R. Coveygit
在第14章『圖像IO』討論如何高效地載入和顯示圖像,經過視圖來避免可能引發動畫幀率降低的性能問題。在最後一章,咱們將着重圖層樹自己,以發掘最好的性能。github
寄宿圖能夠經過Core Graphics直接繪製,也能夠直接載入一個圖片文件並賦值給contents
屬性,或事先繪製一個屏幕以外的CGContext
上下文。在以前的兩章中咱們討論了這些場景下的優化。可是除了常見的顯式建立寄宿圖,你也能夠經過如下三種方式建立隱式的:1,使用特性的圖層屬性。2,特定的視圖。3,特定的圖層子類。緩存
瞭解這個狀況爲何發生什麼時候發生是很重要的,它可以讓你避免引入沒必要要的軟件繪製行爲。ide
CATextLayer
和UILabel
都是直接將文本繪製在圖層的寄宿圖中。事實上這兩種方式用了徹底不一樣的渲染方式:在iOS 6及以前,UILabel
用WebKit的HTML渲染引擎來繪製文本,而CATextLayer
用的是Core Text.後者渲染更迅速,因此在全部須要繪製大量文本的情形下都優先使用它吧。可是這兩種方法都用了軟件的方式繪製,所以他們實際上要比硬件加速合成方式要慢。佈局
不論如何,儘量地避免改變那些包含文本的視圖的frame,由於這樣作的話文本就須要重繪。例如,若是你想在圖層的角落裏顯示一段靜態的文本,可是這個圖層常常改動,你就應該把文本放在一個子圖層中。性能
在第四章『視覺效果』中咱們提到了CALayer
的shouldRasterize
屬性,它能夠解決重疊透明圖層的混合失靈問題。一樣在第12章『速度的曲調』中,它也是做爲繪製複雜圖層樹結構的優化方法。學習
啓用 shouldRasterize 屬性會將圖層繪製到一個屏幕以外的圖像。而後這個圖像將會被緩存起來並繪製到實際圖層的contents
和子圖層。若是有不少的子圖層或者有複雜的效果應用,這樣作就會比重繪全部事務的全部幀划得來得多。可是光柵化原始圖像須要時間,並且還會消耗額外的內存。優化
當咱們使用得當時,光柵化能夠提供很大的性能優點(如你在第12章所見),可是必定要避免做用在內容不斷變更的圖層上,不然它緩存方面的好處就會消失,並且會讓性能變的更糟。動畫
爲了檢測你是否正確地使用了光柵化方式,用Instrument查看一下Color Hits Green和Misses Red項目,是否已光柵化圖像被頻繁地刷新(這樣就說明圖層並非光柵化的好選擇,或則你無心間觸發了沒必要要的改變致使了重繪行爲)。ui
Offscreen rendering does not necessarily imply software drawing, but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed. The layer attributes that trigger offscreen rendering are as follows:
當圖層屬性的混合體被指定爲在未預合成以前不能直接在屏幕中繪製時,屏幕外渲染就被喚起了。屏幕外渲染並不意味着軟件繪製,可是它意味着圖層必須在被顯示以前在一個屏幕外上下文中被渲染(不論CPU仍是GPU)。圖層的如下屬性將會觸發屏幕外繪製:
圓角(當和maskToBounds
一塊兒使用時)
圖層蒙板
陰影
屏幕外渲染和咱們啓用光柵化時類似,除了它並無像光柵化圖層那麼消耗大,子圖層並無被影響到,並且結果也沒有被緩存,因此不會有長期的內存佔用。可是,若是太多圖層在屏幕外渲染依然會影響到性能。
有時候咱們能夠把那些須要屏幕外繪製的圖層開啓光柵化以做爲一個優化方式,前提是這些圖層並不會被頻繁地重繪。
對於那些須要動畫並且要在屏幕外渲染的圖層來講,你能夠用CAShapeLayer
,contentsCenter
或者shadowPath
來得到一樣的表現並且較少地影響到性能。
cornerRadius
和maskToBounds
獨立做用的時候都不會有太大的性能問題,可是當他倆結合在一塊兒,就觸發了屏幕外渲染。有時候你想顯示圓角並沿着圖層裁切子圖層的時候,你可能會發現你並不須要沿着圓角裁切,這個狀況下用CAShapeLayer
就能夠避免這個問題了。
你想要的只是圓角且沿着矩形邊界裁切,同時還不但願引發性能問題。其實你能夠用現成的UIBezierPath
的構造器 +bezierPathWithRoundedRect:cornerRadius: (見清單15.1).這樣作並不會比直接用cornerRadius
更快,可是它避免了性能問題。
清單15.1 用CAShapeLayer
畫一個圓角矩形
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>@interface ViewController () @property (nonatomic, weak) IBOutlet UIView *layerView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //create shape layer CAShapeLayer *blueLayer = [CAShapeLayer layer]; blueLayer.frame = CGRectMake(50, 50, 100, 100); blueLayer.fillColor = [UIColor blueColor].CGColor; blueLayer.path = [UIBezierPath bezierPathWithRoundedRect: CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath;  //add it to our view [self.layerView.layer addSublayer:blueLayer]; }@end
另外一個建立圓角矩形的方法就是用一個圓形內容圖片並結合第二章『寄宿圖』提到的 contentsCenter 屬性去建立一個可伸縮圖片(見清單15.2).理論上來講,這個應該比用CAShapeLayer
要快,由於一個可拉伸圖片只須要18個三角形(一個圖片是由一個3*3網格渲染而成),然而,許多都須要渲染成一個順滑的曲線。在實際應用上,兩者並無太大的區別。
清單15.2 用可伸縮圖片繪製圓角矩形
@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //create layer CALayer *blueLayer = [CALayer layer]; blueLayer.frame = CGRectMake(50, 50, 100, 100); blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0); blueLayer.contentsScale = [UIScreen mainScreen].scale; blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage; //add it to our view [self.layerView.layer addSublayer:blueLayer]; }@end
使用可伸縮圖片的優點在於它能夠繪製成任意邊框效果而不須要額外的性能消耗。舉個例子,可伸縮圖片甚至還能夠顯示出矩形陰影的效果。
在第2章咱們有提到shadowPath
屬性。若是圖層是一個簡單幾何圖形如矩形或者圓角矩形(假設不包含任何透明部分或者子圖層),建立出一個對應形狀的陰影路徑就比較容易,並且Core Animation繪製這個陰影也至關簡單,避免了屏幕外的圖層部分的預排版需求。這對性能來講頗有幫助。
若是你的圖層是一個更復雜的圖形,生成正確的陰影路徑可能就比較難了,這樣子的話你能夠考慮用繪圖軟件預先生成一個陰影背景圖。
在第12章有提到,GPU每一幀能夠繪製的像素有一個最大限制(就是所謂的fill rate),這個狀況下能夠輕易地繪製整個屏幕的全部像素。可是若是因爲重疊圖層的關係須要不停地重繪同一區域的話,掉幀就可能發生了。
GPU會放棄繪製那些徹底被其餘圖層遮擋的像素,可是要計算出一個圖層是否被遮擋也是至關複雜而且會消耗處理器資源。一樣,合併不一樣圖層的透明重疊像素(即混合)消耗的資源也是至關客觀的。因此爲了加速處理進程,不到必須時刻不要使用透明圖層。任何狀況下,你應該這樣作:
給視圖的backgroundColor
屬性設置一個固定的,不透明的顏色
設置opaque
屬性爲YES
這樣作減小了混合行爲(由於編譯器知道在圖層以後的東西都不會對最終的像素顏色產生影響)而且計算獲得了加速,避免了過分繪製行爲由於Core Animation能夠捨棄全部被徹底遮蓋住的圖層,而不用每一個像素都去計算一遍。
若是用到了圖像,儘可能避免透明除非很是必要。若是圖像要顯示在一個固定的背景顏色或是固定的背景圖以前,你不必相對前景移動,你只須要預填充背景圖片就能夠避免運行時混色了。
若是是文本的話,一個白色背景的UILabel
(或者其餘顏色)會比透明背景要更高效。
最後,明智地使用shouldRasterize
屬性,能夠將一個固定的圖層體系摺疊成單張圖片,這樣就不須要每一幀從新合成了,也就不會有由於子圖層之間的混合和過分繪製的性能問題了。
初始化圖層,處理圖層,打包經過IPC發給渲染引擎,轉化成OpenGL幾何圖形,這些是一個圖層的大體資源開銷。事實上,一次性可以在屏幕上顯示的最大圖層數量也是有限的。
確切的限制數量取決於iOS設備,圖層類型,圖層內容和屬性等。可是總得說來能夠容納上百或上千個,下面咱們將演示即便圖層自己並無作什麼也會遇到的性能問題。
在對圖層作任何優化以前,你須要肯定你不是在建立一些不可見的圖層,圖層在如下幾種狀況下回事不可見的:
圖層在屏幕邊界以外,或是在父圖層邊界以外。
徹底在一個不透明圖層以後。
徹底透明
Core Animation很是擅長處理對視覺效果無心義的圖層。可是常常性地,你本身的代碼會比Core Animation更早地想知道一個圖層是不是有用的。理想情況下,在圖層對象在建立以前就想知道,以免建立和配置沒必要要圖層的額外工做。
舉個例子。清單15.3 的代碼展現了一個簡單的滾動3D圖層矩陣。這看上去很酷,尤爲是圖層在移動的時候(見圖15.1),可是繪製他們並非很麻煩,由於這些圖層就是一些簡單的矩形色塊。
清單15.3 繪製3D圖層矩陣
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>#define WIDTH 10#define HEIGHT 10#define DEPTH 10#define SIZE 100#define SPACING 150#define CAMERA_DISTANCE 500@interface ViewController ()  @property (nonatomic, strong) IBOutlet UIScrollView *scrollView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; //create layers for (int z = DEPTH - 1; z >= 0; z--) { for (int y = 0; y < HEIGHT; y++) { for (int x = 0; x < WIDTH; x++) { //create layer CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [self.scrollView.layer addSublayer:layer]; } } }  //log NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH); }@end
圖15.1 滾動的3D圖層矩陣
WIDTH
,HEIGHT
和DEPTH
常量控制着圖層的生成。在這個狀況下,咱們獲得的是10*10*10個圖層,總量爲1000個,不過一次性顯示在屏幕上的大約就幾百個。
若是把WIDTH
和HEIGHT
常量增長到100,咱們的程序就會慢得像龜爬了。這樣咱們有了100000個圖層,性能降低一點兒也不奇怪。
可是顯示在屏幕上的圖層數量並無增長,那麼根本沒有額外的東西須要繪製。程序慢下來的緣由實際上是由於在管理這些圖層上花掉了很多功夫。他們大部分對渲染的最終結果沒有貢獻,可是在丟棄這麼圖層以前,Core Animation要強制計算每一個圖層的位置,就這樣,咱們的幀率就慢了下來。
咱們的圖層是被安排在一個均勻的柵格中,咱們能夠計算出哪些圖層會被最終顯示在屏幕上,根本不須要對每一個圖層的位置進行計算。這個計算並不簡單,由於咱們還要考慮到透視的問題。若是咱們直接這樣作了,Core Animation就不用費神了。
既然這樣,讓咱們來重構咱們的代碼吧。改造後,隨着視圖的滾動動態地實例化圖層而不是事先都分配好。這樣,在創造他們以前,咱們就能夠計算出是否須要他。接着,咱們增長一些代碼去計算可視區域這樣就能夠排除區域以外的圖層了。清單15.4是改造後的結果。
清單15.4 排除可視區域以外的圖層
#import "ViewController.h"#import <QuartzCore/QuartzCore.h>#define WIDTH 100#define HEIGHT 100#define DEPTH 10#define SIZE 100#define SPACING 150#define CAMERA_DISTANCE 500#define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE)@interface ViewController () <UIScrollViewDelegate>@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; } - (void)viewDidLayoutSubviews { [self updateLayers]; }- (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLayers]; }- (void)updateLayers { //calculate clipping bounds CGRect bounds = self.scrollView.bounds; bounds.origin = self.scrollView.contentOffset; bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); //create layers NSMutableArray *visibleLayers = [NSMutableArray array]; for (int z = DEPTH - 1; z >= 0; z--) { //increase bounds size to compensate for perspective CGRect adjusted = bounds; adjusted.size.width /= PERSPECTIVE(z*SPACING); adjusted.size.height /= PERSPECTIVE(z*SPACING); adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; for (int y = 0; y < HEIGHT; y++) { //check if vertically outside visible rect if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height) { continue; } for (int x = 0; x < WIDTH; x++) { //check if horizontally outside visible rect if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width) { continue; }  //create layer CALayer *layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [visibleLayers addObject:layer]; } } } //update layers self.scrollView.layer.sublayers = visibleLayers; //log NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH); }@end
這個計算機制並不具備普適性,可是原則上是同樣。(當你用一個UITableView
或者UICollectionView
時,系統作了相似的事情)。這樣作的結果?咱們的程序能夠處理成百上千個『虛擬』圖層並且徹底沒有性能問題!由於它不須要一次性實例化幾百個圖層。
處理巨大數量的類似視圖或圖層時還有一個技巧就是回收他們。對象回收在iOS頗爲常見;UITableView
和UICollectionView
都有用到,MKMapView
中的動畫pin碼也有用到,還有其餘不少例子。
對象回收的基礎原則就是你須要建立一個類似對象池。當一個對象的指定實例(本例子中指的是圖層)結束了使命,你把它添加到對象池中。每次當你須要一個實例時,你就從池中取出一個。當且僅當池中爲空時再建立一個新的。
這樣作的好處在於避免了不斷建立和釋放對象(至關消耗資源,由於涉及到內存的分配和銷燬)並且也沒必要給類似實例重複賦值。
好了,讓咱們再次更新代碼吧(見清單15.5)
清單15.5 經過回收減小沒必要要的分配
@interface ViewController () <UIScrollViewDelegate>@property (nonatomic, weak) IBOutlet UIScrollView *scrollView; @property (nonatomic, strong) NSMutableSet *recyclePool;@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; //create recycle pool self.recyclePool = [NSMutableSet set]; //set content size self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); //set up perspective transform CATransform3D transform = CATransform3DIdentity; transform.m34 = -1.0 / CAMERA_DISTANCE; self.scrollView.layer.sublayerTransform = transform; }- (void)viewDidLayoutSubviews { [self updateLayers]; }- (void)scrollViewDidScroll:(UIScrollView *)scrollView { [self updateLayers]; }- (void)updateLayers {  //calculate clipping bounds CGRect bounds = self.scrollView.bounds; bounds.origin = self.scrollView.contentOffset; bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); //add existing layers to pool [self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers]; //disable animation [CATransaction begin]; [CATransaction setDisableActions:YES]; //create layers NSInteger recycled = 0; NSMutableArray *visibleLayers = [NSMutableArray array]; for (int z = DEPTH - 1; z >= 0; z--) { //increase bounds size to compensate for perspective CGRect adjusted = bounds; adjusted.size.width /= PERSPECTIVE(z*SPACING); adjusted.size.height /= PERSPECTIVE(z*SPACING); adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; for (int y = 0; y < HEIGHT; y++) { //check if vertically outside visible rect if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height) { continue; } for (int x = 0; x < WIDTH; x++) { //check if horizontally outside visible rect if (x*SPACING < adjusted.origin.x || x*SPACING >= adjusted.origin.x + adjusted.size.width) { continue; } //recycle layer if available CALayer *layer = [self.recyclePool anyObject]; if (layer) {  recycled ++; [self.recyclePool removeObject:layer]; } else { layer = [CALayer layer]; layer.frame = CGRectMake(0, 0, SIZE, SIZE); } //set position layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; //set background color layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; //attach to scroll view [visibleLayers addObject:layer]; } } } [CATransaction commit]; //update layers self.scrollView.layer.sublayers = visibleLayers; //log NSLog(@"displayed: %i/%i recycled: %i", [visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled); }@end
本例中,咱們只有圖層對象這一種類型,可是UIKit有時候用一個標識符字符串來區分存儲在不一樣對象池中的不一樣的可回收對象類型。
你可能注意到當設置圖層屬性時咱們用了一個CATransaction
來抑制動畫效果。在以前並不須要這樣作,由於在顯示以前咱們給全部圖層設置一次屬性。可是既然圖層正在被回收,禁止隱式動畫就有必要了,否則當屬性值改變時,圖層的隱式動畫就會被觸發。
當排除掉對屏幕顯示沒有任何貢獻的圖層或者視圖以後,長遠看來,你可能仍然須要減小圖層的數量。例如,若是你正在使用多個UILabel
或者UIImageView
實例去顯示固定內容,你能夠把他們所有替換成一個單獨的視圖,而後用-drawRect:
方法繪製出那些複雜的視圖層級。
這個提議看上去並不合理由於你們都知道軟件繪製行爲要比GPU合成要慢並且還須要更多的內存空間,可是在由於圖層數量而使得性能受限的狀況下,軟件繪製極可能提升性能呢,由於它避免了圖層分配和操做問題。
你能夠本身實驗一下這個狀況,它包含了性能和柵格化的權衡,可是意味着你能夠從圖層樹上去掉子圖層(用shouldRasterize
,與徹底遮擋圖層相反)。
用Core Graphics去繪製一個靜態佈局有時候會比用層級的UIView
實例來得快,可是使用UIView
實例要簡單得多並且比用手寫代碼寫出相同效果要可靠得多,更邊說Interface Builder來得直接明瞭。爲了性能而捨棄這些便利實在是不該該。
幸虧,你沒必要這樣,若是大量的視圖或者圖層真的關聯到了屏幕上將會是一個大問題。沒有與圖層樹相關聯的圖層不會被送到渲染引擎,也沒有性能問題(在他們被建立和配置以後)。
使用CALayer
的-renderInContext:
方法,你能夠將圖層及其子圖層快照進一個Core Graphics上下文而後獲得一個圖片,它能夠直接顯示在UIImageView
中,或者做爲另外一個圖層的contents
。不一樣於shouldRasterize
—— 要求圖層與圖層樹相關聯 —— ,這個方法沒有持續的性能消耗。
當圖層內容改變時,刷新這張圖片的機會取決於你(不一樣於shouldRasterize
,它自動地處理緩存和緩存驗證),可是一旦圖片被生成,相比於讓Core Animation處理一個複雜的圖層樹,你節省了至關客觀的性能。
本章學習了使用Core Animation圖層可能遇到的性能瓶頸,並討論瞭如何避免或減少壓力。你學習瞭如何管理包含上千虛擬圖層的場景(事實上只建立了幾百個)。同時也學習了一些有用的技巧,選擇性地選取光柵化或者繪製圖層內容在合適的時候從新分配給CPU和GPU。這些就是咱們要講的關於Core Animation的所有了(至少能夠等到蘋果發明什麼新的玩意兒)。