NSTimerとCADisplayLinkの詳細とスクリーンFPSをテストするための小道具の作り方

6820 ワード

昨日、NSTimerとCADisplayLinkの2つのタイマーの違いを描いた記事を見ました.ちょうど感銘を受けて、突然スクリーンFPSをテストする小道具の実現構想を思いついた.
NSTimerもCADisplayLinkもタイミングの効果があることを知っています.違いは、NSTimerが時間に基づいているのに対し、CADisplayLinkがフレームレートに基づいていることである.NSTimerタイマの間隔を1/60秒に設定してタスクを実行する場合、実際の効果はCADisplayLinkと似ています.CADisplayLinkはフレームレートに基づいて実行されるため、iOS携帯電話のリフレッシュ頻度は1秒60回で、1/60秒ごとにリフレッシュ(デフォルト)に相当します.NSTimerでもCADisplayLinkでも、インタフェースのリフレッシュでは、最小の実行間隔時間は1/60秒未満ではありません.スクリーンの更新はそんなに速くないので、この時間より小さくても無駄です.
この論理に従って,1/60秒ごとに動作を実現するには,NSTimerとCADisplayLinkを用いて実現できる.しかしNSTimerのtimeIntervalプロパティは浮動小数点数であり、任意に所望の間隔を調整することができますが、CADisplayLinkはフレームレートに基づいてのみ動作を実行できます.frameIntervalという整数プロパティ(デフォルト値は1で、1フレームのリフレッシュを表し、2フレームのリフレッシュを1回、つまり1/30秒のリフレッシュを2フレームに変更)は実行頻度を変更することができますが、変更できる数値は限られています.しかしCADisplayLinkにはそれにふさわしい使用シーンがあり、今日ご紹介するテストFPSガジェットがその1つです.

NETimerの使用


カスタム値のタイマーを使用する場合は、NSTimerを使用すると、繰り返しの動作に対して任意の時間間隔をカスタマイズできます.このように:
{
  _timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
}

- (void)timerAction{
    NSLog(@"
--- %f",CACurrentMediaTime()); }

NSTimer実行Runloopで
しかしNSTimerの使用には欠陥がないわけではない.プログラムマスタースレッドのRunLoopでは、次のような処理が行われます.
  • タッチイベント
  • の処理
  • ネットワークパケット
  • の送受信
  • は、GCDコード
  • の使用を実行する.
  • 処理タイマ実行動作
  • スクリーン再描画
  • これらの時間がrunloopに追加されると、すべてdataSourceまたはtimeSource(イベントソース)と呼ばれます.NSTimerはtimeSourceに属するため、タイマを作成するたびに、対応するスレッドに追加するrunloopが実行される必要があります.これは、上記のコードにこのコードがある理由です.
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];
    

    NSRunLoopには多くのmodeがあり、デフォルトではNSDefaultRunLoopModeが使用されています.このほかにもNSRunLoopCommonModesやUItrackingRunLoopModeなどのmodeがあります.この2つのmodeはそれぞれ異なるシーンで使用されています.同じインタフェースのNSTimerとUIscrollViewは同時に実行されないことがあります.UIscrollViewをスライドするたびにNSTimerは一時停止します.RunLoopのmodeに優先度があるためです.UIscrollViewをスライドするとRunloopがUItrackingRunLoopModeに切り替わります.このとき、NSTimerが置かれているNSDefaultRunLoopModeは棚上げされ、NSTimerも一時停止します.解決策はNSTimerをNSRunLoopCommonModesまたはUItrackingRunLoopModeに組み込むことです.
    繰り返し実行されるNSTimerによるメモリリーク
    NSTimerがRunloopに加入した後に実行される原理は、RunloopにはNSTimer可toll-free-bridgeのクラスであるCFRunLoopTimerRefがあり、それは直接NSTimerと混用され、タイマとコールバック関数が含まれている.NSTimerのtargetを持っていて、後続の呼び出しに備えています.重複しないNStimerの場合、コールバック関数を1回実行すると、NSTimerが直接解放されます.これにより、NSTimerはtargatを保持しません.targetが解放されると、NSTimerは解放に従い、処理する必要はありません.しかし、繰り返し実行されるNSTimerの場合、NSTimerのtargetは解放されず、リファレンスサイクルが発生し、メモリが漏洩します.解決策は、Targeth(UIView/UIViewController)が消える前に、NSTimerのinvildataを使用して、NSTimerオブジェクトを停止して参照関係を解除することです.
    NSTimerの使用弊害
    NSTimerはある関数を時間間隔を置いて繰り返し実行することができるが,その時の京都は保証できない.いくつかの古いデバイスでは、NSTimerタイマの遅延とストライプ数がよく見られます.例えば、私は2秒ごとに実行するタイマーを作成しましたが、正確には2秒ごとに実行されず、実行が遅れる可能性があります.このような状況は、一般的なデータ処理ではまあまあです.しかし、アニメーションのシーンでは、アニメーションが非常にスムーズではありません.
    この原因は何ですか?
    NSTimerをRunloopに入れた後、runloopの中にタイマーのイベントソースが1つしかない場合、このタイマーは正確に実行されますが、runloopがしなければならない仕事は少なくありません.内部には他の様々なイベントソースがあります.これらのイベントソースにはタッチイベントがあります.データ送信イベントもあります.他のタイマーもあります.秩序ある順序で実行されます.タイマの前のイベントの実行に時間がかかると、後続のタイマが時間通りに実行できなくなります.これにより、タイマ実行時間のジャンプの問題が発生する.このジャンプは大きな問題を引き起こすことはありません.なぜなら、大きな時間でタイマーの実行回数は変わりません.タイマーの実行は遅刻するかもしれませんが、来ないことはありません.
    ここから、NSTimerは合成アニメーションに関するタイマーには向いていないことがわかります.その実行時間のジャンプはアニメーションにカートンの感覚をもたらすからです.
    まず、スクリーンのリフレッシュの頻度は毎秒60回であることを理解しなければなりません.つまり、システムは毎回描画する前に1/60の時間で描画する内容を計算し、ビューにレンダリングし、タイムリーに描画が完了しなければ、次の描画の時点で画像を敷くまで待っています.一般的に簡単な描画は、この時間内に完成し、スムーズな画面が見えます.描画された画像が複雑すぎると、1/60秒を超える時間がかかる可能性があります.このとき、次のフレームの画面が表示されるのは、前の時点の描画内容です.つまり、画面が一時停止し、肉眼ではカートンになります.タイマの実行時間ホッピングは、1/60秒で実行されるコンテンツに脆弱性を生じさせ、時間ホッピングによってシステムのリフレッシュを逃しやすくなります.これは画像レンダリングとは関係なく、実行される時間点に関係しています.タイマが1/60秒ごとに実行されることを正確に定義しても、時間ホッピングを実行するためにフレームレートを逃します.

    CADisplayLinkの使用


    前述したように、NSTimerはアニメーション関連のタイマーには向いていません.CADisplayLinkはほとんどこれのために生まれた.
    CADisplayLinkの実行間隔は、特定の時間ではなく、フレームごとに実行が呼び出されます.画面のフレームレートが60の場合、つまり1/60秒ごとに実行されます.これにより、画像の描画に時間がかかりすぎて、ある時点のレンダリングを逃した場合、大丈夫です.CADisplayLinkはそのために乱されず、設定できます.
    もちろん、CADisplayLinkやNSTimerにかかわらず、フレームレートのムラによるスクリーンカートンや遅くなることは完全に回避できません.画面をよりスムーズに表示するには、各フレーム間の時間間隔を手動で計算し、各フレームがレンダリングされる前に実行を呼び出すことが望ましい.この考え方はすでに実現している人がいるが,ここでは詳しくは言わない.
    CADisplayLinkのフレーム前呼び出し特性を用いてFPSテストを作成する
    CADisplayLinkの特性は各フレームの画面の前に呼び出され,この特性を利用して簡単なFPSテストツールを実現することができる.基本原理は簡単で、1秒あたりのCADisplayLinkの実行回数を集計すればよい.正常な状態で、この値は60です.画面カートンが現れると、この値は下がります.我々が作成するツールは,現在のFPS値をリアルタイムで表示することである.コードは以下の通りである.hファイル
    #import 
    
    @interface FPSWhatchDog : UIView
    
    + (void)showFPSLabel;
    + (void)dismissFPSLabel;
    
    @end
    

    .mファイル
    #import "FPSWhatchDog.h"
    
    @interface FPSWhatchDog ()
    
    @end
    
    @implementation FPSWhatchDog
    {
        CADisplayLink *_dispalyLink;
        UILabel *_fpsLabel;
        NSInteger _fps;
        NSTimeInterval _lastTime;
    }
    
    + (id)shareInstance{
        static FPSWhatchDog* tool = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            tool = [[FPSWhatchDog alloc] init];
        });
        return tool;
    }
    
    + (void)showFPSLabel{
        FPSWhatchDog *dog = [FPSWhatchDog shareInstance];
        [dog showLabel];
        [dog openDispalyLink];
    }
    
    + (void)dismissFPSLabel{
        FPSWhatchDog *dog = [FPSWhatchDog shareInstance];
        [dog invaildDisplayLink];
        [dog removeLabel];
    }
    
    - (void)showLabel{
        _fpsLabel = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 30)];
        _fpsLabel.text = @"60 FPS";
        _fpsLabel.textColor = [UIColor greenColor];
        _fpsLabel.backgroundColor = [UIColor darkGrayColor];
        _fpsLabel.layer.cornerRadius = 15.0;
        _fpsLabel.center = CGPointMake([UIScreen mainScreen].bounds.size.width/2.0, 85);
        _fpsLabel.textAlignment = NSTextAlignmentCenter;
        UIWindow *keyWindow = [[UIApplication sharedApplication] windows].firstObject;
        _fpsLabel.layer.zPosition = 100;  // 
        [keyWindow addSubview:_fpsLabel];
    }
    
    - (void)openDispalyLink{
        _fps = 0;
        _lastTime = (NSTimeInterval)CACurrentMediaTime(); // APP 
        _dispalyLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(countFPS)];
        [_dispalyLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    }
    
    
    - (void)invaildDisplayLink{
        [_dispalyLink invalidate];
    }
    
    - (void)removeLabel{
        [_fpsLabel removeFromSuperview];
    }
    
    - (void)countFPS{
        if (_lastTime + 0.5 <= (NSTimeInterval)CACurrentMediaTime()) {
            _lastTime = (NSTimeInterval)CACurrentMediaTime();
            _fpsLabel.text = [NSString stringWithFormat:@"%d FPS",(int)_fps * 2];
            _fps = 0;
        }else{
            _fps++;
        }
    }
    
    @end
    

    クラス全体が露出するのは、呼び出しメソッドと削除メソッドの2つのメソッドだけです.以下は私が5 Sシミュレータの下でテストした効果で、テスト内容はtableView上の各Cellが現れたときにcell上のいくつかの画像を変更して、それからスライドして、カートン現象を発見しました.このとき表示されるFPS値がフィードバックされます.
    2018-03-31 17_17_47.gif
    コード量が少ないので、Demoは貼らないで、直接copyで行けばいいです.