【转】iOS-Core-Animation-Advanced-Techniques(六)
原文:http://www.cocoachina.com/ios/20150106/10839.html
? ?基于定時器的動畫和性能調優
基于定時器的動畫
我可以指導你,但是你必須按照我說的做。 -- 駭客帝國
在第10章“緩沖”中,我們研究了CAMediaTimingFunction,它是一個通過控制動畫緩沖來模擬物理效果例如加速或者減速來增強現實感的東西,那么如果想更加真實地模擬
物理交互或者實時根據用戶輸入修改動畫改怎么辦呢?在這一章中,我們將繼續探索一種能夠允許我們精確地控制一幀一幀展示的基于定時器的動畫。
定時幀
動畫看起來是用來顯示一段連續的運動過程,但實際上當在固定位置上展示像素的時候并不能做到這一點。一般來說這種顯示都無法做到連續的移動,能做的僅僅是足夠快地展示一系列靜態圖片,只是看起來像是做了運動。
我們之前提到過iOS按照每秒60次刷新屏幕,然后CAAnimation計算出需要展示的新的幀,然后在每次屏幕更新的時候同步繪制上去,CAAnimation最機智的地方在于每次刷新需要展示的時候去計算插值和緩沖。
在第10章中,我們解決了如何自定義緩沖函數,然后根據需要展示的幀的數組來告訴CAKeyframeAnimation的實例如何去繪制。所有的Core Animation實際上都是按照一定的序列來顯示這些幀,那么我們可以自己做到這些么?
NSTimer
實際上,我們在第三章“圖層幾何學”中已經做過類似的東西,就是時鐘那個例子,我們用了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 一個木箱圖片,根據模擬的重力掉落
添加用戶交互
下一步就是在視圖周圍添加一道不可見的墻,這樣木箱就不會掉落出屏幕之外。或許你會用另一個矩形的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.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 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 使用假數據的一個簡單聯系人列表
| 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 滑動幀率降到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提高性能
| 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 Color Hits Green and Misses Red驗證了緩存有效
結果和預期一致 - 大部分都是綠色,只有當滑動到屏幕上的時候會閃爍成紅色。因此,現在幀率更加平滑了。
所以我們最初的設想是錯的。圖片的加載并不是真正的瓶頸所在,而且試圖把它置于一個復雜的多線程加載和緩存的實現都將是徒勞。所以在動手修復之前驗證問題所在是個很好的習慣!
總結
在這章中,我們學習了Core Animation是如何渲染,以及我們可能出現的瓶頸所在。你同樣學習了如何使用Instruments來檢測和修復性能問題。
在下三章中,我們將對每個普通程序的性能陷阱進行詳細討論,然后學習如何修復。
?
轉載于:https://www.cnblogs.com/A--G/p/4707790.html
總結
以上是生活随笔為你收集整理的【转】iOS-Core-Animation-Advanced-Techniques(六)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: win10 64 安装VSS2005报错
- 下一篇: java-异常处理