基於定時器的動畫和性能調優

基於定時器的動畫 數據庫

我能夠指導你,可是你必須按照我說的作。 -- 駭客帝國 編程

在第10章「緩衝」中,咱們研究了CAMediaTimingFunction,它是一個經過控制動畫緩衝來模擬物理效果例如加速或者減速來加強現實感的東西,那麼若是想更加真實地模擬 數組

物理交互或者實時根據用戶輸入修改動畫改怎麼辦呢?在這一章中,咱們將繼續探索一種可以容許咱們精確地控制一幀一幀展現的基於定時器的動畫。 緩存

定時幀
性能優化

動畫看起來是用來顯示一段連續的運動過程,但實際上當在固定位置上展現像素的時候並不能作到這一點。通常來講這種顯示都沒法作到連續的移動,能作的僅僅是足夠快地展現一系列靜態圖片,只是看起來像是作了運動。 服務器

咱們以前提到過iOS按照每秒60次刷新屏幕,而後CAAnimation計算出須要展現的新的幀,而後在每次屏幕更新的時候同步繪製上去,CAAnimation最機智的地方在於每次刷新須要展現的時候去計算插值和緩衝。 網絡

在第10章中,咱們解決了如何自定義緩衝函數,而後根據須要展現的幀的數組來告訴CAKeyframeAnimation的實例如何去繪製。全部的Core Animation實際上都是按照必定的序列來顯示這些幀,那麼咱們能夠本身作到這些麼? 多線程

NSTimer app

實際上,咱們在第三章「圖層幾何學」中已經作過相似的東西,就是時鐘那個例子,咱們用了NSTimer來對鐘錶的指針作定時動畫,一秒鐘更新一次,可是若是咱們把頻率調整成一秒鐘更新60次的話,原理是徹底相同的。 框架

咱們來試着用NSTimer來修改第十章中彈性球的例子。因爲如今咱們在定時器啓動以後連續計算動畫幀,咱們須要在類中添加一些額外的屬性來存儲動畫的fromValue,toValue,duration和當前的timeOffset(見清單11.1)。

清單11.1 使用NSTimer實現彈性球動畫

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
83
84
85
86
87
88
89
90
91
92
93
94
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, assign) NSTimeInterval duration;
@property (nonatomic, assign) NSTimeInterval timeOffset;
@property (nonatomic, strong) id fromValue;
@property (nonatomic, strong) id toValue;
@end
@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //add ball image view
    UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
    self.ballView = [[UIImageView alloc] initWithImage:ballImage];
    [self.containerView addSubview:self.ballView];
    //animate
    [self animate];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    //replay animation on tap
    [self animate];
}
float interpolate(float from, float to, float time)
{
    return (to - from) * time + from;
}
- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
{
    if ([fromValue isKindOfClass:[NSValue class]]) {
        //get type
        const char *type = [(NSValue *)fromValue objCType];
        if (strcmp(type, @encode(CGPoint)) == 0) {
            CGPoint from = [fromValue CGPointValue];
            CGPoint to = [toValue CGPointValue];
            CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
            return [NSValue valueWithCGPoint:result];
        }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
}
float bounceEaseOut(float t)
{
    if (t < 4/11.0) {
        return (121 * t * t)/16.0;
    } else if (t < 8/11.0) {
        return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
    } else if (t < 9/10.0) {
        return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
    }
    return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}
- (void)animate
{
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //configure the animation
    self.duration = 1.0;
    self.timeOffset = 0.0;
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [self.timer invalidate];
    //start the timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0
                                                  target:self
                                                selector:@selector(step:)
                                                userInfo:nil
                                                 repeats:YES];
}
- (void)step:(NSTimer *)step
{
    //update time offset
    self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration);
    //get normalized time offset (in range 0 - 1)
    float time = self.timeOffset / self.duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:self.fromValue
                                     toValue:self.toValue
                                  time:time];
    //move ball view to new position
    self.ballView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (self.timeOffset >= self.duration) {
        [self.timer invalidate];
        self.timer = nil;
    }
}
@end

很贊,並且和基於關鍵幀例子的代碼同樣不少,可是若是想一次性在屏幕上對不少東西作動畫,很明顯就會有不少問題。

NSTimer並非最佳方案,爲了理解這點,咱們須要確切地知道NSTimer是如何工做的。iOS上的每一個線程都管理了一個NSRunloop,字面上看就是經過一個循環來完成一些任務列表。可是對主線程,這些任務包含以下幾項:

  • 處理觸摸事件

  • 發送和接受網絡數據包

  • 執行使用gcd的代碼

  • 處理計時器行爲

  • 屏幕重繪

當你設置一個NSTimer,他會被插入到當前任務列表中,而後直到指定時間過去以後纔會被執行。可是什麼時候啓動定時器並無一個時間上限,並且它只會在列表中上一個任務完成以後開始執行。這一般會致使有幾毫秒的延遲,可是若是上一個任務過了好久才完成就會致使延遲很長一段時間。

屏幕重繪的頻率是一秒鐘六十次,可是和定時器行爲同樣,若是列表中上一個執行了很長時間,它也會延遲。這些延遲都是一個隨機值,因而就不能保證定時器精準地一秒鐘執行六十次。有時候發生在屏幕重繪以後,這就會使得更新屏幕會有個延遲,看起來就是動畫卡殼了。有時候定時器會在屏幕更新的時候執行兩次,因而動畫看起來就跳動了。

咱們能夠經過一些途徑來優化:

  • 咱們能夠用CADisplayLink讓更新頻率嚴格控制在每次屏幕刷新以後。

  • 基於真實幀的持續時間而不是假設的更新頻率來作動畫。

  • 調整動畫計時器的run loop模式,這樣就不會被別的事件干擾。

CADisplayLink

CADisplayLink是CoreAnimation提供的另外一個相似於NSTimer的類,它老是在屏幕完成一次更新以前啓動,它的接口設計的和NSTimer很相似,因此它實際上就是一個內置實現的替代,可是和timeInterval以秒爲單位不一樣,CADisplayLink有一個整型的frameInterval屬性,指定了間隔多少幀以後才執行。默認值是1,意味着每次屏幕更新以前都會執行一次。可是若是動畫的代碼執行起來超過了六十分之一秒,你能夠指定frameInterval爲2,就是說動畫每隔一幀執行一次(一秒鐘30幀)或者3,也就是一秒鐘20次,等等。

用CADisplayLink而不是NSTimer,會保證幀率足夠連續,使得動畫看起來更加平滑,但即便CADisplayLink也不能保證每一幀都按計劃執行,一些失去控制的離散的任務或者事件(例如資源緊張的後臺程序)可能會致使動畫偶爾地丟幀。當使用NSTimer的時候,一旦有機會計時器就會開啓,可是CADisplayLink卻不同:若是它丟失了幀,就會直接忽略它們,而後在下一次更新的時候接着運行。

計算幀的持續時間

不管是使用NSTimer仍是CADisplayLink,咱們仍然須要處理一幀的時間超出了預期的六十分之一秒。因爲咱們不可以計算出一幀真實的持續時間,因此須要手動測量。咱們能夠在每幀開始刷新的時候用CACurrentMediaTime()記錄當前時間,而後和上一幀記錄的時間去比較。

經過比較這些時間,咱們就能夠獲得真實的每幀持續的時間,而後代替硬編碼的六十分之一秒。咱們來更新一下上個例子(見清單11.2)。

清單11.2 經過測量沒幀持續的時間來使得動畫更加平滑

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
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@property (nonatomic, strong) CADisplayLink *timer;
@property (nonatomic, assign) CFTimeInterval duration;
@property (nonatomic, assign) CFTimeInterval timeOffset;
@property (nonatomic, assign) CFTimeInterval lastStep;
@property (nonatomic, strong) id fromValue;
@property (nonatomic, strong) id toValue;
@end
@implementation ViewController
...
- (void)animate
{
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //configure the animation
    self.duration = 1.0;
    self.timeOffset = 0.0;
    self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    //stop the timer if it's already running
    [self.timer invalidate];
    //start the timer
    self.lastStep = CACurrentMediaTime();
    self.timer = [CADisplayLink displayLinkWithTarget:self
                                             selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
                     forMode:NSDefaultRunLoopMode];
}
- (void)step:(CADisplayLink *)timer
{
    //calculate time delta
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - self.lastStep;
    self.lastStep = thisStep;
    //update time offset
    self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration);
    //get normalized time offset (in range 0 - 1)
    float time = self.timeOffset / self.duration;
    //apply easing
    time = bounceEaseOut(time);
    //interpolate position
    id position = [self interpolateFromValue:self.fromValue toValue:self.toValue
                                        time:time];
    //move ball view to new position
    self.ballView.center = [position CGPointValue];
    //stop the timer if we've reached the end of the animation
    if (self.timeOffset >= self.duration) {
        [self.timer invalidate];
        self.timer = nil;
    }
}
@end

Run Loop 模式

注意到當建立CADisplayLink的時候,咱們須要指定一個run loop和run loop mode,對於run loop來講,咱們就使用了主線程的run loop,由於任何用戶界面的更新都須要在主線程執行,可是模式的選擇就並不那麼清楚了,每一個添加到run loop的任務都有一個指定了優先級的模式,爲了保證用戶界面保持平滑,iOS會提供和用戶界面相關任務的優先級,並且當UI很活躍的時候的確會暫停一些別的任務。

一個典型的例子就是當是用UIScrollview滑動的時候,重繪滾動視圖的內容會比別的任務優先級更高,因此標準的NSTimer和網絡請求就不會啓動,一些常見的run loop模式以下:

  • NSDefaultRunLoopMode - 標準優先級

  • NSRunLoopCommonModes - 高優先級

  • UITrackingRunLoopMode - 用於UIScrollView和別的控件的動畫

在咱們的例子中,咱們是用了NSDefaultRunLoopMode,可是不能保證動畫平滑的運行,因此就能夠用NSRunLoopCommonModes來替代。可是要當心,由於若是動畫在一個高幀率狀況下運行,你會發現一些別的相似於定時器的任務或者相似於滑動的其餘iOS動畫會暫停,直到動畫結束。

一樣能夠同時對CADisplayLink指定多個run loop模式,因而咱們能夠同時加入NSDefaultRunLoopMode和UITrackingRunLoopMode來保證它不會被滑動打斷,也不會被其餘UIKit控件動畫影響性能,像這樣:

1
2
3
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];

和CADisplayLink相似,NSTimer一樣也可使用不一樣的run loop模式配置,經過別的函數,而不是+scheduledTimerWithTimeInterval:構造器

1
2
3
4
5
6
7
self.timer = [NSTimer timerWithTimeInterval:1/60.0
                                 target:self
                               selector:@selector(step:)
                               userInfo:nil
                                repeats:YES];
[[NSRunLoop mainRunLoop] addTimer:self.timer
                          forMode:NSRunLoopCommonModes];

物理模擬

即便使用了基於定時器的動畫來複制第10章中關鍵幀的行爲,但仍是會有一些本質上的區別:在關鍵幀的實現中,咱們提早計算了全部幀,可是在新的解決方案中,咱們實際上實在按須要在計算。意義在於咱們能夠根據用戶輸入實時修改動畫的邏輯,或者和別的實時動畫系統例如物理引擎進行整合。

Chipmunk

咱們來基於物理學建立一個真實的重力模擬效果來取代當前基於緩衝的彈性動畫,但即便模擬2D的物理效果就已近極其複雜了,因此就不要嘗試去實現它了,直接用開源的物理引擎庫好了。

咱們將要使用的物理引擎叫作Chipmunk。另外的2D物理引擎也一樣能夠(例如Box2D),可是Chipmunk使用純C寫的,而不是C++,好處在於更容易和Objective-C項目整合。Chipmunk有不少版本,包括一個和Objective-C綁定的「indie」版本。C語言的版本是免費的,因此咱們就用它好了。在本書寫做的時候6.1.4是最新的版本;你能夠從http://chipmunk-physics.net下載它。

Chipmunk完整的物理引擎至關巨大複雜,可是咱們只會使用以下幾個類:

  • cpSpace - 這是全部的物理結構體的容器。它有一個大小和一個可選的重力矢量

  • cpBody - 它是一個固態無彈力的剛體。它有一個座標,以及其餘物理屬性,例如質量,運動和摩擦係數等等。

  • cpShape - 它是一個抽象的幾何形狀,用來檢測碰撞。能夠給結構體添加一個多邊形,並且cpShape有各類子類來表明不一樣形狀的類型。

在例子中,咱們來對一個木箱建模,而後在重力的影響下下落。咱們來建立一個Crate類,包含屏幕上的可視效果(一個UIImageView)和一個物理模型(一個cpBody和一個cpPolyShape,一個cpShape的多邊形子類來表明矩形木箱)。

用C版本的Chipmunk會帶來一些挑戰,由於它如今並不支持Objective-C的引用計數模型,因此咱們須要準確的建立和釋放對象。爲了簡化,咱們把cpShape和cpBody的生命週期和Crate類進行綁定,而後在木箱的-init方法中建立,在-dealloc中釋放。木箱物理屬性的配置很複雜,因此閱讀了Chipmunk文檔會頗有意義。

視圖控制器用來管理cpSpace,還有和以前同樣的計時器邏輯。在每一步中,咱們更新cpSpace(用來進行物理計算和全部結構體的從新擺放)而後迭代對象,而後再更新咱們的木箱視圖的位置來匹配木箱的模型(在這裏,實際上只有一個結構體,可是以後咱們將要添加更多)。

Chipmunk使用了一個和UIKit顛倒的座標系(Y軸向上爲正方向)。爲了使得物理模型和視圖之間的同步更簡單,咱們須要經過使用geometryFlipped屬性翻轉容器視圖的集合座標(第3章中有提到),因而模型和視圖都共享一個相同的座標系。

具體的代碼見清單11.3。注意到咱們並無在任何地方釋放cpSpace對象。在這個例子中,內存空間將會在整個app的生命週期中一直存在,因此這沒有問題。可是在現實世界的場景中,咱們須要像建立木箱結構體和形狀同樣去管理咱們的空間,封裝在標準的Cocoa對象中,而後來管理Chipmunk對象的生命週期。圖11.1展現了掉落的木箱。

清單11.3 使用物理學來對掉落的木箱建模

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
83
84
85
86
87
88
89
90
#import "ViewController.h" 
#import #import "chipmunk.h"
@interface Crate : UIImageView
@property (nonatomic, assign) cpBody *body;
@property (nonatomic, assign) cpShape *shape;
@end
@implementation Crate
#define MASS 100
- (id)initWithFrame:(CGRect)frame
{
    if ((self = [super initWithFrame:frame])) {
        //set image
        self.image = [UIImage imageNamed:@"Crate.png"];
        self.contentMode = UIViewContentModeScaleAspectFill;
        //create the body
        self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height));
        //create the shape
        cpVect corners[] = {
            cpv(0, 0),
            cpv(0, frame.size.height),
            cpv(frame.size.width, frame.size.height),
            cpv(frame.size.width, 0),
        };
        self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2));
        //set shape friction & elasticity
        cpShapeSetFriction(self.shape, 0.5);
        cpShapeSetElasticity(self.shape, 0.8);
        //link the crate to the shape
        //so we can refer to crate from callback later on
        self.shape->data = (__bridge void *)self;
        //set the body position to match view
        cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2));
    }
    return self;
}
- (void)dealloc
{
    //release shape and body
    cpShapeFree(_shape);
    cpBodyFree(_body);
}
@end
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, assign) cpSpace *space;
@property (nonatomic, strong) CADisplayLink *timer;
@property (nonatomic, assign) CFTimeInterval lastStep;
@end
@implementation ViewController
#define GRAVITY 1000
- (void)viewDidLoad
{
    //invert view coordinate system to match physics
    self.containerView.layer.geometryFlipped = YES;
    //set up physics space
    self.space = cpSpaceNew();
    cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
    //add a crate
    Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)];
    [self.containerView addSubview:crate];
    cpSpaceAddBody(self.space, crate.body);
    cpSpaceAddShape(self.space, crate.shape);
    //start the timer
    self.lastStep = CACurrentMediaTime();
    self.timer = [CADisplayLink displayLinkWithTarget:self
                                             selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
                     forMode:NSDefaultRunLoopMode];
}
void updateShape(cpShape *shape, void *unused)
{
    //get the crate object associated with the shape
    Crate *crate = (__bridge Crate *)shape->data;
    //update crate view position and angle to match physics shape
    cpBody *body = shape->body;
    crate.center = cpBodyGetPos(body);
    crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body));
}
- (void)step:(CADisplayLink *)timer
{
    //calculate step duration
    CFTimeInterval thisStep = CACurrentMediaTime();
    CFTimeInterval stepDuration = thisStep - self.lastStep;
    self.lastStep = thisStep;
    //update physics
    cpSpaceStep(self.space, stepDuration);
    //update all the shapes
    cpSpaceEachShape(self.space, &updateShape, NULL);
}
@end

11.1.jpg

圖11.1 一個木箱圖片,根據模擬的重力掉落

添加用戶交互

下一步就是在視圖周圍添加一道不可見的牆,這樣木箱就不會掉落出屏幕以外。或許你會用另外一個矩形的cpPolyShape來實現,就和以前建立木箱那樣,可是咱們須要檢測的是木箱什麼時候離開視圖,而不是什麼時候碰撞,因此咱們須要一個空心而不是固體矩形。

咱們能夠經過給cpSpace添加四個cpSegmentShape對象(cpSegmentShape表明一條直線,因此四個拼起來就是一個矩形)。而後賦給空間的staticBody屬性(一個不被重力影響的結構體)而不是像木箱那樣一個新的cpBody實例,由於咱們不想讓這個邊框矩形滑出屏幕或者被一個下落的木箱擊中而消失。

一樣能夠再添加一些木箱來作一些交互。最後再添加一個加速器,這樣能夠經過傾斜手機來調整重力矢量(爲了測試須要在一臺真實的設備上運行程序,由於模擬器不支持加速器事件,即便旋轉屏幕)。清單11.4展現了更新後的代碼,運行結果見圖11.2。

因爲示例只支持橫屏模式,因此交換加速計矢量的x和y值。若是在豎屏下運行程序,請把他們換回來,否則重力方向就錯亂了。試一下就知道了,木箱會沿着橫向移動。

清單11.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
- (void)addCrateWithFrame:(CGRect)frame
{
    Crate *crate = [[Crate alloc] initWithFrame:frame];
    [self.containerView addSubview:crate];
    cpSpaceAddBody(self.space, crate.body);
    cpSpaceAddShape(self.space, crate.shape);
}
- (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end
{
    cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1);
    cpShapeSetCollisionType(wall, 2);
    cpShapeSetFriction(wall, 0.5);
    cpShapeSetElasticity(wall, 0.8);
    cpSpaceAddStaticShape(self.space, wall);
}
- (void)viewDidLoad
{
    //invert view coordinate system to match physics
    self.containerView.layer.geometryFlipped = YES;
    //set up physics space
    self.space = cpSpaceNew();
    cpSpaceSetGravity(self.space, cpv(0, -GRAVITY));
    //add wall around edge of view
    [self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)];
    [self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)];
    [self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)];
    [self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)];
    //add a crates
    [self addCrateWithFrame:CGRectMake(0, 0, 32, 32)];
    [self addCrateWithFrame:CGRectMake(32, 0, 32, 32)];
    [self addCrateWithFrame:CGRectMake(64, 0, 64, 64)];
    [self addCrateWithFrame:CGRectMake(128, 0, 32, 32)];
    [self addCrateWithFrame:CGRectMake(0, 32, 64, 64)];
    //start the timer
    self.lastStep = CACurrentMediaTime();
    self.timer = [CADisplayLink displayLinkWithTarget:self
                                             selector:@selector(step:)];
    [self.timer addToRunLoop:[NSRunLoop mainRunLoop]
                     forMode:NSDefaultRunLoopMode];
    //update gravity using accelerometer
    [UIAccelerometer sharedAccelerometer].delegate = self;
    [UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0;
}
- (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration
{
    //update gravity
    cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, -acceleration.x * GRAVITY));
}

11.2.jpg

圖11.1 真實引力場下的木箱交互

模擬時間以及固定的時間步長

對於實現動畫的緩衝效果來講,計算每幀持續的時間是一個很好的解決方案,可是對模擬物理效果並不理想。經過一個可變的時間步長來實現有着兩個弊端:

  • 若是時間步長不是固定的,精確的值,物理效果的模擬也就隨之不肯定。這意味着即便是傳入相同的輸入值,也可能在不一樣場合下有着不一樣的效果。有時候沒多大影響,可是在基於物理引擎的遊戲下,玩家就會因爲相同的操做行爲致使不一樣的結果而感到困惑。一樣也會讓測試變得麻煩。

  • 因爲性能故常形成的丟幀或者像電話呼入的中斷均可能會形成不正確的結果。考慮一個像子彈那樣快速移動物體,每一幀的更新都須要移動子彈,檢測碰撞。若是兩幀之間的時間加長了,子彈就會在這一步移動更遠的距離,穿過圍牆或者是別的障礙,這樣就丟失了碰撞。

咱們想獲得的理想的效果就是經過固定的時間步長來計算物理效果,可是在屏幕發生重繪的時候仍然可以同步更新視圖(可能會因爲在咱們控制範圍以外形成不可預知的效果)。

幸運的是,因爲咱們的模型(在這個例子中就是Chipmunk的cpSpace中的cpBody)被視圖(就是屏幕上表明木箱的UIView對象)分離,因而就很簡單了。咱們只須要根據屏幕刷新的時間跟蹤時間步長,而後根據每幀去計算一個或者多個模擬出來的效果。

咱們能夠經過一個簡單的循環來實現。經過每次CADisplayLink的啓動來通知屏幕將要刷新,而後記錄下當前的CACurrentMediaTime()。咱們須要在一個小增量中提早重複物理模擬(這裏用120分之一秒)直到遇上顯示的時間。而後更新咱們的視圖,在屏幕刷新的時候匹配當前物理結構體的顯示位置。

清單11.5展現了固定時間步長版本的代碼

清單11.5 固定時間步長的木箱模擬

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#define SIMULATION_STEP (1/120.0)
- (void)step:(CADisplayLink *)timer
{
    //calculate frame step duration
    CFTimeInterval frameTime = CACurrentMediaTime();
    //update simulation
    while (self.lastStep < frameTime) {
        cpSpaceStep(self.space, SIMULATION_STEP);
        self.lastStep += SIMULATION_STEP;
    }
    ?
    //update all the shapes
    cpSpaceEachShape(self.space, &updateShape, NULL);
}

避免死亡螺旋

當使用固定的模擬時間步長時候,有一件事情必定要注意,就是用來計算物理效果的現實世界的時間並不會加速模擬時間步長。在咱們的例子中,咱們隨意選擇了120分之一秒來模擬物理效果。Chipmunk很快,咱們的例子也很簡單,因此cpSpaceStep()會完成的很好,不會延遲幀的更新。

可是若是場景很複雜,好比有上百個物體之間的交互,物理計算就會很複雜,cpSpaceStep()的計算也可能會超出1/120秒。咱們沒有測量出物理步長的時間,由於咱們假設了相對於幀刷新來講並不重要,可是若是模擬步長更久的話,就會延遲幀率。

若是幀刷新的時間延遲的話會變得很糟糕,咱們的模擬須要執行更多的次數來同步真實的時間。這些額外的步驟就會繼續延遲幀的更新,等等。這就是所謂的死亡螺旋,由於最後的結果就是幀率變得愈來愈慢,直到最後應用程序卡死了。

咱們能夠經過添加一些代碼在設備上來對物理步驟計算真實世界的時間,而後自動調整固定時間步長,可是實際上它不可行。其實只要保證你給容錯留下足夠的邊長,而後在指望支持的最慢的設備上進行測試就能夠了。若是物理計算超過了模擬時間的50%,就須要考慮增長模擬時間步長(或者簡化場景)。若是模擬時間步長增長到超過1/60秒(一個完整的屏幕更新時間),你就須要減小動畫幀率到一秒30幀或者增長CADisplayLink的frameInterval來保證不會隨機丟幀,否則你的動畫將會看起來不平滑。

總結

在這一章中,咱們瞭解瞭如何經過一個計時器建立一幀幀的實時動畫,包括緩衝,物理模擬等等一系列動畫技術,以及用戶輸入(經過加速計)。

在第三部分中,咱們將研究動畫性能是如何被被設備限制所影響的,以及如何調整咱們的代碼來活的足夠好的幀率。
--------------------------------------------------------------------------------------------------------------------------------------------------------

性能調優

代碼應該運行的儘可能快,而不是更快 - 理查德

在第一和第二部分,咱們瞭解了Core Animation提供的關於繪製和動畫的一些特性。Core Animation功能和性能都很是強大,但若是你對背後的原理不清楚的話也會下降效率。讓它達到最優的狀態是一門藝術。在這章中,咱們將探究一些動畫運行慢的緣由,以及如何去修復這些問題。

CPU VS GPU

關於繪圖和動畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)。在現代iOS設備中,都有能夠運行不一樣軟件的可編程芯片,可是因爲歷史緣由,咱們能夠說CPU所作的工做都在軟件層面,而GPU在硬件層面。

總的來講,咱們能夠用軟件(使用CPU)作任何事情,可是對於圖像處理,一般用硬件會更快,由於GPU使用圖像對高度並行浮點運算作了優化。因爲某些緣由,咱們想盡量把屏幕渲染的工做交給硬件去處理。問題在於GPU並無無限制處理性能,並且一旦資源用完的話,性能就會開始降低了(即便CPU並無徹底佔用)

大多數動畫性能優化都是關於智能利用GPU和CPU,使得它們都不會超出負荷。因而咱們首先須要知道Core Animation是如何在這兩個處理器之間分配工做的。

動畫的舞臺

Core Animation處在iOS的核心地位:應用內和應用間都會用到它。一個簡單的動畫可能同步顯示多個app的內容,例如當在iPad上多個程序之間使用手勢切換,會使得多個程序同時顯示在屏幕上。在一個特定的應用中用代碼實現它是沒有意義的,由於在iOS中不可能實現這種效果(App都是被沙箱管理,不能訪問別的視圖)。

動畫和屏幕上組合的圖層實際上被一個單獨的進程管理,而不是你的應用程序。這個進程就是所謂的渲染服務。在iOS5和以前的版本是SpringBoard進程(同時管理着iOS的主屏)。在iOS6以後的版本中叫作BackBoard。

當運行一段動畫時候,這個過程會被四個分離的階段被打破:

  • 佈局 - 這是準備你的視圖/圖層的層級關係,以及設置圖層屬性(位置,背景色,邊框等等)的階段。

  • 顯示 - 這是圖層的寄宿圖片被繪製的階段。繪製有可能涉及你的-drawRect:和-drawLayer:inContext:方法的調用路徑。

  • 準備 - 這是Core Animation準備發送動畫數據到渲染服務的階段。這同時也是Core Animation將要執行一些別的事務例如解碼動畫過程當中將要顯示的圖片的時間點。

  • 提交 - 這是最後的階段,Core Animation打包全部圖層和動畫屬性,而後經過IPC(內部處理通訊)發送到渲染服務進行顯示。

可是這些僅僅階段僅僅發生在你的應用程序以內,在動畫在屏幕上顯示以前仍然有更多的工做。一旦打包的圖層和動畫到達渲染服務進程,他們會被反序列化來造成另外一個叫作渲染樹的圖層樹(在第一章「圖層樹」中提到過)。使用這個樹狀結構,渲染服務對動畫的每一幀作出以下工做:

  • 對全部的圖層屬性計算中間值,設置OpenGL幾何形狀(紋理化的三角形)來執行渲染

  • 在屏幕上渲染可見的三角形

因此一共有六個階段;最後兩個階段在動畫過程當中不停地重複。前五個階段都在軟件層面處理(經過CPU),只有最後一個被GPU執行。並且,你真正只能控制前兩個階段:佈局和顯示。Core Animation框架在內部處理剩下的事務,你也控制不了它。

這並非個問題,由於在佈局和顯示階段,你能夠決定哪些由CPU執行,哪些交給GPU去作。那麼改如何判斷呢?

GPU相關的操做

GPU爲一個具體的任務作了優化:它用來採集圖片和形狀(三角形),運行變換,應用紋理和混合而後把它們輸送到屏幕上。現代iOS設備上可編程的GPU在這些操做的執行上又很大的靈活性,可是Core Animation並無暴露出直接的接口。除非你想繞開Core Animation並編寫你本身的OpenGL着色器,從根本上解決硬件加速的問題,那麼剩下的全部都仍是須要在CPU的軟件層面上完成。

寬泛的說,大多數CALayer的屬性都是用GPU來繪製。好比若是你設置圖層背景或者邊框的顏色,那麼這些能夠經過着色的三角板實時繪製出來。若是對一個contents屬性設置一張圖片,而後裁剪它 - 它就會被紋理的三角形繪製出來,而不須要軟件層面作任何繪製。

可是有一些事情會下降(基於GPU)圖層繪製,好比:

  • 太多的幾何結構 - 這發生在須要太多的三角板來作變換,以應對處理器的柵格化的時候。現代iOS設備的圖形芯片能夠處理幾百萬個三角板,因此在Core Animation中幾何結構並非GPU的瓶頸所在。但因爲圖層在顯示以前經過IPC發送到渲染服務器的時候(圖層其實是由不少小物體組成的特別重量級的對象),太多的圖層就會引發CPU的瓶頸。這就限制了一次展現的圖層個數(見本章後續「CPU相關操做」)。

  • 重繪 - 主要由重疊的半透明圖層引發。GPU的填充比率(用顏色填充像素的比率)是有限的,因此須要避免重繪(每一幀用相同的像素填充屢次)的發生。在現代iOS設備上,GPU都會應對重繪;即便是iPhone 3GS均可以處理高達2.5的重繪比率,並任然保持60幀率的渲染(這意味着你能夠繪製一個半的整屏的冗餘信息,而不影響性能),而且新設備能夠處理更多。

  • 離屏繪製 - 這發生在當不能直接在屏幕上繪製,而且必須繪製到離屏圖片的上下文中的時候。離屏繪製發生在基於CPU或者是GPU的渲染,或者是爲離屏圖片分配額外內存,以及切換繪製上下文,這些都會下降GPU性能。對於特定圖層效果的使用,好比圓角,圖層遮罩,陰影或者是圖層光柵化都會強制Core Animation提早渲染圖層的離屏繪製。但這不意味着你須要避免使用這些效果,只是要明白這會帶來性能的負面影響。

  • 過大的圖片 - 若是視圖繪製超出GPU支持的2048x2048或者4096x4096尺寸的紋理,就必需要用CPU在圖層每次顯示以前對圖片預處理,一樣也會下降性能。

CPU相關的操做

大多數工做在Core Animation的CPU都發生在動畫開始以前。這意味着它不會影響到幀率,因此很好,可是他會延遲動畫開始的時間,讓你的界面看起來會比較遲鈍。

如下CPU的操做都會延遲動畫的開始時間:

  • 佈局計算 - 若是你的視圖層級過於複雜,當視圖呈現或者修改的時候,計算圖層幀率就會消耗一部分時間。特別是使用iOS6的自動佈局機制尤其明顯,它應該是比老版的自動調整邏輯增強了CPU的工做。

  • 視圖懶加載 - iOS只會當視圖控制器的視圖顯示到屏幕上時纔會加載它。這對內存使用和程序啓動時間頗有好處,可是當呈現到屏幕上以前,按下按鈕致使的許多工做都會不能被及時響應。好比控制器從數據庫中獲取數據,或者視圖從一個nib文件中加載,或者涉及IO的圖片顯示(見後續「IO相關操做」),都會比CPU正常操做慢得多。

  • Core Graphics繪製 - 若是對視圖實現了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那麼在繪製任何東西以前都會產生一個巨大的性能開銷。爲了支持對圖層內容的任意繪製,Core Animation必須建立一個內存中等大小的寄宿圖片。而後一旦繪製結束以後,必須把圖片數據經過IPC傳到渲染服務器。在此基礎上,Core Graphics繪製就會變得十分緩慢,因此在一個對性能十分挑剔的場景下這樣作十分很差。

  • 解壓圖片 - PNG或者JPEG壓縮以後的圖片文件會比同質量的位圖小得多。可是在圖片繪製到屏幕上以前,必須把它擴展成完整的未解壓的尺寸(一般等同於圖片寬 x 長 x 4個字節)。爲了節省內存,iOS一般直到真正繪製的時候纔去解碼圖片(14章「圖片IO」會更詳細討論)。根據你加載圖片的方式,第一次對圖層內容賦值的時候(直接或者間接使用UIImageView)或者把它繪製到Core Graphics中,都須要對它解壓,這樣的話,對於一個較大的圖片,都會佔用必定的時間。

當圖層被成功打包,發送到渲染服務器以後,CPU仍然要作以下工做:爲了顯示屏幕上的圖層,Core Animation必須對渲染樹種的每一個可見圖層經過OpenGL循環轉換成紋理三角板。因爲GPU並不知曉Core Animation圖層的任何結構,因此必需要由CPU作這些事情。這裏CPU涉及的工做和圖層個數成正比,因此若是在你的層級關係中有太多的圖層,就會致使CPU沒一幀的渲染,即便這些事情不是你的應用程序可控的。

IO相關操做

還有一項沒涉及的就是IO相關工做。上下文中的IO(輸入/輸出)指的是例如閃存或者網絡接口的硬件訪問。一些動畫可能須要從山村(甚至是遠程URL)來加載。一個典型的例子就是兩個視圖控制器之間的過渡效果,這就須要從一個nib文件或者是它的內容中懶加載,或者一個旋轉的圖片,可能在內存中尺寸太大,須要動態滾動來加載。

IO比內存訪問更慢,因此若是動畫涉及到IO,就是一個大問題。總的來講,這就須要使用聰敏但尷尬的技術,也就是多線程,緩存和投機加載(提早加載當前不須要的資源,可是以後可能須要用到)。這些技術將會在第14章中討論。

測量,而不是猜想

因而如今你知道有哪些點可能會影響動畫性能,那該如何修復呢?好吧,其實不須要。有不少種詭計來優化動畫,但若是盲目使用的話,可能會形成更多性能上的問題,而不是修復。

如何正確的測量而不是猜想這點很重要。根據性能相關的知識寫出代碼不一樣於倉促的優化。前者很好,後者實際上就是在浪費時間。

那該如何測量呢?第一步就是確保在真實環境下測試你的程序。

真機測試,而不是模擬器

當你開始作一些性能方面的工做時,必定要在真機上測試,而不是模擬器。模擬器雖然是加快開發效率的一把利器,但它不能提供準確的真機性能參數。

模擬器運行在你的Mac上,然而Mac上的CPU每每比iOS設備要快。相反,Mac上的GPU和iOS設備的徹底不同,模擬器不得已要在軟件層面(CPU)模擬設備的GPU,這意味着GPU相關的操做在模擬器上運行的更慢,尤爲是使用CAEAGLLayer來些一些OpenGL的代碼時候。

這就是說在模擬器上的測試出的性能會高度失真。若是動畫在模擬器上運行流暢,可能在真機上十分糟糕。若是在模擬器上運行的很卡,也可能在真機上很平滑。你沒法肯定。

另外一件重要的事情就是性能測試必定要用發佈配置,而不是調試模式。由於當用發佈環境打包的時候,編譯器會引入一系列提升性能的優化,例如去掉調試符號或者移除並從新組織代碼。你也能夠本身作到這些,例如在發佈環境禁用NSLog語句。你只關心發佈性能,那纔是你須要測試的點。

最後,最好在你支持的設備中性能最差的設備上測試:若是基於iOS6開發,這意味着最好在iPhone 3GS或者iPad2上測試。若是可能的話,測試不一樣的設備和iOS版本,由於蘋果在不一樣的iOS版本和設備中作了一些改變,這也可能影響到一些性能。例如iPad3明顯要在動畫渲染上比iPad2慢不少,由於渲染4倍多的像素點(爲了支持視網膜顯示)。

保持一致的幀率

爲了作到動畫的平滑,你須要以60FPS(幀每秒)的速度運行,以同步屏幕刷新速率。經過基於NSTimer或者CADisplayLink的動畫你能夠下降到30FPS,並且效果還不錯,可是沒辦法經過Core Animation作到這點。若是不保持60FPS的速率,就可能隨機丟幀,影響到體驗。

你能夠在使用的過程當中明顯感到有沒有丟幀,但沒辦法經過肉眼來獲得具體的數據,也無法知道你的作法有沒有真的提升性能。你須要的是一系列精確的數據。

你能夠在程序中用CADisplayLink來測量幀率(就像11章「基於定時器的動畫」中那樣),而後在屏幕上顯示出來,但應用內的FPS顯示並不可以徹底真實測量出Core Animation性能,由於它僅僅測出應用內的幀率。咱們知道不少動畫都在應用以外發生(在渲染服務器進程中處理),但同時應用內FPS計數的確能夠對某些性能問題提供參考,一旦找出一個問題的地方,你就須要獲得更多精確詳細的數據來定位到問題所在。蘋果提供了一個強大的Instruments工具集來幫咱們作到這些。

Instruments

Instruments是Xcode套件中沒有被充分利用的一個工具。不少iOS開發者從沒用過Instruments,或者只是用Leaks工具檢測循環引用。實際上有不少Instruments工具,包括爲動畫性能調優的東西。

你能夠經過在菜單中選擇Profile選項來打開Instruments(在這以前,記住要把目標設置成iOS設備,而不是模擬器)。而後將會顯示出圖12.1(若是沒有看到全部選項,你可能設置成了模擬器選項)。

12.1.jpg

圖12.1 Instruments工具選項窗口

就像以前提到的那樣,你應該始終將程序設置成發佈選項。幸運的是,配置文件默認就是發佈選項,因此你不須要在分析的時候調整編譯策略。

咱們將討論以下幾個工具:

時間分析器 - 用來測量被方法/函數打斷的CPU使用狀況。

  • Core Animation - 用來調試各類Core Animation性能問題。

  • OpenGL ES驅動 - 用來調試GPU性能問題。這個工具在編寫Open GL代碼的時候頗有用,但有時也用來處理Core Animation的工做。

  • Instruments的一個很棒的功能在於它能夠建立咱們自定義的工具集。除了你初始選擇的工具以外,若是在Instruments中打開Library窗口,你能夠拖拽別的工具到左側邊欄。咱們將建立以上咱們提到的三個工具,而後就能夠並行使用了(見圖12.2)。

12.2.jpg

圖12.2 添加額外的工具到Instruments側邊欄

時間分析器

時間分析器工具用來檢測CPU的使用狀況。它能夠告訴咱們程序中的哪一個方法正在消耗大量的CPU時間。使用大量的CPU並不必定是個問題 - 你可能指望動畫路徑對CPU很是依賴,由於動畫每每是iOS設備中最苛刻的任務。

可是若是你有性能問題,查看CPU時間對於判斷性能是否是和CPU相關,以及定位到函數都頗有幫助(見圖12.3)。

12.3.jpg

圖12.3 時間分析器工具

  • 時間分析器有一些選項來幫助咱們定位到咱們關心的的方法。可使用左側的複選框來打開。其中最有用的是以下幾點:

  • 經過線程分離 - 這能夠經過執行的線程進行分組。若是代碼被多線程分離的話,那麼就能夠判斷究竟是哪一個線程形成了問題。

  • 隱藏系統庫 - 能夠隱藏全部蘋果的框架代碼,來幫助咱們尋找哪一段代碼形成了性能瓶頸。因爲咱們不能優化框架方法,因此這對定位到咱們能實際修復的代碼頗有用。

只顯示Obj-C代碼 - 隱藏除了Objective-C以外的全部代碼。大多數內部的Core Animation代碼都是用C或者C++函數,因此這對咱們集中精力到咱們代碼中顯式調用的方法就頗有用。

Core Animation

Core Animation工具用來監測Core Animation性能。它給咱們提供了週期性的FPS,而且考慮到了發生在程序以外的動畫(見圖12.4)。

12.4.jpg

圖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.jpg

圖12.5 iOS模擬器中Core Animation可視化調試選項

OpenGL ES驅動

OpenGL ES驅動工具能夠幫你測量GPU的利用率,一樣也是一個很好的來判斷和GPU相關動畫性能的指示器。它一樣也提供了相似Core Animation那樣顯示FPS的工具(圖12.6)。

12.6.jpg

圖12.6 OpenGL ES驅動工具

側欄的郵編是一系列有用的工具。其中和Core Animation性能最相關的是以下幾點:

  • Renderer Utilization - 若是這個值超過了~50%,就意味着你的動畫可能對幀率有所限制,極可能由於離屏渲染或者是重繪致使的過分混合。

  • Tiler Utilization - 若是這個值超過了~50%,就意味着你的動畫可能限制於幾何結構方面,也就是在屏幕上有太多的圖層佔用了。

一個可用的案例

如今咱們已經對Instruments中動畫性能工具很是熟悉了,那麼能夠用它在現實中解決一些實際問題。

咱們建立一個簡單的顯示模擬聯繫人姓名和頭像列表的應用。注意即便把頭像圖片存在應用本地,爲了使應用看起來更真實,咱們分別實時加載圖片,而不是用–imageNamed:預加載。一樣添加一些圖層陰影來使得列表顯示得更真實。清單12.1展現了最第一版本的實現。

清單12.1 使用假數據的一個簡單聯繫人列表

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
#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.jpg

圖12.7 滑動幀率降到15FPS

僅憑直覺,咱們猜想性能瓶頸應該在圖片加載。咱們實時從閃存加載圖片,並且沒有緩存,因此極可能是這個緣由。咱們能夠用一些很讚的代碼修復,而後使用GCD異步加載圖片,而後緩存。。。等一下,在開始編碼以前,測試一下假設是否成立。首先用咱們的三個Instruments工具分析一下程序來定位問題。咱們推測問題可能和圖片加載相關,因此用Time Profiler工具來試試(圖12.8)。

12.8.jpg

圖12.8 用The timing profile分析聯繫人列表

-tableView:cellForRowAtIndexPath:中的CPU時間總利用率只有~28%(也就是加載頭像圖片的地方),很是低。因而建議是CPU/IO並非真正的限制因素。而後看看是否是GPU的問題:在OpenGL ES Driver工具中檢測GPU利用率(圖12.9)。

12.9.jpg

圖12.9 OpenGL ES Driver工具顯示的GPU利用率

渲染服務利用率的值達到51%和63%。看起來GPU須要作不少工做來渲染聯繫人列表。

爲何GPU利用率這麼高呢?咱們來用Core Animation調試工具選項來檢查屏幕。首先打開Color Blended Layers(圖12.10)。

12.10.jpg

圖12.10 使用Color Blended Layers選項調試程序

屏幕中全部紅色的部分都意味着字符標籤視圖的高級別混合,這很正常,由於咱們把背景設置成了透明色來顯示陰影效果。這就解釋了爲何渲染利用率這麼高了。

那麼離屏繪製呢?打開Core Animation工具的Color Offscreen - Rendered Yellow選項(圖12.11)。

12.11.jpg

圖12.11 Color Offscreen–Rendered Yellow選項

全部的表格單元內容都在離屏繪製。這必定是由於咱們給圖片和標籤視圖添加的陰影效果。在代碼中禁用陰影,而後看下性能是否有提升(圖12.12)。

12.12.jpg

圖12.12 禁用陰影以後運行程序接近60FPS

問題解決了。幹掉陰影以後,滑動很流暢。可是咱們的聯繫人列表看起來沒有以前好了。那如何保持陰影效果並且不會影響性能呢?

好吧,每一行的字符和頭像在每一幀刷新的時候並不須要變,因此看起來UITableViewCell的圖層很是時候作緩存。咱們可使用shouldRasterize來緩存圖層內容。這將會讓圖層離屏以後渲染一次而後把結果保存起來,直到下次利用的時候去更新(見清單12.2)。

清單12.2 使用shouldRasterize提升性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (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.jpg

圖12.13 Color Hits Green and Misses Red驗證了緩存有效

結果和預期一致 - 大部分都是綠色,只有當滑動到屏幕上的時候會閃爍成紅色。所以,如今幀率更加平滑了。

因此咱們最初的設想是錯的。圖片的加載並非真正的瓶頸所在,並且試圖把它置於一個複雜的多線程加載和緩存的實現都將是徒勞。因此在動手修復以前驗證問題所在是個很好的習慣!

總結

在這章中,咱們學習了Core Animation是如何渲染,以及咱們可能出現的瓶頸所在。你一樣學習瞭如何使用Instruments來檢測和修復性能問題。

相關文章
相關標籤/搜索