學(xué)習(xí)案例
創(chuàng)建一個簡單的顯示模擬聯(lián)系人姓名和頭像列表的應(yīng)用。注意即使把頭像圖片存在應(yīng)用本地,為了使應(yīng)用看起來更真實,分別實時加載圖片,而不是用–imageNamed:預(yù)加載。同樣添加一些圖層陰影來使得列表顯示得更真實。
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
@interface ViewController () <UITableViewDataSource>
@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
當(dāng)快速滑動的時候就會非常卡

滑動幀率降到15FPS
僅憑直覺,我們猜測性能瓶頸應(yīng)該在圖片加載。我們實時從閃存加載圖片,而且沒有緩存,所以很可能是這個原因。我們可以用一些很贊的代碼修復(fù),然后使用GCD異步加載圖片,然后緩存。。。等一下,在開始編碼之前,測試一下假設(shè)是否成立。首先用我們的三個Instruments工具分析一下程序來定位問題。我們推測問題可能和圖片加載相關(guān),所以用Time Profiler工具來試試。

用The timing profile分析聯(lián)系人列表
-tableView:cellForRowAtIndexPath:中的CPU時間總利用率只有~28%(也就是加載頭像圖片的地方),非常低。于是建議是CPU/IO并不是真正的限制因素。然后看看是不是GPU的問題:在OpenGL ES Driver工具中檢測GPU利用率。

OpenGL ES Driver工具顯示的GPU利用率
渲染服務(wù)利用率的值達(dá)到51%和63%??雌饋鞧PU需要做很多工作來渲染聯(lián)系人列表。
為什么GPU利用率這么高呢?我們來用Core Animation調(diào)試工具選項來檢查屏幕。首先打開Color Blended Layers。

使用Color Blended Layers選項調(diào)試程序
屏幕中所有紅色的部分都意味著字符標(biāo)簽視圖的高級別混合,這很正常,因為我們把背景設(shè)置成了透明色來顯示陰影效果。這就解釋了為什么渲染利用率這么高了。
那么離屏繪制呢?打開Core Animation工具的Color Offscreen - Rendered Yellow選項。

Color Offscreen–Rendered Yellow選項
所有的表格單元內(nèi)容都在離屏繪制。這一定是因為我們給圖片和標(biāo)簽視圖添加的陰影效果。在代碼中禁用陰影,然后看下性能是否有提高。

禁用陰影之后運(yùn)行程序接近60FPS
問題解決了。干掉陰影之后,滑動很流暢。但是我們的聯(lián)系人列表看起來沒有之前好了。那如何保持陰影效果而且不會影響性能呢?
好吧,每一行的字符和頭像在每一幀刷新的時候并不需要變,所以看起來UITableViewCell的圖層非常時候做緩存。我們可以使用shouldRasterize來緩存圖層內(nèi)容。這將會讓圖層離屏之后渲染一次然后把結(jié)果保存起來,直到下次利用的時候去更新。
使用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;
}
我們?nèi)匀浑x屏繪制圖層內(nèi)容,但是由于顯式地禁用了柵格化,Core Animation就對繪圖緩存了結(jié)果,于是對提高了性能。我們可以驗證緩存是否有效,在Core Animation工具中點擊Color Hits Green and Misses Red選項。

Color Hits Green and Misses Red驗證了緩存有效
結(jié)果和預(yù)期一致 - 大部分都是綠色,只有當(dāng)滑動到屏幕上的時候會閃爍成紅色。因此,現(xiàn)在幀率更加平滑了。
所以我們最初的設(shè)想是錯的。圖片的加載并不是真正的瓶頸所在,而且試圖把它置于一個復(fù)雜的多線程加載和緩存的實現(xiàn)都將是徒勞。所以在動手修復(fù)之前驗證問題所在是個很好的習(xí)慣!