NSTimer使用詳解

lmg4819發表於2018-05-15

1.開發中如何使用NSTimer

1.   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];複製程式碼

2.  self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
複製程式碼

這兩個方法是等價的,區別是第一個方法預設建立了一個NSTimer並自動新增到了當前執行緒的Runloop中去,第二個需要我們手動新增。如果當前執行緒是主執行緒的話,某些UI事件,比如UIScrollView的拖拽操作,會將Runloop切換成UITrackingRunLoopMode,這時候,預設的NSDefaultRunLoopMode模式中註冊的事件是不會被執行的。所以為了設定一個不會被UI干擾的Timer,我們需要手動將timer的當前RunloopMode設定為NSRunLoopCommonModes,這個模式等效於NSDefaultRunLoopMode和UITrackingRunLoopMode的結合。

2.NSTimer無法釋放的原因分析

上面的使用方法是沒問題的,但是大家在使用過程中一定遇到過因使用了NSTimer,導致所在的UIViewController記憶體洩漏的問題,這種原因是怎麼出現的呢?
其中許多人都認為是UIViewController和NSTimer迴圈引用的問題,彼此強引用,導致了彼此無法釋放,那麼問題真的是這樣嗎?
  1. 驗證如下:

     NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerFired) userInfo:nil repeats:YES];
     [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    //結果:將NSTimer設定成區域性變數,你會發現兩者仍釋放不了。複製程式碼
  2. 將self設定成弱引用,又會是什麼現象呢?

     __weak typeof(self) weakSelf = self;
     NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:weakSelf selector:@selector(timerFired) userInfo:nil repeats:YES];
     [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    //結果:兩者仍然無法釋放。複製程式碼
  3. 如果我們將target強制釋放,強制破壞迴圈引用呢?

     TimerAction *Test = [TimerAction new];
     NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:Test selector:@selector(test) userInfo:nil repeats:YES];
     [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
     CFRelease((__bridge CFTypeRef)(Test));
    //結果:Test順利釋放,但Timer仍在執行。並且在Timer觸發事件時崩潰複製程式碼
  4. 在timer建立後面斷點,檢視執行的時候記憶體圖

NSTimer使用詳解

結果:其實只有timer單向的指向target,target並未指向timer,是因為timer執行的時候釋放不了,導致被強引用的target也無法釋放。並非迴圈引用導致不釋放。

3.解決NSTimer的記憶體洩漏問題

一般呢解決NSTimer的記憶體洩漏問題,通常有兩種方法,第一種是找對合適的時機釋放NSTimer,通常人們會想到兩個呼叫時機。
  1. -(void)dealloc
     {
        [self.timer invalidate];
     }
    //NSTimer,通常人們會想到兩個呼叫時機。
    複製程式碼
  2. -(void)viewWillDisappear:(BOOL)animated
     {
        [super viewWillDisappear:animated];
        [self.timer invalidate];
     }
    //這種情況是可以解決迴圈引用的問題,記憶體可以釋放,但是又會引來新的問題,當導航控制器push到下一個頁面時,當前VC並沒有被釋放,這時候我們可能並不想銷燬NSTimer,我們通常希望VC該銷燬的時候,同時銷燬NSTimer,所以呼叫invalidate方法的時機很難找複製程式碼
那麼就是第二種了,想辦法破除強引用,讓NSTimer和VC同生共死,這種方法呢也有兩種方式

1.使用block的方式:

 #import <Foundation/Foundation.h>
 typedef void(^JSTimerBlcok)(NSTimer *timer);
 @interface NSTimer (Category)

 + (NSTimer *)js_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval executeBlock:(JSTimerBlcok)block repeats:(BOOL)repeats;

 @end

 #import "NSTimer+Category.h"

 @implementation NSTimer (Category)

 +(NSTimer *)js_scheduledTimerWithTimeInterval:(NSTimeInterval)timeInterval executeBlock:(JSTimerBlcok)block repeats:(BOOL)repeats
 {

 NSTimer *timer = [self scheduledTimerWithTimeInterval:timeInterval target:self selector:@selector(js_executeTimer:) userInfo:[block copy] repeats:repeats];

 return timer;

 }

 +(void)js_executeTimer:(NSTimer *)timer

 {
     JSTimerBlcok block = timer.userInfo;
     if (block) {
     block(timer);
     }
 }
 @end
使用案例: - (void)viewDidLoad { [super viewDidLoad]; __weak typeof(self) weakSelf = self; self.timer = [NSTimer js_scheduledTimerWithTimeInterval:1.0 executeBlock:^(NSTimer *timer){         __strong typeof(weakSelf) strongSelf = weakSelf;        [strongSelf timerFired:timer];       } repeats:YES]; }複製程式碼

2.使用NSProxy來初始化一個子類,這裡我們直接用YYWeakProcy

#import <Foundation/Foundation.h>
 
 NS_ASSUME_NONNULL_BEGIN

@interface YYWeakProxy : NSProxy

@property (nullable, nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END
 
 #import "YYWeakProxy.h"
 
 
 @implementation YYWeakProxy
 
 - (instancetype)initWithTarget:(id)target {
 _target = target;
 return self;
 }
 
 + (instancetype)proxyWithTarget:(id)target {
 return [[YYWeakProxy alloc] initWithTarget:target];
 }
 
 - (id)forwardingTargetForSelector:(SEL)selector {
 return _target;
 }
 
 - (void)forwardInvocation:(NSInvocation *)invocation {
 void *null = NULL;
 [invocation setReturnValue:&null];
 }
 
 - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
 return [NSObject instanceMethodSignatureForSelector:@selector(init)];
 }
 
 - (BOOL)respondsToSelector:(SEL)aSelector {
 return [_target respondsToSelector:aSelector];
 }
 
 - (BOOL)isEqual:(id)object {
 return [_target isEqual:object];
 }
 
 - (NSUInteger)hash {
 return [_target hash];
 }
 
 - (Class)superclass {
 return [_target superclass];
 }
 
 - (Class)class {
 return [_target class];
 }
 
 - (BOOL)isKindOfClass:(Class)aClass {
 return [_target isKindOfClass:aClass];
 }
 
 - (BOOL)isMemberOfClass:(Class)aClass {
 return [_target isMemberOfClass:aClass];
 }
 
 - (BOOL)conformsToProtocol:(Protocol *)aProtocol {
 return [_target conformsToProtocol:aProtocol];
 }
 
 - (BOOL)isProxy {
 return YES;
 }
 
 - (NSString *)description {
 return [_target description];
 }
 
 - (NSString *)debugDescription {
 return [_target debugDescription];
 }
 
 @end
使用案例:
 - (void)initTimer {
    YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
    _timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
 }
//至於具體的原理,讓NSTimer定時中的方法由YYWeakProxy轉發給VC執行.但是NStimer持有的卻不是VC.這樣就不會迴圈引用.複製程式碼

4.開發中如何建立更精確的定時器

大家應該知道,NSTimer的精確度一般能達到1ms,也就是小於1毫秒時,誤差會很大,那麼如何建立一個誤差很小,甚至沒有誤差的定時器呢
  1. 納秒級精度的Timer

    #include <mach mach.h="">
     #include <mach mach_time.h="">
     static const uint64_t NANOS_PER_USEC = 1000ULL;
    
     static const uint64_t NANOS_PER_MILLISEC = 1000ULL * NANOS_PER_USEC;
     static const uint64_t NANOS_PER_SEC = 1000ULL * NANOS_PER_MILLISEC;
     static mach_timebase_info_data_t timebase_info;
    
     static uint64_t nanos_to_abs(uint64_t nanos) {
    
     return nanos * timebase_info.denom / timebase_info.numer;
    
     }
     void waitSeconds(int seconds) {
    
     mach_timebase_info(&timebase_info);
    
     uint64_t time_to_wait = nanos_to_abs(seconds * NANOS_PER_SEC);
    
     uint64_t now = mach_absolute_time();
    
     mach_wait_until(now + time_to_wait);
    
     }</mach></mach>
    //理論上這是iPhone上最精準的定時器,可以達到納秒級別的精度複製程式碼
  2. CADisplayLink

    CADisplayLink * displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
     [displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    //CADisplayLink是一個頻率能達到螢幕重新整理率的定時器類。iPhone螢幕重新整理頻率為60幀/秒,也就是說最小間隔可以達到1/60s。複製程式碼
  3. GCD定時器

    NSTimeInterval interval = 1.0;
     _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
    
     dispatch_source_set_timer(_timer, dispatch_walltime(NULL, 0), interval * NSEC_PER_SEC, 0);
    
     dispatch_source_set_event_handler(_timer, ^{
    
     NSLog(@"GCD timer test");
    
     });
     dispatch_resume(_timer);
    //RunLoop是dispatch_source_t實現的timer,所以理論上來說,GCD定時器的精度比NSTimer只高不低。複製程式碼

參考資料:

www.cnblogs.com/pioneerMax/…

blog.csdn.net/allangold/a…

www.aliyun.com/jiaocheng/3…l