本文轉載自:http://www.cocoachina.com/ios/20150106/10840.html? 為了防止cocochina以后刪除該文章,故轉載至此;
圖層性能
要更快性能,也要做對正確的事情。 ——Stephen R. Covey
在第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:(見清單15.1).這樣做并不會比直接用cornerRadius更快,但是它避免了性能問題。
清單15.2 用CAShapeLayer畫一個圓角矩形
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19#import?"ViewController.h"
#import?@interface?ViewController?()
@property?(nonatomic,?weak)?IBOutlet?UIView?*layerView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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
可伸縮圖片
另一個創建圓角矩形的方法就是用一個圓形內容圖片并結合第二章『寄宿圖』提到的contensCenter屬性去創建一個可伸縮圖片(見清單15.2).理論上來說,這個應該比用CAShapeLayer要快,因為一個可拉伸圖片只需要18個三角形(一個圖片是由一個3*3網格渲染而成),然而,許多都需要渲染成一個順滑的曲線。在實際應用上,二者并沒有太大的區別。
清單15.2 用可伸縮圖片繪制圓角矩形
1
2
3
4
5
6
7
8
9
10
11
12
13
14@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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
使用可伸縮圖片的優勢在于它可以繪制成任意邊框效果而不需要額外的性能消耗。舉個例子,可伸縮圖片甚至還可以顯示出矩形陰影的效果。
shadowPath
在第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圖層矩陣
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39#import?"ViewController.h"
#import?#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
{
[superviewDidLoad];
//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 排除可視區域之外的圖層
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77#import?"ViewController.h"
#import?#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?()?@property?(nonatomic,?weak)?IBOutlet?UIScrollView?*scrollView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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 通過回收減少不必要的分配
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82@interface?ViewController?()?@property?(nonatomic,?weak)?IBOutlet?UIScrollView?*scrollView;
@property?(nonatomic,?strong)?NSMutableSet?*recyclePool;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];//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.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來抑制動畫效果。在之前并不需要這樣做,因為在顯示之前我們給所有圖層設置一次屬性。但是既然圖層正在被回收,禁止隱式動畫就有必要了,不然當屬性值改變時,圖層的隱式動畫就會被觸發。
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的全部了(至少可以等到蘋果發明什么新的玩意兒)