主頁 > 移動端開發 > iOS核心影片高級技巧 - 6

iOS核心影片高級技巧 - 6

2020-09-11 20:58:33 移動端開發

11. 基于定時器的影片

基于定時器的影片

我可以指導你,但是你必須按照我說的做, -- 駭客帝國

在第10章“緩沖”中,我們研究了CAMediaTimingFunction,它是一個通過控制影片緩沖來模擬物理效果例如加速或者減速來增強現實感的東西,那么如果想更加真實地模擬物理互動或者實時根據用戶輸入修改影片改怎么辦呢?在這一章中,我們將繼續探索一種能夠允許我們精確地控制一幀一幀展示的基于定時器的影片,

11.1 定時幀

定時幀

影片看起來是用來顯示一段連續的運動程序,但實際上當在固定位置上展示像素的時候并不能做到這一點,一般來說這種顯示都無法做到連續的移動,能做的僅僅是足夠快地展示一系列靜態圖片,只是看起來像是做了運動,

我們之前提到過iOS按照每秒60次重繪螢屏,然后CAAnimation計算出需要展示的新的幀,然后在每次螢屏更新的時候同步繪制上去,CAAnimation最機智的地方在于每次重繪需要展示的時候去計算插值和緩沖,

一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:1012951431, 分享BAT,阿里面試題、面試經驗,討論技術, 大家一起交流學習成長!希望幫助開發者少走彎路,

在第10章中,我們解決了如何自定義緩沖函式,然后根據需要展示的幀的陣列來告訴CAKeyframeAnimation的實體如何去繪制,所有的Core Animation實際上都是按照一定的序列來顯示這些幀,那么我們可以自己做到這些么?

NSTimer

實際上,我們在第三章“圖層幾何學”中已經做過類似的東西,就是時鐘那個例子,我們用了NSTimer來對鐘表的指標做定時影片,一秒鐘更新一次,但是如果我們把頻率調整成一秒鐘更新60次的話,原理是完全相同的,

我們來試著用NSTimer來修改第十章中彈性球的例子,由于現在我們在定時器啟動之后連續計算影片幀,我們需要在類中添加一些額外的屬性來存盤影片的fromValuetoValueduration和當前的timeOffset(見清單11.1),

清單11.1 使用NSTimer實作彈性球影片

@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 通過測量沒幀持續的時間來使得影片更加平滑

@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 looprun loop mode,對于run loop來說,我們就使用了主執行緒的run loop,因為任何用戶界面的更新都需要在主執行緒執行,但是模式的選擇就并不那么清楚了,每個添加到run loop的任務都有一個指定了優先級的模式,為了保證用戶界面保持平滑,iOS會提供和用戶界面相關任務的優先級,而且當UI很活躍的時候的確會暫停一些別的任務,

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

  • NSDefaultRunLoopMode- 標準優先級

  • NSRunLoopCommonModes - 高優先級

  • UITrackingRunLoopMode - 用于UIScrollView和別的控制元件的影片

在我們的例子中,我們是用了NSDefaultRunLoopMode,但是不能保證影片平滑的運行,所以就可以用NSRunLoopCommonModes來替代,但是要小心,因為如果影片在一個高幀率情況下運行,你會發現一些別的類似于定時器的任務或者類似于滑動的其他iOS影片會暫停,直到影片結束,

同樣可以同時對CADisplayLink指定多個run loop模式,于是我們可以同時加入NSDefaultRunLoopModeUITrackingRunLoopMode來保證它不會被滑動打斷,也不會被其他UIKit控制元件影片影響性能,像這樣:

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:構造器

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

 

11.2 物理模擬

物理模擬

即使使用了基于定時器的影片來復制第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的參考計數模型,所以我們需要準確的創建和釋放物件,為了簡化,我們把cpShapecpBody的生命周期和Crate類進行系結,然后在木箱的-init方法中創建,在-dealloc中釋放,木箱物理屬性的配置很復雜,所以閱讀了Chipmunk檔案會很有意義,

視圖控制器用來管理cpSpace,還有和之前一樣的計時器邏輯,在每一步中,我們更新cpSpace(用來進行物理計算和所有結構體的重新擺放)然后迭代物件,然后再更新我們的木箱視圖的位置來匹配木箱的模型(在這里,實際上只有一個結構體,但是之后我們將要添加更多),

Chipmunk使用了一個和UIKit顛倒的坐標系(Y軸向上為正方向),為了使得物理模型和視圖之間的同步更簡單,我們需要通過使用geometryFlipped屬性翻轉容器視圖的集合坐標(第3章中有提到),于是模型和視圖都共享一個相同的坐標系,

具體的代碼見清單11.3,注意到我們并沒有在任何地方釋放cpSpace物件,在這個例子中,記憶體空間將會在整個app的生命周期中一直存在,所以這沒有問題,但是在現實世界的場景中,我們需要像創建木箱結構體和形狀一樣去管理我們的空間,封裝在標準的Cocoa物件中,然后來管理Chipmunk物件的生命周期,圖11.1展示了掉落的木箱,

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

#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 = https://www.cnblogs.com/Julday/p/(__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 真實引力場下的木箱互動

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

對于實作影片的緩沖效果來說,計算每幀持續的時間是一個很好的解決方案,但是對模擬物理效果并不理想,通過一個可變的時間步長來實作有著兩個弊端:

  • 如果時間步長不是固定的,精確的值,物理效果的模擬也就隨之不確定,這意味著即使是傳入相同的輸入值,也可能在不同場合下有著不同的效果,有時候沒多大影響,但是在基于物理引擎的游戲下,玩家就會由于相同的操作行為導致不同的結果而感到困惑,同樣也會讓測驗變得麻煩,

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

我們想得到的理想的效果就是通過固定的時間步長來計算物理效果,但是在螢屏發生重繪的時候仍然能夠同步更新視圖(可能會由于在我們控制范圍之外造成不可預知的效果),

幸運的是,由于我們的模型(在這個例子中就是Chipmunk的cpSpace中的cpBody)被視圖(就是螢屏上代表木箱的UIView物件)分離,于是就很簡單了,我們只需要根據螢屏重繪的時間跟蹤時間步長,然后根據每幀去計算一個或者多個模擬出來的效果,

我們可以通過一個簡單的回圈來實作,通過每次CADisplayLink的啟動來通知螢屏將要重繪,然后記錄下當前的CACurrentMediaTime(),我們需要在一個小增量中提前重復物理模擬(這里用120分之一秒)直到趕上顯示的時間,然后更新我們的視圖,在螢屏重繪的時候匹配當前物理結構體的顯示位置,

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

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

#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幀或者增加CADisplayLinkframeInterval來保證不會隨機丟幀,不然你的影片將會看起來不平滑,

物理模擬

12. 性能調優

性能調優

代碼應該運行的盡量快,而不是更快 - 理查德

在第一和第二部分,我們了解了Core Animation提供的關于繪制和影片的一些特性,Core Animation功能和性能都非常強大,但如果你對背后的原理不清楚的話也會降低效率,讓它達到最優的狀態是一門藝術,在這章中,我們將探究一些影片運行慢的原因,以及如何去修復這些問題,

12.1. CPU VS GPU

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章中討論,

12.2 測量,而不是猜測

測量,而不是猜測

于是現在你知道有哪些點可能會影響影片性能,那該如何修復呢?好吧,其實不需要,有很多種詭計來優化影片,但如果盲目使用的話,可能會造成更多性能上的問題,而不是修復,

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

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

真機測驗,而不是模擬器

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

模擬器運行在你的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工具集來幫我們做到這些,

12.4 總結

總結

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

在下三章中,我們將對每個普通程式的性能陷阱進行詳細討論,然后學習如何修復,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/7513.html

標籤:iOS

上一篇:【iOS bug記錄】UICollectionviewCell重繪變得這么莫名其妙?

下一篇:為什么參考不了android.support.v4.app.ActivityCompat類

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more