# 減少圖層數量
????初始化圖層,處理圖層,打包通過IPC發給渲染引擎,轉化成OpenGL幾何圖形,這些是一個圖層的大致資源開銷。事實上,一次性能夠在屏幕上顯示的最大圖層數量也是有限的。
????確切的限制數量取決于iOS設備,圖層類型,圖層內容和屬性等。但是總得說來可以容納上百或上千個,下面我們將演示即使圖層本身并沒有做什么也會遇到的性能問題。
## 裁切
????在對圖層做任何優化之前,你需要確定你不是在創建一些不可見的圖層,圖層在以下幾種情況下回事不可見的:
* 圖層在屏幕邊界之外,或是在父圖層邊界之外。
* 完全在一個不透明圖層之后。
* 完全透明
????Core Animation非常擅長處理對視覺效果無意義的圖層。但是經常性地,你自己的代碼會比Core Animation更早地想知道一個圖層是否是有用的。理想狀況下,在圖層對象在創建之前就想知道,以避免創建和配置不必要圖層的額外工作。
????舉個例子。清單15.3 的代碼展示了一個簡單的滾動3D圖層矩陣。這看上去很酷,尤其是圖層在移動的時候(見圖15.1),但是繪制他們并不是很麻煩,因為這些圖層就是一些簡單的矩形色塊。
清單15.3 繪制3D圖層矩陣
~~~
#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
{
[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
#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
{
[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 ()
@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`來抑制動畫效果。在之前并不需要這樣做,因為在顯示之前我們給所有圖層設置一次屬性。但是既然圖層正在被回收,禁止隱式動畫就有必要了,不然當屬性值改變時,圖層的隱式動畫就會被觸發。
## Core Graphics繪制
????當排除掉對屏幕顯示沒有任何貢獻的圖層或者視圖之后,長遠看來,你可能仍然需要減少圖層的數量。例如,如果你正在使用多個`UILabel`或者`UIImageView`實例去顯示固定內容,你可以把他們全部替換成一個單獨的視圖,然后用`-drawRect:`方法繪制出那些復雜的視圖層級。
????這個提議看上去并不合理因為大家都知道軟件繪制行為要比GPU合成要慢而且還需要更多的內存空間,但是在因為圖層數量而使得性能受限的情況下,軟件繪制很可能提高性能呢,因為它避免了圖層分配和操作問題。
????你可以自己實驗一下這個情況,它包含了性能和柵格化的權衡,但是意味著你可以從圖層樹上去掉子圖層(用`shouldRasterize`,與完全遮擋圖層相反)。
## -renderInContext: 方法
????用Core Graphics去繪制一個靜態布局有時候會比用層級的`UIView`實例來得快,但是使用`UIView`實例要簡單得多而且比用手寫代碼寫出相同效果要可靠得多,更邊說Interface Builder來得直接明了。為了性能而舍棄這些便利實在是不應該。
????幸好,你不必這樣,如果大量的視圖或者圖層真的關聯到了屏幕上將會是一個大問題。沒有與圖層樹相關聯的圖層不會被送到渲染引擎,也沒有性能問題(在他們被創建和配置之后)。
????使用`CALayer`的`-renderInContext:`方法,你可以將圖層及其子圖層快照進一個Core Graphics上下文然后得到一個圖片,它可以直接顯示在`UIImageView`中,或者作為另一個圖層的`contents`。不同于`shouldRasterize`?—— 要求圖層與圖層樹相關聯 —— ,這個方法沒有持續的性能消耗。
????當圖層內容改變時,刷新這張圖片的機會取決于你(不同于`shouldRasterize`,它自動地處理緩存和緩存驗證),但是一旦圖片被生成,相比于讓Core Animation處理一個復雜的圖層樹,你節省了相當客觀的性能。
- Introduction
- 1. 圖層樹
- 1.1 圖層與視圖
- 1.2 圖層的能力
- 1.3 使用圖層
- 1.4 總結
- 2. 寄宿圖
- 2.1 contents屬性
- 2.2 Custom Drawing
- 2.3 總結
- 3. 圖層幾何學
- 3.1 布局
- 3.2 錨點
- 3.3 坐標系
- 3.4 Hit Testing
- 3.5 自動布局
- 3.6 總結
- 4. 視覺效果
- 4.1 圓角
- 4.2 圖層邊框
- 4.3 陰影
- 4.4 圖層蒙板
- 4.5 拉伸過濾
- 4.6 組透明
- 4.7 總結
- 5. 變換
- 5.1 仿射變換
- 5.2 3D變換
- 5.3 固體對象
- 5.4 總結
- 6. 專用圖層
- 6.1 CAShapeLayer
- 6.2 CATextLayer
- 6.3 CATransformLayer
- 6.4 CAGradientLayer
- 6.5 CAReplicatorLayer
- 6.6 CAScrollLayer
- 6.7 CATiledLayer
- 6.8 CAEmitterLayer
- 6.9 CAEAGLLayer
- 6.10 AVPlayerLayer
- 6.11 總結
- 7. 隱式動畫
- 7.1 事務
- 7.2 完成塊
- 7.3 圖層行為
- 7.4 呈現與模型
- 7.5 總結
- 8. 顯式動畫
- 8.1 屬性動畫
- 8.2 動畫組
- 8.3 過渡
- 8.4 在動畫過程中取消動畫
- 8.5 總結
- 9. 圖層時間
- 9.1 CAMediaTiming協議
- 9.2 層級關系時間
- 9.3 手動動畫
- 9.4 總結
- 10. 緩沖
- 10.1 動畫速度
- 10.2 自定義緩沖函數
- 10.3 總結
- 11. 基于定時器的動畫
- 11.1 定時幀
- 11.2 物理模擬
- 12. 性能調優
- 12.1. CPU VS GPU
- 12.2 測量,而不是猜測
- 12.3 Instruments
- 12.4 總結
- 13. 高效繪圖
- 13.1 軟件繪圖
- 13.2 矢量圖形
- 13.3 臟矩形
- 13.4 異步繪制
- 13.5 總結
- 14. 圖像IO
- 14.1 加載和潛伏
- 14.2 緩存
- 14.3 文件格式
- 14.4 總結
- 15. 圖層性能
- 15.1 隱式繪制
- 15.2 離屏渲染
- 15.3 混合和過度繪制
- 15.4 減少圖層數量
- 15.5 總結