NSTimer 基本使用和注意事項

NSTimer的基本使用

  • NSTimer: 一個在確定時間間隔內執行一次或多次我們指定對象方法的對象。

  • 基本使用:

兩個比較常用的方法:
timerWithTimeInterval: target: selector: userInfo: repeats:;

scheduledTimerWithTimeInterval: target: selector: userInfo: repeats:;

區別:

  • 第一個需要手動添加到Runloop 中。第二個不需要,自動就添加到了當前的Runloop 中。
第一種方式的使用:
_timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
//默認是 NSDefaultRunLoopMode
//NSRunLoopCommonModes 包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode,所以滑動的時候也能響應定時器
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

第二種方式:
//默認會自動添加到 NSDefaultRunLoopMode
_timer = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

- (void)timerAction {
    NSLog(@" === timerAction == ");
}

NSTimer在線程中的使用

  • 在子線程上直接使用是沒有反應的,因爲runloop 在子線程上,需要手動去開啓當前的runloop [[NSRunLoop currentRunLoop] run];
  • 在子線程上創建的定時器,必須要在子線程中銷燬,不要在主線程中銷燬,否者會造成runloop 資源泄露[self performSelector:@selector(invalidateTimer) withObject:nil afterDelay:3];
  • runloop 的創建方式不是通過alloc init 是通過 [NSRunLoop currentRunLoop] 來直接獲取的
  • 如果當前線程中有大量的複雜操作,會導致定時器的卡住
//子線程中使用定時器
[NSThread detachNewThreadSelector:@selector(threadTimer) toTarget:self withObject:nil];

//NSTimer 在線程中的使用
- (void)threadTimer {
    NSLog(@"%@",[NSThread currentThread]);
    //直接調用是沒有任何反應的
    //runloop 在子線程中是需要手動開啓的
    _timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

    //子線程上的定時器,必須要在子線程中銷燬,不要在主線程中銷燬,否者會造成runloop 資源泄露
    [self performSelector:@selector(invalidateTimer) withObject:nil afterDelay:3];

    //手動開啓runoop
    [[NSRunLoop currentRunLoop] run];

    //runloop 的創建方式不是通過alloc init 是通過 [NSRunLoop currentRunLoop] 來直接獲取的
    NSLog(@" === 子線程的timer 銷燬了 === ");
}
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        //如果在當前線程有複雜的操作,會導致定時器卡住
        [self busyCalculate];
    });

//如果在當前線程有複雜的操作,會導致定時器卡住
- (void)busyCalculate {
    NSUInteger count = 0xFFFFFFF;
    CGFloat num = 0;
    for (int i = 0; i < count; i ++) {
        num = i/count;
    }
}

NSTimer在UIScrollView中的使用

  • 當在scrollView 中滑動的時候,定時器會暫停,原因是默認的Timer是在NSDefaultRunLoopMode ,但是在滑動的時候runloop是UITrackingRunLoopMode,
  • runloop 同一時刻只能在一個mode 上來運行,其他 mode 上的任務暫停。
  • 所以在Timer 中最好是設置 mode爲 NSRunLoopCommonModes ,因爲NSRunLoopCommonModes 包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode,所以滑動的時候也能響應定時器

NSTimer循環引用的問題和解決

  • NSTimer 的銷燬(invalidate):
    • invalidate方法 會停止計時器的再次觸發,並在RunLoop中將其移除。
    • invalidate 方法 是將NSTimer對象從RunLoop 中移除的唯一方法。
    • 調用invalidate方法會刪除RunLoop對NSTimer的強引用,以及NSTimer對target和userInfo的強引用!
Description: 
//invalidate方法 會停止計時器的再次觸發,並在RunLoop中將其移除。
Stops the timer from ever firing again and requests its removal from its run loop.

//invalidate 方法 是將NSTimer對象從RunLoop 中移除的唯一方法。
This method is the only way to remove a timer from an NSRunLoop object.

//調用invalidate方法會刪除RunLoop對NSTimer的強引用,以及NSTimer對target和userInfo的強引用!
The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
  • NSTimer的循環引用(從NSTimer的描述和方法的描述可以看出)
    • 計時器與運行循環一起工作。RunLoop 維持着對定時器的強引用。
    • 當計時器觸發後,在調用invalidated 之前會一直保持對target的強引用
//計時器與運行循環一起工作。RunLoop 維持着對定時器的強引用。
Timers work in conjunction with run loops. Run loops maintain strong references to their timers, so you don’t have to maintain your own strong reference to a timer after you have added it to a run loop.
//當計時器觸發後,在調用invalidated 之前會一直保持對target的強引用
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to this object until it (the timer) is invalidated.
  • 內存泄露原因:控制器對 NSTimer 強引用,NSTimer 又對控制器強引用,RunLoop 對NSTimer 也強引用。這樣就造成了循環引用。

循環引用

@interface TimerViewController ()
@property (nonatomic,strong) NSTimer *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSDefaultRunLoopMode];

   //_timer = [NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

}

- (void)timerAction {
    NSLog(@" === timerAction == ");
}

- (void)dealloc {
    [_timer invalidate];
    _timer = nil;
    NSLog(@" _timer invalidate 銷燬了");
}

@end

以上代碼平時我們一般都是這樣使用的,但是這樣dealloc 方法一般是不會走到的,這樣定時器是永遠都不會銷燬的。

  • 解決循環引用的方法有三種

    • 一般情況下在直接在 viewWillDisappear 中手動去銷燬定時器
    • 自己實現一個帶block的定時器分類,實現一個不保留目標對象的定時器
    • 通過NSProxy 來處理內存泄露的問題
  • 1、在viewWillDisappear 中手動去銷燬定時器

//可以手動在 viewWillDisappear 中去銷燬定時器
//在控制器即將銷燬的時候銷燬定時器,這樣定時器對控制的強引用就解除了,循環引用也解除了
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [_timer invalidate];
    _timer = nil;
}
  • 2、自己實現一個帶block的定時器分類,實現一個不保留目標對象的定時器(把保留轉移到了定時器的類對象身上,這樣就避免了實例對象被保留。)
@interface NSTimer (RCTimer)

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti block:(void(^)(void))block repeats:(BOOL)repeats;

@end

@implementation NSTimer (RCTimer)

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti block:(void (^)(void))block repeats:(BOOL)repeats {

    return [NSTimer timerWithTimeInterval:ti target:self selector:@selector(timerAction:) userInfo:block repeats:repeats];
}

+ (void)timerAction:(NSTimer *)timer {
    void(^block)(void) = [timer userInfo];
    if (block) {
        block();
    }
}

@end

- (void)viewDidLoad {
    [super viewDidLoad];
    //引起循環引用的主要是 targrt:self
    //自己實現一個不保留目標的定時器
    __weak typeof(self)Weakself = self;
    _timer = [NSTimer timerWithTimeInterval:1.f block:^{
        [Weakself timerAction];
    } repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}
- (void)timerAction {
    NSLog(@" === timerAction == ");
}

- (void)dealloc {
      // 務必在當前線程調用invalidate方法,使得Runloop釋放對timer的強引用
     [_timer invalidate];
     _timer = nil;
}
 //在iOS 10 之後系統提供了類似的block 的方法來解決循環引用的問題
    __weak typeof(self)Weakself = self;
    _timer = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
        [Weakself timerAction];
    }];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];

    //schedule方式
    _timer = [NSTimer scheduledTimerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
        [Weakself timerAction];
    }];
  • 3、通過NSProxy 來處理循環引用的問題(參考YYKit)
#import <Foundation/Foundation.h>

@interface RCProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
-(instancetype)initWithTarget:(id)target;
+(instancetype)proxyWithTarget:(id)target;
@end

@implementation RCProxy
-(instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}
+(instancetype)proxyWithTarget:(id)target {
    return [[RCProxy 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)viewDidLoad {
    [super viewDidLoad];    
    _timer = [NSTimer scheduledTimerWithTimeInterval:1
                                                  target:[RCProxy proxyWithTarget:self]
                                                selector:@selector(timerAction)
                                                userInfo:nil
                                                 repeats:YES];
    }
}

- (void)timerAction {
    NSLog(@" === timerAction == ");
}

- (void)dealloc {
    [_timer invalidate];
    _timer = nil;
    NSLog(@" _timer invalidate 銷燬了");
}
  • NSProxy本身是一個抽象類,它遵循NSObject協議,提供了消息轉發的通用接口,NSProxy通常用來實現消息轉發機制和惰性初始化資源。不能直接使用NSProxy。需要創建NSProxy的子類,並實現init以及消息轉發的相關方法,纔可以用。
  • RCProxy繼承了NSProxy,定義了一個弱引用的target對象,通過重寫消息轉發等方法,讓target對象去處理接收到的消息。在整個引用鏈中,Controller對象強引用NSTimer對象,NSTimer對象強引用RCProxy對象,而RCProxy對象弱引用Controller對象,所以在YYWeakProxy對象的作用下,Controller對象和NSTimer對象之間並沒有相互持有,完美解決循環引用的問題。

這裏寫圖片描述

GCD實現定時器

  • GCD的定時器不受RunLoop中Mode的影響(RunLoop內部也是基於GCD實現的,可以根據源碼看到), 比如滾動TableView的時候,GCD的定時器不受影響;且比NSTimer更加準時。
@property (nonatomic,strong) dispatch_source_t timer;

- (void)gcdTimer:(NSTimeInterval)timeInterval repeats:(BOOL)repeats {
    //獲取隊列
    dispatch_queue_t queue = dispatch_queue_create(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //創建一個定時器(dispatch_source_t本質還是個OC對象,創建出來的對象需要強引用)
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //設置定時器的各種屬性(什麼時候開始任務,每隔多久執行一次)  GCD的時間參數,一般是納秒(1秒 = 10的9次方納秒)
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, timeInterval * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    //設置回調
    dispatch_source_set_event_handler(_timer, ^{
        if (!repeats) {
            dispatch_cancel(_timer);  //取消定時器
            _timer = nil;
        }else {
            [self timerAction];
        }
    });
    //啓動定時器
    dispatch_resume(_timer);

    //只執行一次操作
//    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.f * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//
//    });
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章