iOS NSTimer定时器

Posted 长沙火山

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS NSTimer定时器相关的知识,希望对你有一定的参考价值。

ios中定时器有三种,分别是NSTimer、CADisplayLink、dispatch_source,下面就分别对这三种计时器进行说明。

一、NSTimer

NSTimer这种定时器用的比较多,但是特别需要注意释放问题,如果处理不好很容易引起循环引用问题,造成内存泄漏。

1.1 NSTimer的创建

NSTimer有两种创建方法。

方法一:

这种方法虽然创建了NSTimer,但是定时器却没有起作用。这种方式创建的NSTimer,需要加入到NSRunLoop中,有NSRunLoop的驱动才会让定时器跑起来。

self.timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

方法二:

这种方法创建的NSTimer不需要加入到NSRunLoop中就能跑起来,因为创建了定时器之后,系统默认会把定时器加到RunLoop中,不需要我们自己手动加了。

self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];

1.2 NSTimer的释放

- (void)dealloc

    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"dealloc...");

通常这样写其实是有问题的,由于self强引用了timer,同时timer也强引用了self,所以循环引用造成dealloc方法根本不会走,self和timer都不会被释放,造成内存泄漏。

二、循环引用的几种解决办法

3.1 使用invalidate结束timer运行

我们第一时间肯定想到的是[self.timer invalidate]不就可以了吗,当然这是正确的思路,那么我们调用时机是什么呢?viewWillDisAppear还是viewDidDisAppear?实际上在我们实际操作中,如果当前页面有push操作的话,当前页面还在栈里面,这时候我们释放timer肯定是错误的,所以这时候我们可以用到下面的方法:

- (void)didMoveToParentViewController:(UIViewController *)parent 
    // 无论push 进来 还是 pop 出去 正常运行
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) 
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer销毁");
    

3.2 中介者模式

换个思路,timer会造成循环引用是因为target强持有了self,造成的循环引用,那我们是否可以包装一下target,使得timer绑定另外一个不是self的target对象来打破这层强持有关系。

@interface HSTimer : NSObject

+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo
                                     repeats:(BOOL)repeats;
@end
#import "HSTimer.h"

@interface HSTimerTarget : NSObject
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
@property (nonatomic, weak) NSTimer* timer;
@end

@implementation HSTimerTarget
- (void)timeAction:(NSTimer *)timer 
    if(self.target) 
        [self.target performSelector:self.selector withObject:timer.userInfo afterDelay:0.0f];
     else 
        [self.timer invalidate];
    

@end

@implementation HSTimer
+ (NSTimer *) scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                      target:(id)aTarget
                                    selector:(SEL)aSelector
                                    userInfo:(id)userInfo 
                                     repeats:(BOOL)repeats 
    HSTimerTarget* timerTarget = [[HSTimerTarget alloc] init];
    timerTarget.target = aTarget;
    timerTarget.selector = aSelector;
    timerTarget.timer = [NSTimer scheduledTimerWithTimeInterval:interval
                                                         target:timerTarget
                                                       selector:@selector(timeAction:)
                                                       userInfo:userInfo
                                                        repeats:repeats];
    return timerTarget.timer;

@end

使用:

#import "HSTimer.h"

@property(nonatomic, strong) NSTimer *timer;

 self.timer = [HSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];

3.3 NSProxy虚基类的方式

NSProxy是一个虚基类,它的地位等同于NSObject。我们不用self来响应timer方法的target,而是用NSProxy来响应。

HSProxy.h

@interface HSProxy : NSObject
+ (instancetype)proxyWithTransformObject:(id)object;
@end

HSProxy.m

#import "HSProxy.h"

@interface HSProxy ()
@property (nonatomic, weak) id object;
@end

@implementation HSProxy

+ (instancetype)proxyWithTransformObject:(id)object 
    HSProxy *proxy = [HSProxy alloc];
    proxy.object = object; // 我们拿到外边的self,weak弱引用持有
    return proxy;

// 仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self,这一步至关重要,主要涉及到runtime的消息机制。
// proxy虚基类并没有持有vc,而是消息的转发,又给了vc
- (id)forwardingTargetForSelector:(SEL)aSelector 
    return self.object;


@end

使用

#import "HSProxy.h"

@property(nonatomic, strong) HSProxy *proxy;

self.proxy = [HSProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self.proxy selector:@selector(timeEvent) userInfo:nil repeats:YES];

三、拓展

3.1 能不能用weakSelf打破循环引用?

 __weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:weakSelf selector:@selector(timeEvent) userInfo:nil repeats:YES];

答案:不能。

weakSelf 和 self 虽然指针地址不一样,但是都是指向当前的vc,也就是说这两者指向的内存地址其实是一样的,所以传入target里面的其实还是当前的vc,这样还是不能打破循环引用。

3.2 滑动tableView或者scrollView会暂停NSTimer

在同一个ViewController里面创建NSTimer和tableView或者scrollView,当滑动tableView或者scrollView的时候,会发现NSTimer会暂停,当tableView或者scrollView停止之后,NSTimer又会继续运行。这是为什么呢?

RunLoop在不同的情况下,运行的模式不一样:

在viewDidApperar()方法中打印的Current Mode为:kCFRunLoopDefaultMode

在scrollViewDidScroll()方法中打印的Current Mode为:UITrackingRunLoopMode

因为NSTimer对象默认添加到了当前RunLoop的DefaultMode中,而在切换成TrackingRunLoopMode时,定时器就会停止工作。

解决该问题最直接方法是,将NSTimer在TrackingRunLoopMode中也添加一份。这样的话无论是在 DefaultMode还是TrackingRunLoopMode中,定时器都会正常的工作。

如果你对RunLoop比较熟悉的话,可以知道CommonModes就是DefaultModeTrackingRunLoopMode的集合,所以我们只需要将NSTimer对象与当前线程所对应的RunLoop中的CommonModes关联即可。

self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timeEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

3.3 NSTimer精度不高

NSTimer计时的精度不是很高,但是项目中一般的需求还是能满足,如果需要更高的计时精度就需要用CADisplayLink。

以上是关于iOS NSTimer定时器的主要内容,如果未能解决你的问题,请参考以下文章

iOS NSTimer定时器

iOS NSTimer定时器

iOS中定时器NSTimer使用

iOS-NSTimer的3种创建方式

转IOS NSTimer 定时器用法总结

iOS中定时器NSTimer的使用/开启与关闭