NSTimer、GCD定時器、CADisplayLink詳細分析

前言

? 您知道NSTimer是否一定需要手動調用invalidate方法?如何避免NSTimer的內存泄漏問題?NSTimer準時嗎?為什么大家都說GCD定時器比NSTimer時間更精確,這句話絕對正確嗎?NSTimer如果遇到延遲調用,會疊加觸發嗎?CADisplayLink又是干什么的呢?本文就帶著這些問題進行詳細的一一解答。

一、NSTimer

1、概念

? 定時器,在一段確定的時間后定時器開始工作,向target目標發送指定的消息(調用對應方法)。

2、NSTimer與target關系

? NSTimer會強引用target,直到timer調用invalidate()方法

開發者要不要手動調用invalidate()方法,分為兩種情況:

1)如果repeats為NO,則不需要手動調用invalidate:當定時器執行的時候一直是強引用target,當定時器執行一次結束后,系統自動調用invalidate方法,從而解除強引用。也就是說repeats為NO時,不會發生循環引用。驗證代碼如下:

// 詳情頁,從上個界面跳轉過來
@interface DetailViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
@end

@implementation DetailViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    // repeats為NO時,系統會自動執行invalidate,且不會發生循環引用
    self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:NO];
    NSLog(@"定時器開始工作");
}

- (void)dealloc {
    NSLog(@"DetailViewController dealloc");
}

- (void)timerAction:(NSTimer *)timer {
    self.num ++;
    NSLog(@"num = %ld", self.num);
}

@end

操作步驟:從ViewController跳轉到DetailViewController,然后馬上點擊返回按鈕。

運行結果:當定時器工作時,點擊返回按鈕后,DetailViewController并沒有馬上釋放,而是5s之后才釋放。

2020-01-10 09:44:20.567568+0800 OCTest[17155:820752] 定時器開始工作
2020-01-10 09:44:25.568842+0800 OCTest[17155:820752] num = 1
2020-01-10 09:44:25.569196+0800 OCTest[17155:820752] DetailViewController dealloc

2)如果repeats為YES,則需要手動調用invalidate:那在什么時候調用invalidate呢?在DetailViewController的dealloc方法里嗎?代碼驗證下:

// repeats為YES
self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];

操作步驟:大體代碼都和上面類似,只修改生成timer的代碼。從ViewController進入DetailViewController,過段時間再點擊返回按鈕。

運行結果:定時器一直在運行,點擊返回按鈕后,DetailViewController沒有走dealloc方法,也就是沒有釋放。

定時器開始工作
num = 1
num = 2
num = 3
...

此時就會發生內存泄漏,這里簡單說明下原因:NavigationController強持有DetailViewController,DetailViewController強持有timer(這里無論強持有或者弱持有都一樣),timer強持有DetailViewController,RunLoop強持有timer;點擊返回按鈕后,總有RunLoop持有timer,timer持有DetailViewController,所以就會發生內存泄漏。更多細節,請查看另一篇博客-iOS 內存管理

這里使用NSProxy進行消息轉發,解決內存泄漏問題:

創建一個TimerProxy類:

// .h文件
@interface TimerProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end

// .m文件
@interface TimerProxy ()
@property(nonatomic, weak) id target;
@end

@implementation TimerProxy

+ (instancetype)proxyWithTarget:(id)target {
    TimerProxy *proxy = [TimerProxy alloc]; //注意:沒有init方法
    proxy.target = target;
    return proxy;
}

// NSProxy接收到消息會自動進入到調用這個方法 進入消息轉發流程
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}
@end

那么DetailViewController的代碼是:

@interface DetailViewController ()
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;
@end

@implementation DetailViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
    NSLog(@"定時器開始工作");
}

- (void)dealloc {
    NSLog(@"DetailViewController dealloc");
    [self.timer invalidate];
}

- (void)timerAction:(NSTimer *)timer {
    self.num ++;
    NSLog(@"num = %ld", self.num);
}
@end

操作步驟和上面類似,

運行結果:DetailViewController會被釋放,此時在dealloc方法里調用timer的invalidate方法是合適的。

定時器開始工作
num = 1
num = 2
num = 3
DetailViewController dealloc
3、NSTimer需要添加到RunLoop中

? 創建NSTimer一般有兩種方法,一種直接創建使用,一種需要手動添加到RunLoop中。

1)直接創建使用:通過scheduledTimer創建一個定時器,系統默認把timer添加到當前的RunLoop中,模式是NSDefaultRunLoopMode。

self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];

2)手動添加到RunLoop中:通過timerWithTimeInterval創建一個定時器,需要手動把timer添加到RunLoop中,并指定RunLoop的Mode。

self.timer = [NSTimer timerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

這里順便說明invalidate方法的兩個作用:

  • 停止定時器
  • 把定時器從RunLoop中移除,并把定時器對target的強引用移除

至于RunLoop各種Mode怎么使用,請看-iOS RunLoop

4、NSTimer準時嗎

? 答案是否定的。因為NSTimer需要添加到RunLoop中,那么必然會受到RunLoop的影響,具體原因有兩個:

  • 受RunLoop循環處理的時間影響
  • 受RunLoop模式的影響

驗證時間影響:在ViewDidLoad中添加一個5s之后延時方法,并執行休眠5s(模擬RunLoop處理繁重任務)

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
    NSLog(@"定時器開始工作");
    // 執行繁重任務
    [self performSelector:@selector(performDelay) withObject:nil afterDelay:5.0];
}

- (void)performDelay {
    NSLog(@"Begin delay");
    sleep(5);
    NSLog(@"End delay");
}

運行結果:timer執行兩個周期之后1秒,開始執行繁重任務,5秒后繁重任務結束,重新開始執行定時器任務。注意:處理繁重任務中,定時器任務并沒有執行,也就是存在延時;處理繁重任務之后,timer并沒有連著觸發多次消息,而只是觸發了一次,并且執行完繁重的任務之后的觸發是正常的。也就是說NSTimer遇到RunLoop有繁重的任務會進行延遲,如果延遲時間超過一個周期,不會疊加在一起運行,即在一個周期內只會觸發一次,并且后面的timer的觸發時間總是倍數于第一次添加timer的間隙

2020-01-10 11:39:17.899898+0800 OCTest[18665:918402] 定時器開始工作
2020-01-10 11:39:19.901209+0800 OCTest[18665:918402] num = 1
2020-01-10 11:39:21.901262+0800 OCTest[18665:918402] num = 2
2020-01-10 11:39:22.901337+0800 OCTest[18665:918402] Begin delay
2020-01-10 11:39:27.902903+0800 OCTest[18665:918402] End delay
2020-01-10 11:39:27.903364+0800 OCTest[18665:918402] num = 3
2020-01-10 11:39:29.900309+0800 OCTest[18665:918402] num = 4

驗證模式影響:在ViewDidLoad中添加一個UITableView,當手指一直拽著tableView時,如果timer的Mode是NSDefaultRunLoopMode,那么定時器任務不會觸發。

創建UITableView的代碼省略,

直接顯示運行結果:拖拽著tableView,當前RunLoop模式由NSDefaultRunLoopMode切換到UITrackingRunLoopMode,此時不會觸發定時器消息;當拖拽結束并滾動完成減速后,35.17s觸發了33.57s本應該觸發的消息,然后接著觸發35.57s要觸發的消息,所以這里連續觸發了兩次

2020-01-10 13:24:25.573946+0800 OCTest[19514:973998] 定時器開始工作
2020-01-10 13:24:27.575234+0800 OCTest[19514:973998] num = 1
2020-01-10 13:24:29.575002+0800 OCTest[19514:973998] num = 2
// 開始拽著tableView
2020-01-10 13:24:30.658728+0800 OCTest[19514:973998] scrollViewWillBeginDragging
// 停止拖拽tableView
2020-01-10 13:24:34.635138+0800 OCTest[19514:973998] scrollViewDidEndDragging
// tableView完成減速
2020-01-10 13:24:35.169677+0800 OCTest[19514:973998] scrollViewDidEndDecelerating
// 13:24:35.17:這個時間周期是33s應該觸發的,但是被延遲了
2020-01-10 13:24:35.170381+0800 OCTest[19514:973998] num = 3 
// 13:24:35.57:35s時間觸發
2020-01-10 13:24:35.575338+0800 OCTest[19514:973998] num = 4
2020-01-10 13:24:37.575340+0800 OCTest[19514:973998] num = 5

綜上所述,NSTimer時間會被RunLoop處理時間和RunLoop模式切換影響。當然,如果把timer定時器Mode改為NSRunLoopCommonModes,那么就不會受模式切換影響,但仍然受RunLoop處理時間影響

5、NSTimer如何在子線程運行

? NSTimer雖然能在子線程運行,但是處理起來較為麻煩。先要創建線程,啟動線程,然后創建定時器,把定時器添加到當前的RunLoop中,最后運行RunLoop,還要注意內存泄漏問題,銷毀線程問題等

直接上代碼:

@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) NSTimer *timer;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    // thread會強引用self,直到線程結束
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
    [thread start];
}

- (void)dealloc {
    NSLog(@"DetailViewController dealloc");
}

- (void)startThread {
    NSLog(@"thread = %@", [NSThread currentThread]);
    // 創建timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:) userInfo:nil repeats:YES];
    // 把timer添加到當前子線程的RunLoop
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
    /*運行當前RunLoop,代碼運行于此,將不再執行下去,整個線程處于活躍。
    當線程中不再有需要執行的事件時,再會放開事件循環,代碼繼續執行下去。
    */
    [[NSRunLoop currentRunLoop] run];
}

- (void)timerAction:(NSTimer *)timer {
        NSLog(@"num = %ld, thread = %@", self.num ++, [NSThread currentThread]);
    if (self.num > 3) {
        [self.timer invalidate]; //需要在dealloc之前調用invalidate
    }
}

運行結果:DetailViewController完美釋放。

thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 0, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 1, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 2, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
num = 3, thread = <NSThread: 0x60000097f7c0>{number = 8, name = (null)}
TimerProxy dealloc
DetailViewController dealloc

必須在dealloc之前手動調用invalidate,才能避免內存泄漏。過程詳解:調用invalidate之后,子線程RunLoop移除timer,RunLoop沒有任何事件源,RunLoop結束,從而當前子線程結束,移除對self的強引用,點擊返回按鈕,會執行dealloc方法。

二、GCD定時器

? GCD定時器創建時不需要指定RunLoop的Mode,自然不受RunLoop模式切換的影響,但如果把GCD定時器放在主線程運行,仍然會受到RunLoop循環處理時間的影響。至于遇到繁重任務的情況,和NSTimer情況類似。GCD定時器如果在主線程運行,遇到MainRunLoop有繁重的任務會進行延遲,如果延遲時間超過一個周期,不會疊加在一起運行,即在一個周期內只會觸發一次,并且后面的timer的觸發時間總是倍數于第一次添加timer的間隙

@interface DetailViewController () 
@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) dispatch_source_t timer;
@end

@implementation DetailViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    // 創建一個定時器
    dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    self.timer = sourceTimer; //持有

    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
    uint64_t interval = (uint64_t)(2.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(sourceTimer, start, interval, 0);
    // 設置回調
    __weak typeof(self) wself = self;
    dispatch_source_set_event_handler(sourceTimer, ^{
        NSLog(@"num = %ld", wself.num ++); //注意:需要使用weakSelf,不然會內存泄漏
    });
    // 啟動定時器
    dispatch_resume(sourceTimer);
    NSLog(@"定時器開始工作");
}

- (void)dealloc {
    NSLog(@"DetailViewController dealloc");
    // 如果前面block回調使用了weakSelf,那么cancel可以寫在這里
    dispatch_source_cancel(self.timer);
}
@end

操作步驟:從ViewController進入DetailViewController,定時器運行,當num=2時點擊返回按鈕。

運行結果:DetailViewController立刻釋放

2020-01-10 14:57:44.440598+0800 OCTest[20989:1098517] 定時器開始工作
2020-01-10 14:57:46.441211+0800 OCTest[20989:1098517] num = 0
2020-01-10 14:57:48.441782+0800 OCTest[20989:1098517] num = 1
2020-01-10 14:57:50.441348+0800 OCTest[20989:1098517] num = 2
2020-01-10 14:57:51.347448+0800 OCTest[20989:1098517] DetailViewController dealloc

或者在event_handler回調中主動調用dispatch_source_cancel,這樣取消定時器后也能避免內存泄漏。

dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"num = %ld", self.num ++);
    if (self.num > 5) {
       dispatch_source_cancel(self.timer);
    }
});

操作步驟:從ViewController進入DetailViewController,定時器運行,當num=2時點擊返回按鈕。

運行結果:點擊返回按鈕后,DetailViewController并沒有馬上釋放,定時器的block一直運行,直到num>5時調用dispatch_source_cancel后,DetailViewController才進行釋放

2020-01-10 15:11:05.283164+0800 OCTest[21284:1119575] 定時器開始工作
2020-01-10 15:11:07.283189+0800 OCTest[21284:1119575] num = 0
2020-01-10 15:11:09.283260+0800 OCTest[21284:1119575] num = 1
2020-01-10 15:11:11.283546+0800 OCTest[21284:1119575] num = 2
2020-01-10 15:11:13.284427+0800 OCTest[21284:1119575] num = 3
2020-01-10 15:11:15.284450+0800 OCTest[21284:1119575] num = 4
2020-01-10 15:11:17.283821+0800 OCTest[21284:1119575] num = 5
2020-01-10 15:11:17.284562+0800 OCTest[21284:1119575] DetailViewController dealloc

當然,GCD定時器也能在子線程運行,不用添加到RunLoop中。

// 在global_queue上運行timer
dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

運行結果:event_handler在多個線程進行回調處理。

定時器開始工作
num = 0, thread = <NSThread: 0x60000211fcc0>{number = 5, name = (null)}
num = 1, thread = <NSThread: 0x60000216ef00>{number = 4, name = (null)}
num = 2, thread = <NSThread: 0x60000216ef00>{number = 4, name = (null)}
DetailViewController dealloc

注意:如果GCD定時器在子線程運行,主線程RunLoop即使有繁重的任務,也會準時觸發

代碼如下:

@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) dispatch_source_t timer;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_source_t sourceTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    self.timer = sourceTimer;

    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
    uint64_t interval = (uint64_t)(2.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(sourceTimer, start, interval, 0);
    __weak typeof(self) wself = self;
    dispatch_source_set_event_handler(sourceTimer, ^{
        NSLog(@"num = %ld, thread = %@", wself.num ++, [NSThread currentThread]);
        /*
        //如果在block里有繁重的任務處理,GCD定時器也會延時
        if (wself.num == 3) {
            [wself performDelay];
        }
         */
    });
    dispatch_resume(sourceTimer);
    NSLog(@"定時器開始工作");
    
    // 模擬在主線程有繁重任務
    [self performSelector:@selector(performDelay) withObject:nil afterDelay:5.0];
}

- (void)performDelay {
    NSLog(@"Begin delay");
    sleep(4);
    NSLog(@"End delay");
}

運行結果:GCD定時器在子線程運行,不會受到主線程RunLoop執行時間的影響

2020-01-13 09:47:26.236410+0800 OCTest[24444:1339137] 定時器開始工作
2020-01-13 09:47:28.236864+0800 OCTest[24444:1339777] num = 0, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
2020-01-13 09:47:30.237588+0800 OCTest[24444:1339415] num = 1, thread = <NSThread: 0x600000d4a340>{number = 10, name = (null)}
2020-01-13 09:47:31.237452+0800 OCTest[24444:1339137] Begin delay
2020-01-13 09:47:32.237753+0800 OCTest[24444:1339777] num = 2, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
2020-01-13 09:47:34.237759+0800 OCTest[24444:1339777] num = 3, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}
2020-01-13 09:47:35.238945+0800 OCTest[24444:1339137] End delay
2020-01-13 09:47:36.237684+0800 OCTest[24444:1339415] num = 4, thread = <NSThread: 0x600000d4a340>{number = 10, name = (null)}
2020-01-13 09:47:38.237792+0800 OCTest[24444:1339777] num = 5, thread = <NSThread: 0x600000d8bcc0>{number = 9, name = (null)}

總結:如果GCD定時器在主線程運行,那么會受到RunLoop運行時間影響,即和NSTimer類似,時間可能不會很精確;如果GCD定時器在子線程運行,那么不會受到MainRunLoop的影響,時間就很精確。當然,如果GCD的block回調處理繁重任務,時間也會進行相應的延時

三、CADisplayLink

1、概念

? CADisplayLink是一個執行頻率(fps)和屏幕刷新相同的定時器(可以修改preferredFramesPerSecond屬性來修改具體執行的頻率)。時間精度比NSTimer高,但是也要添加到RunLoop里。通常情況下CADisaplayLink用于構建幀動畫,看起來相對更加流暢,而NSTimer則有更廣泛的用處。

2、基本使用

? CADisplayLink和NSTimer類似,會容易造成循環引用問題,所以還是需要一個中間類TimerProxy來解決內存泄漏問題。如果設置RunLoop的模式是NSDefaultRunLoopMode,那么也會受到RunLoop模式切換的影響。在Dealloc方法里必須調用invalidate方法釋放定時器。

代碼如下:

@property(nonatomic, assign) NSInteger num;
@property(nonatomic, strong) CADisplayLink *timer;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.num = 0;
    
    CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:[TimerProxy proxyWithTarget:self] selector:@selector(timerAction:)];
    self.timer = timer;
    if (@available(iOS 10.0, *)) {
        timer.preferredFramesPerSecond = 30; //30幀
    } else {
        timer.frameInterval = 2; //屏幕刷新60幀,每2幀刷一次,就是每秒30幀頻率
    }
    /**
    添加到當前的RunLoop
    NSDefaultRunLoopMode:默認模式,會受到RunLoop模式切換的影響
    NSRunLoopCommonModes:不會受RunLoop模式切換的影響
    */
    [timer addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
}

- (void)dealloc {
    NSLog(@"DetailViewController dealloc");
    [self.timer invalidate]; 
}

- (void)timerAction:(CADisplayLink *)timer {
    NSLog(@"num = %ld", self.num ++);
}
3、制作FPS工具

? 根據CADisplayLink是一個執行頻率(fps)和屏幕刷新相同的定時器原理,可以制作一個FPS檢測器。具體代碼如下:

#define kFPSLabelSize CGSizeMake(55, 20)

// .h
@interface FPSLabel : UILabel
@end

// .m
@implementation FPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    if (!self) { return nil; }
    
    self.layer.cornerRadius = 5;
    self.clipsToBounds = YES;
    self.textAlignment = NSTextAlignmentCenter;
    self.userInteractionEnabled = NO;
    self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];
    
    _link = [CADisplayLink displayLinkWithTarget:[TimerProxy proxyWithTarget:self] selector:@selector(tick:)];
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    return self;
}

- (void)dealloc {
    [_link invalidate];
}

- (CGSize)sizeThatFits:(CGSize)size {
    return kFPSLabelSize;
}

- (void)tick:(CADisplayLink *)link {
    if (_lastTime == 0) {
        _lastTime = link.timestamp;
        return;
    }
    
    _count++;
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return; // 計算一秒的次數
    _lastTime = link.timestamp;
    float fps = _count / delta;
    _count = 0; // 重新計數
    
    CGFloat progress = fps / 60.0;
    // 根據色調,飽和度,亮度生成顏色
    UIColor *color = [UIColor colorWithHue:0.27 * (progress - 0.2) saturation:1 brightness:0.9 alpha:1];
    
    NSMutableAttributedString *text = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%d FPS",(int)round(fps)]];
    [text addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, text.length-3)];
    [text addAttribute:NSForegroundColorAttributeName value:[UIColor whiteColor] range:NSMakeRange(text.length-3, 3)];
    [text addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:NSMakeRange(0, text.length)];
    self.attributedText = text;
}
@end

使用FPSLabel:

_fpsLabel = [FPSLabel new];
CGRect frame = self.view.frame;
_fpsLabel.frame = CGRectMake(15, frame.size.height-15-kFPSLabelSize.height, kFPSLabelSize.width, kFPSLabelSize.height);
[self.view addSubview:_fpsLabel];

三個定時器最終總結

  • NSTimer:使用頻繁,一般在主線程中運行,添加到主RunLoop中;受到RunLoop模式切換影響和RunLoop運行時間影響;使用時,注意內存泄漏問題、RunLoop模式切換問題、調用invalidate方法時機問題等。
  • GCD定時器:使用頻繁,不需要主動添加到RunLoop中,不受到模式切換的影響;如果GCD定時器在主線程運行,那么還是會受到主RunLoop運行時間的影響;如果GCD定時器在子線程運行,那么不會受到主RunLoop的影響,所以這個場景下,時間精確度比NSTimer要高。使用時,需要注意內存泄漏問題、dispatch_source_cancel調用時機問題等。
  • CADisplayLink:使用較少,一般使用在與幀動畫有關的場景,保持和屏幕幀率一致的定時器,也可以制作FPS檢測工具。使用時,也要注意內存泄漏問題、RunLoop模式切換問題、調用invalidate方法時機問題等。

如果對RunLoop感興趣,請查看-iOS RunLoop

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容