NSTimer

牛犊子们好,我是shark, 老规矩,上问题,搞懵逼你们

1: 咋滚动下UI定时器就不起效了?
2: NSTimer为什么要添加到RunLoop中才会有作用
3: NSTimer加到了RunLoop中但有时迟迟的不触发事件
4: 为啥使用NSTimer容易导致循环应用?
5: 解决NSTimer循环应用你有几种方式?
6: 为啥NSTimer定时不准确?你有啥替代方案不?
7: 为啥在子线程使用NSTimer定时不起效?怎么解决?

有个疑问先先问牛犊子们,为啥如图下,还是无法解决循环引用问题,明明都是弱引用了,求解答


image.png

我也只能是猜测了 weak关键字适用于block,当block引用了块外的变量时,会根据修饰变量的关键字来决定是强引用还是弱引用,如果变量使用weak关键字修饰,那block会对变量进行弱引用,如果没有__weak关键字,那就是强引用。
  但是NSTimer的 scheduledTimerWithTimeInterval:target方法内部不会判断修饰target的关键字,所以这里传self 和 weakSelf是没区别的,其内部会对target进行强引用,还是会产生循环引用。

1: 咋滚动下UI定时器就不起效了?
//在主线程中调用
_timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(logInfo) userInfo:nil repeats:YES]

当你创建NSTimer时,没有将对象添加到NSRunLoop中并指定runloop model 模式runloop默认会添加到RunLoopDefaultMode中,当页面有滑动时,主线程的runloop会切换到TrackingRunLoopMode,此模式下,NSTimer不会被触发,只需
[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
注释:NSTimer的定时原理与NSRunLoop息息相关

2: NSTimer为什么要添加到RunLoop中才会有作用

NSTimer其实也是一种资源,如果看过多线程变成指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会又效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。你可能会说那我们APP的主线程的runloop我们没有往其中添加任何资源,为什么它还好好的运行。我们不添加,不代表框架没有添加

3: NSTimer加到了RunLoop中但有时迟迟的不触发事件

牛犊子,你肯定还没怎么明白RunLoop与NSTimer的关系
为什么明明添加了,但是就是不按照预先的逻辑触发事件呢???原因主要有以下两个:

1、runloop是否运行

每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。
那么如果我们把一个timer添加到了非主线的runloop中,它还会按照预期按时触发吗?下面请看一段测试程序:

- (void)applicationDidBecomeActive:(UIApplication *)application 
{
    [NSThread detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil]; 
}
// 测试把timer加到不运行的runloop上的情况 
- (void)testTimerSheduleToRunloop1 
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  
    NSLog(@"Test timer shedult to a non-running runloop");
    SvTestObject *testObject4 = [[SvTestObject alloc] init]; 
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject4 selector:@selector(timerAction:) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; 
    // 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去 //NSLog(@"the thread's runloop: %@",
    [NSRunLoop currentRunLoop]); 
    // 打开下面一行, 该线程的runloop就会运行起来,timer才会起作用 
    //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    [testObject4 release]; 
    NSLog(@"invoke release to testObject4"); 
    [pool release]; 
}
- (void)applicationWillResignActive:(UIApplication *)application 
{
    NSLog(@"SvTimerSample Will resign Avtive!"); 
}

上面的程序中,我们新创建了一个线程,然后创建一个timer,并把它添加当该线程的runloop当中,但是运行结果如下:


image.png

观察运行结果,我们发现这个timer知道执行退出也没有触发我们指定的方法,如果我们把上面测试程序中“//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];”这一行的注释去掉,则timer将会正确的掉用我们指定的方法。

2、mode是否正确

我们前面自己动手添加runloop的时候,可以看到有一个参数runloopMode,这个参数是干嘛的呢?

前面提到了要想timer生效,我们就得把它添加到指定runloop的指定mode中去,通常是主线程的defalut mode。但有时我们这样做了,却仍然发现timer还是没有触发事件。这是为什么呢?

这是因为timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会。

举个不恰当的例子,我们说兄弟几个分别代表runloop的mode,timer代表他们自己的才水桶,然后一群人去排队打水,只有一个水龙头,那么同一时刻,肯定只能有一个人处于接水的状态。也就是说你虽然给了老二一个桶,但是还没轮到它,那么你就得等,只有轮到他的时候你的水桶才能碰上用场。

综上: 要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配。

4: 为啥使用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];

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建立后面断点,查看运行的时候内存图


image.png

结果:其实只有timer单向的指向target,target并未指向timer,是因为timer运行的时候释放不了,导致被强引用的target也无法释放。并非循环引用导致不释放。

5: 解决NSTimer循环应用你有几种方式?

一般就是出于两种思路1
1:找对合适的时机释放NSTimer
2:想办法破除强引用,让NSTimer和VC同生共死
出于这两种思路,我们开始尝试解决
方法1:
既然说TimerViewController和timer之间互相强引用,那么如果将之间的一个强指针改为弱指针也许能解决问题,于是有了下面的代码

@interface TimerViewController ()
// 这里变为了weak
@property (nonatomic, weak) NSTimer *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 这里先添加到当前runloop再赋值给timer
    self.timer = timer;
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    [self.timer invalidate];
    NSLog(@"%s", __func__);
}

经过上面的改造,现在TimerViewController和timer之间有一个为弱指针,但是运行代码发现,内存泄漏的问题仍然没有得到解决。

因为这里虽然没有循环引用,但是RunLoop引用着timer,而timer又引用着TimerViewController,虽然pop时指向TimerViewController的强指针销毁,但是仍然有timer的强指针指向TimerViewController,因此仍然还是内存泄漏,如下图所示。


image.png
  1. 方案二
    既然从左边不行,那么试试从右边可不可以。

在这里加入了一个中间代理对象LJProxy,TimerViewController不直接持有timer,而是持有LJProxy实例,让LJProxy实例来弱引用TimerViewController,timer强引用LJProxy实例,直接看代码

@interface LJProxy : NSObject
+ (instancetype) proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJProxy
+ (instancetype) proxyWithTarget:(id)target
{
    LJProxy *proxy = [[LJProxy alloc] init];
    proxy.target = target;
    return proxy;
}

- (id)forwardingTargetForSelector:(SEL)aSelector
{
    return self.target;
}
@end

Controller里只修改了下面一句代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里的target发生了变化
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}
  • 首先当执行pop的时候,1号指针被销毁,现在就没有强指针再指向TimerViewController了,TimerViewController可以被正常销毁。
  • TimerViewController销毁,会走dealloc方法,在dealloc里调用了[self.timer invalidate],那么timer将从RunLoop中移除,3号指针会被销毁。
  • 当TimerViewController销毁了,对应它强引用的指针也会被销毁,那么2号指针也会被销毁。
  • 上面走完,timer已经没有被别的对象强引用,timer会销毁,LJProxy实例也就自动销毁了。


    image.png

这里需要注意的有两个地方:
1.- (id)forwardingTargetForSelector:(SEL)aSelector是什么?
了解iOS消息转发的朋友肯定知道这个东西,不了解的可以去这个博客看看
(https://www.jianshu.com/p/eac6ed137e06)。
简单来说就是如果当前对象没有实现这个方法,系统会到这个方法里来找实现对象。
本文中由于LJProxy没有实现timerRun方法(当然也不需要它实现),让系统去找target实例的方法实现,也就是去找TimerViewController中的方法实现。
2.timer的invalidate方法的具体作用参考苹果官方,这个方法会停止timer并将其从RunLoop中移除。
This method is the only way to remove a timer from an [NSRunLoop]object. The NSRunLoop object removes its strong reference to the timer, either just before the [invalidate] method returns or at some later point.

  1. 方案三
    经过上面的改造,似乎timer已经没有什么问题了,但是在浏览yykit源码的时候发现了一个以前一直没有注意过的类NSProxy,这是一个专门用于做消息转发的类,我们需要通过子类的方式来使用它。
    参考YYTextWeakProxy的写法,写了如下的代码
@interface LJWeakProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@property (weak, nonatomic) id target;
@end
@implementation LJWeakProxy
+ (instancetype)proxyWithTarget:(id)target {
    LJWeakProxy *proxy = [LJWeakProxy alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}

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

Controller里修改了如下代码

- (void)viewDidLoad {
    [super viewDidLoad];
    // 这里的target又发生了变化
    self.timer = [NSTimer timerWithTimeInterval:1.0 target:[LJWeakProxy proxyWithTarget:self] selector:@selector(timerRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

看上去这次的LJWeakProxy和前面的LJProxy似乎没有什么区别,好像LJWeakProxy还更复杂一些,但是还是稍微整理一下

  • LJProxy的父类为NSObject,LJWeakProxy的父类为NSProxy。
  • LJProxy只实现了forwardingTargetForSelector:方法,但是LJWeakProxy没有实现forwardingTargetForSelector:方法,而是实现了methodSignatureForSelector:和forwardInvocation:。

下面是NSProxy的Apple文档说明,简单来说提供了几个信息

  • NSProxy是一个专门用来做消息转发的类
  • NSProxy是个抽象类,使用需自己写一个子类继承自NSProxy
  • NSProxy的子类需要实现两个方法,就是上面那两个

NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation: and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forwardInvocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. methodSignatureForSelector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethodSignature object accordingly. See the NSDistantObject, NSInvocation, and NSMethodSignature class specifications for more information

说了那么多,到底NSProxy的好处在哪呢?
如果了解了OC中消息转发的机制,那么你肯定知道,当某个对象的方法找不到的时候,也就是最后抛出doesNotRecognizeSelector:的时候,它会经历几个步骤

  1. 消息发送,从方法缓存中找方法,找不到去方法列表中找,找到了将该方法加入方法缓存,还是找不到,去父类里重复前面的步骤,如果找到底都找不到那么进入
  2. 动态方法解析,看该类是否实现了resolveInstanceMethod:和resolveClassMethod:,如果实现了就解析动态添加的方法,并调用该方法,如果没有实现进入
  3. 消息转发,这里分二步
  • 调用forwardingTargetForSelector:,看返回的对象是否为nil,如果不为nil,调用objc_msgSend传入对象和SEL。
  • 如果上面为nil,那么就调用methodSignatureForSelector:返回方法签名,如果方法签名不为nil,调用forwardInvocation:来执行该方法

从上面可以看出,当继承自NSObject的对象,方法没有找到实现的时候,是需要经过第1步,第2步,第3步的操作才能抛出错误,如果在这个过程中我们做了补救措施,比如LJProxy就是在第3步的第1小步做了补救,那么就不会抛出doesNotRecognizeSelector:,程序就可以正常执行。
但是如果是继承自NSProxy的LJWeakProxy,就会跳过前面的所有步骤,直接到第3步的第2小步,直接找到对象,执行方法,提高了性能。

有人可能觉得那为什么不在第3步的第1小步来做补救呢?
但是很不巧,NSProxy只有methodSignatureForSelector:和forwardInvocation:这两个方法,官方的文档里也是让实现这两个方法。

  1. 方案四
    上面说了那么多,其实还有一种更加简单的方式来解决这里内存泄漏的问题,代码如下
    苹果意识到自己的问题,所以iOS10以后提供了Block的方式
@interface TimerViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation TimerViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        [weakSelf timerRun];
    }];
}

- (void)timerRun {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    [self.timer invalidate];
    NSLog(@"%s", __func__);
}

上面这种方式虽然TimerViewController强引用timer,但是timer并没有强引用TimerViewController(由于这里的weakSelf),因此不会出现内存泄漏的问题。

方案5:

-(void)viewWillDisappear:(BOOL)animated
 {
    [super viewWillDisappear:animated];
    [self.timer invalidate];
 }

这种情况是可以解决循环引用的问题,内存可以释放,但是又会引来新的问题,当导航控制器push到下一个页面时,当前VC并没有被释放,这时候我们可能并不想销毁NSTimer,我们通常希望VC该销毁的时候,同时销毁NSTimer,所以调用invalidate方法的时机很难找

方案6
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]; }
6: 为啥NSTimer定时不准确?你有啥替代方案不?

今天看到一个有意思的问题:NStimer准吗?如果不准该怎样实现一个精确的NSTimer?
既然这样问了,那从题目的角度出发,NSTimer肯定是不准的,但是它是以哪个精确度来作为“准”的标准呢,我们试着来探讨一下。

我们来写一段代码

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if (!_timer) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(logInfo) userInfo:nil repeats:YES];
    }
}

- (void)logInfo {
    NSLog(@"timer test");
}

好,跑一下

2017-11-10 09:12:32.566622+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:33.566811+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:34.566510+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:35.567532+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:36.567613+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:37.566615+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:38.567415+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:39.567650+0800 QTimer[20276:7878806] timer test
2017-11-10 09:12:40.566592+0800 QTimer[20276:7878806] timer test

可以看到,其计时偏差基本在1毫秒以内。
在正常的使用中,1毫秒以内偏差的计时可以说是非常准确了,如果题目并不是需要实现纳米级精度的计时,那么肯定是考虑到了其他影响到NSTimer计时精度的因素,我们试着来总结一下。

1、RunLoop的影响

为了验证,给计时任务加点重活

- (void)logInfo {
    int count = 0;
    for (int i = 0; i < 1000000; i++) {
        count += i;
    }
    NSLog(@"timer test");
}

先在模拟器上跑

2017-11-10 09:35:33.413804+0800 QTimer[20503:7955660] timer test
2017-11-10 09:35:34.413108+0800 QTimer[20503:7955660] timer test
2017-11-10 09:35:35.414460+0800 QTimer[20503:7955660] timer test
2017-11-10 09:35:36.414036+0800 QTimer[20503:7955660] timer test
2017-11-10 09:35:37.413990+0800 QTimer[20503:7955660] timer test
2017-11-10 09:35:38.413622+0800 QTimer[20503:7955660] timer test

可以看到计时偏差还是能控制在1毫秒以内

我们把上面的代码用真机(iPhone6)跑一下

2017-11-10 09:34:42.332293+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:43.324582+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:44.331287+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:45.333884+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:46.331684+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:47.334392+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:48.332235+0800 QTimer[9739:3329479] timer test
2017-11-10 09:34:49.333350+0800 QTimer[9739:3329479] timer test

可以看到计时偏差已经超过1毫秒了,说明CPU性能对定时器的精度影响是有的,但这是根本原因吗?

我们再加一点重活看看

- (void)logInfo {
    int count = 0;
    for (int i = 0; i < 1000000000; i++) {
        count += i;
    }
    NSLog(@"timer test");
}
2017-11-10 09:40:38.194879+0800 QTimer[9749:3330951] timer test
2017-11-10 09:40:44.188463+0800 QTimer[9749:3330951] timer test
2017-11-10 09:40:50.172012+0800 QTimer[9749:3330951] timer test
2017-11-10 09:40:56.172139+0800 QTimer[9749:3330951] timer test
2017-11-10 09:41:02.179022+0800 QTimer[9749:3330951] timer test
2017-11-10 09:41:08.170254+0800 QTimer[9749:3330951] timer test
2017-11-10 09:41:14.169011+0800 QTimer[9749:3330951] timer test

my god ... 计时偏差已经去到了6秒之多,显然是有问题的

原因分析:

定时器被添加在主线程中,由于定时器在一个RunLoop中被检测一次,所以如果在这一次的RunLoop中做了耗时的操作,当前RunLoop持续的时间超过了定时器的间隔时间,那么下一次定时就被延后了。
解决方法:
1、在子线程中创建timer,在主线程进行定时任务的操作
2、在子线程中创建timer,在子线程中进行定时任务的操作,需要UI操作时切换回主线程进行操作

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    if (!_timer) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"timer test");
        }];
    }
}

- (void)logInfo {
    int count = 0;
    for (int i = 0; i < 1000000000; i++) {
        count += i;
    }
    NSLog(@"timer test");
}
2017-11-10 09:52:57.725870+0800 QTimer[9759:3334283] timer test
2017-11-10 09:52:58.725829+0800 QTimer[9759:3334283] timer test
2017-11-10 09:52:59.725822+0800 QTimer[9759:3334283] timer test
2017-11-10 09:53:00.725979+0800 QTimer[9759:3334283] timer test
2017-11-10 09:53:01.725827+0800 QTimer[9759:3334283] timer test
2017-11-10 09:53:02.725774+0800 QTimer[9759:3334283] timer test
2017-11-10 09:53:03.725831+0800 QTimer[9759:3334283] timer test
2、RunLoop模式的影响

为了验证,我们在当前页面上添加一个tableview,在定时器运行时,我们对tableview进行滑动操作,可以发现,定时器并不会触发下一次的定时任务。

原因分析:

主线程的RunLoop有两种预设的模式,RunLoopDefaultMode和TrackingRunLoopMode。
当定时器被添加到主线程中且无指定模式时,会被默认添加到DefaultMode中,一般情况下定时器会正常触发定时任务。但是当用户进行UI交互操作时(比如滑动tableview),主线程会切换到TrackingRunLoopMode,在此模式下定时器并不会被触发。

解决方法:

添加定时器到主线程的CommonMode中或者子线程中

[[NSRunLoop mainRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];

其他方式的Timer

1、纳秒级精度的Timer

使用mach_absolute_time()来实现更高精度的定时器。
iPhone上有这么一个均匀变化的东西来提供给我们作为时间参考,就是CPU的时钟周期数(ticks)。
通过mach_absolute_time()获取CPU已运行的tick数量。将tick数经过转换变成秒或者纳秒,从而实现时间的计算。
以下代码实现来源于网络:

#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);
}

理论上这是iPhone上最精准的定时器,可以达到纳秒级别的精度,但是怎样去验证呢?
由于日志的输出需要消耗时间,CPU线程之间的调度也需要消耗时间,所以无法从Log中输出的系统时间来验证其更高的精度,根据我测试的系统时间来看,时间偏差也是在1毫秒以内

2、CADisplayLink

CADisplayLink是一个频率能达到屏幕刷新率的定时器类。iPhone屏幕刷新频率为60帧/秒,也就是说最小间隔可以达到1/60s。
基本使用:

CADisplayLink * displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(logInfo)];
[displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

3、GCD定时器

我们知道,RunLoop是dispatch_source_t实现的timer,所以理论上来说,GCD定时器的精度比NSTimer只高不低。
基本使用:

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);

总结

从结果看,NSTimer在其使用场景下足够准了,对于“不准”更多是集中在对其错误的使用方式上,只要我们足够深入了解,正确地使用,就能让它“准”。

实际上,苹果也不推荐使用太高精度的定时器,对于NSTimer,精度在50-100ms都是正常的,如果我们需要足够高精度地进行计时,比如统计APP启动时间、一段任务代码的运行时间等等,NSTimer不是一个好的选择,mach_absolute_time()或者可以帮到你,苹果开发工具也带有更专业的API或者插件提供给开发者。

7: 为啥在子线程使用NSTimer定时不起效?怎么解决?

无外乎就这种原因
1: 没有添加到RunLoop中,并设置runloop modes

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,670评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,928评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,926评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,238评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,112评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,138评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,545评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,232评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,496评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,596评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,369评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,226评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,600评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,906评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,185评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,516评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,721评论 2 335