Instruments
Instruments是Xcode套件中沒有被充分利用的一個工具。很多iOS開發者從沒用過Instruments,或者只是用Leaks工具檢測循環引用。實際上有很多Instruments工具,包括為動畫性能調優的東西。
你可以通過在菜單中選擇Profile選項來打開Instruments(在這之前,記住要把目標設置成iOS設備,而不是模擬器)。然后將會顯示出圖12.1(如果沒有看到所有選項,你可能設置成了模擬器選項)。
圖12.1 Instruments工具選項窗口
就像之前提到的那樣,你應該始終將程序設置成發布選項。幸運的是,配置文件默認就是發布選項,所以你不需要在分析的時候調整編譯策略。
我們將討論如下幾個工具:
-
時間分析器 - 用來測量被方法/函數打斷的CPU使用情況。
-
Core Animation - 用來調試各種Core Animation性能問題。
-
OpenGL ES驅動 - 用來調試GPU性能問題。這個工具在編寫Open GL代碼的時候很有用,但有時也用來處理Core Animation的工作。
Instruments的一個很棒的功能在於它可以創建我們自定義的工具集。除了你初始選擇的工具之外,如果在Instruments中打開Library窗口,你可以拖拽別的工具到左側邊欄。我們將創建以上我們提到的三個工具,然后就可以並行使用了(見圖12.2)。
圖12.2 添加額外的工具到Instruments側邊欄
時間分析器
時間分析器工具用來檢測CPU的使用情況。它可以告訴我們程序中的哪個方法正在消耗大量的CPU時間。使用大量的CPU並不一定是個問題 - 你可能期望動畫路徑對CPU非常依賴,因為動畫往往是iOS設備中最苛刻的任務。
但是如果你有性能問題,查看CPU時間對於判斷性能是不是和CPU相關,以及定位到函數都很有幫助(見圖12.3)。
圖12.3 時間分析器工具
時間分析器有一些選項來幫助我們定位到我們關心的的方法。可以使用左側的復選框來打開。其中最有用的是如下幾點:
-
通過線程分離 - 這可以通過執行的線程進行分組。如果代碼被多線程分離的話,那么就可以判斷到底是哪個線程造成了問題。
-
隱藏系統庫 - 可以隱藏所有蘋果的框架代碼,來幫助我們尋找哪一段代碼造成了性能瓶頸。由於我們不能優化框架方法,所以這對定位到我們能實際修復的代碼很有用。
-
只顯示Obj-C代碼 - 隱藏除了Objective-C之外的所有代碼。大多數內部的Core Animation代碼都是用C或者C++函數,所以這對我們集中精力到我們代碼中顯式調用的方法就很有用。
Core Animation
Core Animation工具用來監測Core Animation性能。它給我們提供了周期性的FPS,並且考慮到了發生在程序之外的動畫(見圖12.4)。
圖12.4 使用可視化調試選項的Core Animation工具
Core Animation工具也提供了一系列復選框選項來幫助調試渲染瓶頸:
-
Color Blended Layers - 這個選項基於渲染程度對屏幕中的混合區域進行綠到紅的高亮(也就是多個半透明圖層的疊加)。由於重繪的原因,混合對GPU性能會有影響,同時也是滑動或者動畫幀率下降的罪魁禍首之一。
-
ColorHitsGreenandMissesRed - 當使用
shouldRasterizep
屬性的時候,耗時的圖層繪制會被緩存,然后當做一個簡單的扁平圖片呈現。當緩存再生的時候這個選項就用紅色對柵格化圖層進行了高亮。如果緩存頻繁再生的話,就意味着柵格化可能會有負面的性能影響了(更多關於使用shouldRasterize
的細節見第15章“圖層性能”)。 -
Color Copied Images - 有時候寄宿圖片的生成意味着Core Animation被強制生成一些圖片,然后發送到渲染服務器,而不是簡單的指向原始指針。這個選項把這些圖片渲染成藍色。復制圖片對內存和CPU使用來說都是一項非常昂貴的操作,所以應該盡可能的避免。
-
Color Immediately - 通常Core Animation Instruments以每毫秒10次的頻率更新圖層調試顏色。對某些效果來說,這顯然太慢了。這個選項就可以用來設置每幀都更新(可能會影響到渲染性能,而且會導致幀率測量不准,所以不要一直都設置它)。
-
Color Misaligned Images - 這里會高亮那些被縮放或者拉伸以及沒有正確對齊到像素邊界的圖片(也就是非整型坐標)。這些中的大多數通常都會導致圖片的不正常縮放,如果把一張大圖當縮略圖顯示,或者不正確地模糊圖像,那么這個選項將會幫你識別出問題所在。
-
Color Offscreen-Rendered Yellow - 這里會把那些需要離屏渲染的圖層高亮成黃色。這些圖層很可能需要用
shadowPath
或者shouldRasterize
來優化。 -
Color OpenGL Fast Path Blue - 這個選項會對任何直接使用OpenGL繪制的圖層進行高亮。如果僅僅使用UIKit或者Core Animation的API,那么不會有任何效果。如果使用
GLKView
或者CAEAGLLayer
,那如果不顯示藍色塊的話就意味着你正在強制CPU渲染額外的紋理,而不是繪制到屏幕。 -
Flash Updated Regions - 這個選項會對重繪的內容高亮成黃色(也就是任何在軟件層面使用Core Graphics繪制的圖層)。這種繪圖的速度很慢。如果頻繁發生這種情況的話,這意味着有一個隱藏的bug或者說通過增加緩存或者使用替代方案會有提升性能的空間。
這些高亮圖層的選項同樣在iOS模擬器的調試菜單也可用(圖12.5)。我們之前說過用模擬器測試性能並不好,但如果你能通過這些高亮選項識別出性能問題出在什么地方的話,那么使用iOS模擬器來驗證問題是否解決也是比真機測試更有效的。
圖12.5 iOS模擬器中Core Animation可視化調試選項
OpenGL ES驅動
OpenGL ES驅動工具可以幫你測量GPU的利用率,同樣也是一個很好的來判斷和GPU相關動畫性能的指示器。它同樣也提供了類似Core Animation那樣顯示FPS的工具(圖12.6)。
圖12.6 OpenGL ES驅動工具
側欄的郵編是一系列有用的工具。其中和Core Animation性能最相關的是如下幾點:
-
Renderer Utilization - 如果這個值超過了~50%,就意味着你的動畫可能對幀率有所限制,很可能因為離屏渲染或者是重繪導致的過度混合。
-
Tiler Utilization - 如果這個值超過了~50%,就意味着你的動畫可能限制於幾何結構方面,也就是在屏幕上有太多的圖層占用了。
一個可用的案例
現在我們已經對Instruments中動畫性能工具非常熟悉了,那么可以用它在現實中解決一些實際問題。
我們創建一個簡單的顯示模擬聯系人姓名和頭像列表的應用。注意即使把頭像圖片存在應用本地,為了使應用看起來更真實,我們分別實時加載圖片,而不是用–imageNamed:
預加載。同樣添加一些圖層陰影來使得列表顯示得更真實。清單12.1展示了最初版本的實現。
清單12.1 使用假數據的一個簡單聯系人列表
#import "ViewController.h"
#import
@interface ViewController ()
@property (nonatomic, strong) NSArray *items;
@property (nonatomic, weak) IBOutlet UITableView *tableView;
@end
@implementation ViewController
- (NSString *)randomName
{
NSArray *first = @[@"Alice", @"Bob", @"Bill", @"Charles", @"Dan", @"Dave", @"Ethan", @"Frank"];
NSArray *last = @[@"Appleseed", @"Bandicoot", @"Caravan", @"Dabble", @"Ernest", @"Fortune"];
NSUInteger index1 = (rand()/(double)INT_MAX) * [first count];
NSUInteger index2 = (rand()/(double)INT_MAX) * [last count];
return [NSString stringWithFormat:@"%@ %@", first[index1], last[index2]];
}
- (NSString *)randomAvatar
{
NSArray *images = @[@"Snowman", @"Igloo", @"Cone", @"Spaceship", @"Anchor", @"Key"];
NSUInteger index = (rand()/(double)INT_MAX) * [images count];
return images[index];
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up data
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {
//add name
[array addObject:@{@"name": [self randomName], @"image": [self randomAvatar]}];
}
self.items = array;
//register cell class
[self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.items count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath];
//load image
NSDictionary *item = self.items[indexPath.row];
NSString *filePath = [[NSBundle mainBundle] pathForResource:item[@"image"] ofType:@"png"];
//set image and text
cell.imageView.image = [UIImage imageWithContentsOfFile:filePath];
cell.textLabel.text = item[@"name"];
//set image shadow
cell.imageView.layer.shadowOffset = CGSizeMake(0, 5);
cell.imageView.layer.shadowOpacity = 0.75;
cell.clipsToBounds = YES;
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
return cell;
}
@end
當快速滑動的時候就會非常卡(見圖12.7的FPS計數器)。
圖12.7 滑動幀率降到15FPS
僅憑直覺,我們猜測性能瓶頸應該在圖片加載。我們實時從閃存加載圖片,而且沒有緩存,所以很可能是這個原因。我們可以用一些很贊的代碼修復,然后使用GCD異步加載圖片,然后緩存。。。等一下,在開始編碼之前,測試一下假設是否成立。首先用我們的三個Instruments工具分析一下程序來定位問題。我們推測問題可能和圖片加載相關,所以用Time Profiler工具來試試(圖12.8)。
圖12.8 用The timing profile分析聯系人列表
-tableView:cellForRowAtIndexPath:
中的CPU時間總利用率只有~28%(也就是加載頭像圖片的地方),非常低。於是建議是CPU/IO並不是真正的限制因素。然后看看是不是GPU的問題:在OpenGL ES Driver工具中檢測GPU利用率(圖12.9)。
圖12.9 OpenGL ES Driver工具顯示的GPU利用率
渲染服務利用率的值達到51%和63%。看起來GPU需要做很多工作來渲染聯系人列表。
為什么GPU利用率這么高呢?我們來用Core Animation調試工具選項來檢查屏幕。首先打開Color Blended Layers(圖12.10)。
圖12.10 使用Color Blended Layers選項調試程序
屏幕中所有紅色的部分都意味着字符標簽視圖的高級別混合,這很正常,因為我們把背景設置成了透明色來顯示陰影效果。這就解釋了為什么渲染利用率這么高了。
那么離屏繪制呢?打開Core Animation工具的Color Offscreen - Rendered Yellow選項(圖12.11)。
圖12.11 Color Offscreen–Rendered Yellow選項
所有的表格單元內容都在離屏繪制。這一定是因為我們給圖片和標簽視圖添加的陰影效果。在代碼中禁用陰影,然后看下性能是否有提高(圖12.12)。
圖12.12 禁用陰影之后運行程序接近60FPS
問題解決了。干掉陰影之后,滑動很流暢。但是我們的聯系人列表看起來沒有之前好了。那如何保持陰影效果而且不會影響性能呢?
好吧,每一行的字符和頭像在每一幀刷新的時候並不需要變,所以看起來UITableViewCell
的圖層非常適合做緩存。我們可以使用shouldRasterize
來緩存圖層內容。這將會讓圖層離屏之后渲染一次然后把結果保存起來,直到下次利用的時候去更新(見清單12.2)。
清單12.2 使用shouldRasterize
提高性能
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//dequeue cell
UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell"
forIndexPath:indexPath];
...
//set text shadow
cell.textLabel.backgroundColor = [UIColor clearColor];
cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2);
cell.textLabel.layer.shadowOpacity = 0.5;
//rasterize
cell.layer.shouldRasterize = YES;
cell.layer.rasterizationScale = [UIScreen mainScreen].scale;
return cell;
}
我們仍然離屏繪制圖層內容,但是由於顯式地禁用了柵格化,Core Animation就對繪圖緩存了結果,於是對提高了性能。我們可以驗證緩存是否有效,在Core Animation工具中點擊Color Hits Green and Misses Red選項(圖12.13)。
圖12.13 Color Hits Green and Misses Red驗證了緩存有效
結果和預期一致 - 大部分都是綠色,只有當滑動到屏幕上的時候會閃爍成紅色。因此,現在幀率更加平滑了。
所以我們最初的設想是錯的。圖片的加載並不是真正的瓶頸所在,而且試圖把它置於一個復雜的多線程加載和緩存的實現都將是徒勞。所以在動手修復之前驗證問題所在是個很好的習慣!