1. 程式人生 > >減少圖層數量(圖層性能 15.4 )

減少圖層數量(圖層性能 15.4 )

gpu 重構 art 直接 這一 增加 orm eight 手寫

減少圖層數量

初始化圖層,處理圖層,打包通過IPC發給渲染引擎,轉化成OpenGL幾何圖形,這些是一個圖層的大致資源開銷。事實上,一次性能夠在屏幕上顯示的最大圖層數量也是有限的。

確切的限制數量取決於iOS設備,圖層類型,圖層內容和屬性等。但是總得說來可以容納上百或上千個,下面我們將演示即使圖層本身並沒有做什麽也會遇到的性能問題。

裁切

在對圖層做任何優化之前,你需要確定你不是在創建一些不可見的圖層,圖層在以下幾種情況下回事不可見的:

  • 圖層在屏幕邊界之外,或是在父圖層邊界之外。
  • 完全在一個不透明圖層之後。
  • 完全透明

Core Animation非常擅長處理對視覺效果無意義的圖層。但是經常性地,你自己的代碼會比Core Animation更早地想知道一個圖層是否是有用的。理想狀況下,在圖層對象在創建之前就想知道,以避免創建和配置不必要圖層的額外工作。

舉個例子。清單15.3 的代碼展示了一個簡單的滾動3D圖層矩陣。這看上去很酷,尤其是圖層在移動的時候(見圖15.1),但是繪制他們並不是很麻煩,因為這些圖層就是一些簡單的矩形色塊。

清單15.3 繪制3D圖層矩陣

技術分享
 1 #import "ViewController.h"
 2 #import 
 3 
 4 #define WIDTH 10
 5 #define HEIGHT 10
 6 #define DEPTH 10
 7 #define SIZE 100
 8 #define SPACING 150
 9 #define CAMERA_DISTANCE 500
10 
11 @interface ViewController ()
12 ? 13 @property (nonatomic, strong) IBOutlet UIScrollView *scrollView; 14 15 @end 16 17 @implementation ViewController 18 19 - (void)viewDidLoad 20 { 21 [super viewDidLoad]; 22 23 //set content size 24 self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
25 26 //set up perspective transform 27 CATransform3D transform = CATransform3DIdentity; 28 transform.m34 = -1.0 / CAMERA_DISTANCE; 29 self.scrollView.layer.sublayerTransform = transform; 30 31 //create layers 32 for (int z = DEPTH - 1; z >= 0; z--) { 33 for (int y = 0; y < HEIGHT; y++) { 34 for (int x = 0; x < WIDTH; x++) { 35 //create layer 36 CALayer *layer = [CALayer layer]; 37 layer.frame = CGRectMake(0, 0, SIZE, SIZE); 38 layer.position = CGPointMake(x*SPACING, y*SPACING); 39 layer.zPosition = -z*SPACING; 40 //set background color 41 layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; 42 //attach to scroll view 43 [self.scrollView.layer addSublayer:layer]; 44 } 45 } 46 } 47 ? 48 //log 49 NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH); 50 } 51 @end
View Code

技術分享

圖15.1 滾動的3D圖層矩陣

WIDTHHEIGHTDEPTH常量控制著圖層的生成。在這個情況下,我們得到的是101010個圖層,總量為1000個,不過一次性顯示在屏幕上的大約就幾百個。

如果把WIDTHHEIGHT常量增加到100,我們的程序就會慢得像龜爬了。這樣我們有了100000個圖層,性能下降一點兒也不奇怪。

但是顯示在屏幕上的圖層數量並沒有增加,那麽根本沒有額外的東西需要繪制。程序慢下來的原因其實是因為在管理這些圖層上花掉了不少功夫。他們大部分對渲染的最終結果沒有貢獻,但是在丟棄這麽圖層之前,Core Animation要強制計算每個圖層的位置,就這樣,我們的幀率就慢了下來。

我們的圖層是被安排在一個均勻的柵格中,我們可以計算出哪些圖層會被最終顯示在屏幕上,根本不需要對每個圖層的位置進行計算。這個計算並不簡單,因為我們還要考慮到透視的問題。如果我們直接這樣做了,Core Animation就不用費神了。

既然這樣,讓我們來重構我們的代碼吧。改造後,隨著視圖的滾動動態地實例化圖層而不是事先都分配好。這樣,在創造他們之前,我們就可以計算出是否需要他。接著,我們增加一些代碼去計算可視區域這樣就可以排除區域之外的圖層了。清單15.4是改造後的結果。

清單15.4 排除可視區域之外的圖層

技術分享
 1 #import "ViewController.h"
 2 #import 
 3 
 4 #define WIDTH 100
 5 #define HEIGHT 100
 6 #define DEPTH 10
 7 #define SIZE 100
 8 #define SPACING 150
 9 #define CAMERA_DISTANCE 500
10 #define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE)
11 
12 @interface ViewController () 
13 
14 @property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
15 
16 @end
17 
18 @implementation ViewController
19 
20 - (void)viewDidLoad
21 {
22     [super viewDidLoad];
23     //set content size
24     self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
25     //set up perspective transform
26     CATransform3D transform = CATransform3DIdentity;
27     transform.m34 = -1.0 / CAMERA_DISTANCE;
28     self.scrollView.layer.sublayerTransform = transform;
29 }
30 ?
31 - (void)viewDidLayoutSubviews
32 {
33     [self updateLayers];
34 }
35 
36 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
37 {
38     [self updateLayers];
39 }
40 
41 - (void)updateLayers
42 {
43     //calculate clipping bounds
44     CGRect bounds = self.scrollView.bounds;
45     bounds.origin = self.scrollView.contentOffset;
46     bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
47     //create layers
48     NSMutableArray *visibleLayers = [NSMutableArray array];
49     for (int z = DEPTH - 1; z >= 0; z--)
50     {
51         //increase bounds size to compensate for perspective
52         CGRect adjusted = bounds;
53         adjusted.size.width /= PERSPECTIVE(z*SPACING);
54         adjusted.size.height /= PERSPECTIVE(z*SPACING);
55         adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2;
56         adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
57         for (int y = 0; y < HEIGHT; y++) {
58         //check if vertically outside visible rect
59             if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height)
60             {
61                 continue;
62             }
63             for (int x = 0; x < WIDTH; x++) {
64                 //check if horizontally outside visible rect
65                 if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width)
66                 {
67                     continue;
68                 }
69                 ?
70                 //create layer
71                 CALayer *layer = [CALayer layer];
72                 layer.frame = CGRectMake(0, 0, SIZE, SIZE);
73                 layer.position = CGPointMake(x*SPACING, y*SPACING);
74                 layer.zPosition = -z*SPACING;
75                 //set background color
76                 layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
77                 //attach to scroll view
78                 [visibleLayers addObject:layer];
79             }
80         }
81     }
82     //update layers
83     self.scrollView.layer.sublayers = visibleLayers;
84     //log
85     NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH);
86 }
87 @end
View Code

這個計算機制並不具有普適性,但是原則上是一樣。(當你用一個UITableView或者UICollectionView時,系統做了類似的事情)。這樣做的結果?我們的程序可以處理成百上千個『虛擬』圖層而且完全沒有性能問題!因為它不需要一次性實例化幾百個圖層。

對象回收

處理巨大數量的相似視圖或圖層時還有一個技巧就是回收他們。對象回收在iOS頗為常見;UITableViewUICollectionView都有用到,MKMapView中的動畫pin碼也有用到,還有其他很多例子。

對象回收的基礎原則就是你需要創建一個相似對象池。當一個對象的指定實例(本例子中指的是圖層)結束了使命,你把它添加到對象池中。每次當你需要一個實例時,你就從池中取出一個。當且僅當池中為空時再創建一個新的。

這樣做的好處在於避免了不斷創建和釋放對象(相當消耗資源,因為涉及到內存的分配和銷毀)而且也不必給相似實例重復賦值。

好了,讓我們再次更新代碼吧(見清單15.5)

清單15.5 通過回收減少不必要的分配

技術分享
 1 @interface ViewController () 
 2 
 3 @property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
 4 @property (nonatomic, strong) NSMutableSet *recyclePool;
 5 
 6 @end
 7 
 8 @implementation ViewController
 9 
10 - (void)viewDidLoad
11 {
12     [super viewDidLoad]; //create recycle pool
13     self.recyclePool = [NSMutableSet set];
14     //set content size
15     self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING);
16     //set up perspective transform
17     CATransform3D transform = CATransform3DIdentity;
18     transform.m34 = -1.0 / CAMERA_DISTANCE;
19     self.scrollView.layer.sublayerTransform = transform;
20 }
21 
22 - (void)viewDidLayoutSubviews
23 {
24     [self updateLayers];
25 }
26 
27 - (void)scrollViewDidScroll:(UIScrollView *)scrollView
28 {
29     [self updateLayers];
30 }
31 
32 - (void)updateLayers {
33     ?
34     //calculate clipping bounds
35     CGRect bounds = self.scrollView.bounds;
36     bounds.origin = self.scrollView.contentOffset;
37     bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2);
38     //add existing layers to pool
39     [self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers];
40     //disable animation
41     [CATransaction begin];
42     [CATransaction setDisableActions:YES];
43     //create layers
44     NSInteger recycled = 0;
45     NSMutableArray *visibleLayers = [NSMutableArray array];
46     for (int z = DEPTH - 1; z >= 0; z--)
47     {
48         //increase bounds size to compensate for perspective
49         CGRect adjusted = bounds;
50         adjusted.size.width /= PERSPECTIVE(z*SPACING);
51         adjusted.size.height /= PERSPECTIVE(z*SPACING);
52         adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2;
53         for (int y = 0; y < HEIGHT; y++) {
54             //check if vertically outside visible rect
55             if (y*SPACING < adjusted.origin.y ||
56                 y*SPACING >= adjusted.origin.y + adjusted.size.height)
57             {
58                 continue;
59             }
60             for (int x = 0; x < WIDTH; x++) {
61                 //check if horizontally outside visible rect
62                 if (x*SPACING < adjusted.origin.x ||
63                     x*SPACING >= adjusted.origin.x + adjusted.size.width)
64                 {
65                     continue;
66                 }
67                 //recycle layer if available
68                 CALayer *layer = [self.recyclePool anyObject]; if (layer)
69                 {
70                     ?
71                     recycled ++;
72                     [self.recyclePool removeObject:layer]; }
73                 else
74                 {
75                     layer = [CALayer layer];
76                     layer.frame = CGRectMake(0, 0, SIZE, SIZE); }
77                 //set position
78                 layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING;
79                 //set background color
80                 layer.backgroundColor =
81                 [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor;
82                 //attach to scroll view
83                 [visibleLayers addObject:layer]; }
84         } }
85     [CATransaction commit]; //update layers
86     self.scrollView.layer.sublayers = visibleLayers;
87     //log
88     NSLog(@"displayed: %i/%i recycled: %i",
89           [visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled);
90 }
91 @end
View Code

本例中,我們只有圖層對象這一種類型,但是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處理一個復雜的圖層樹,你節省了相當客觀的性能。

減少圖層數量(圖層性能 15.4 )