NSTimer和实现弱引用的timer的方式

我们常用NSTimer的方式

如下代码所示,是我们最常见的使用timer的方式

@property (nonatomic , strong) NSTimer *animationTimer;
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:(self.animationDuration = animationDuration)
                                                               target:self
                                                             selector:@selector(animationTimerDidFired:)
                                                             userInfo:nil
                                                              repeats:YES];

当使用NSTimer的scheduledTimerWithTimeInterval方法时。事实上此时Timer会被加入到当前线程的Run Loop中,且模式是默认的NSDefaultRunLoopMode。而如果当前线程就是主线程,也就是UI线程时,某些UI事件,比如UIScrollView的拖动操作,会将Run Loop切换成NSEventTrackingRunLoopMode模式,在这个过程中,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。也就是说,此时使用scheduledTimerWithTimeInterval添加到Run Loop中的Timer就不会执行
我们可以通过添加一个UICollectionView,然后滑动它后打印定时器方法
<pre>
2016-01-27 11:41:59.770 TimerAbout[89719:1419729] enter timer
2016-01-27 11:42:00.339 TimerAbout[89719:1419729] enter timer
2016-01-27 11:42:01.338 TimerAbout[89719:1419729] enter timer
2016-01-27 11:42:02.338 TimerAbout[89719:1419729] enter timer
2016-01-27 11:42:03.338 TimerAbout[89719:1419729] enter timer
2016-01-27 11:42:15.150 TimerAbout[89719:1419729] enter timer
2016-01-27 11:42:15.338 TimerAbout[89719:1419729] enter timer
</pre>

从中可以看到,当UICollectionView滑动时候,定时器方法并没有打印(从03.338到15.150)

为了设置一个不被UI干扰的Timer,我们需要手动创建一个Timer,然后使用NSRunLoop的addTimer:forMode:方法来把Timer按照指定模式加入到Run Loop中。这里使用的模式是:NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的结合,官方参考文档

还是上面的例子,换为

self.animationTimer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.animationTimer forMode:NSRunLoopCommonModes];

则,无论你滑动不滑动UICollectionView,定时器都是起作用的!!


上面的NSTimer无论采用何种方式,都是在主线程上跑的,那么怎么在非主线程中跑一个NSTimer呢?

我们简单的可以使用如下代码

//创建并执行新的线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(newThread) object:nil];
    [thread start];

- (void)newThread
{
    @autoreleasepool
    {
        //在当前Run Loop中添加timer,模式是默认的NSDefaultRunLoopMode
        [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(animationTimerDidFired:) userInfo:nil repeats:YES];
        //开始执行新线程的Run Loop
        [[NSRunLoop currentRunLoop] run];
    }
}

当然了,因为是开启的新的线程,在定时器的回调方法中,需要切换到主线程才能操作UI额

GCD的方式

//GCD方式
    uint64_t interval = 1 * NSEC_PER_SEC;
    //创建一个专门执行timer回调的GCD队列
    dispatch_queue_t queue = dispatch_queue_create("timerQueue", 0);
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //使用dispatch_source_set_timer函数设置timer参数
    dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), interval, 0);
    //设置回调
    dispatch_source_set_event_handler(_timer, ^(){
        NSLog(@"Timer %@", [NSThread currentThread]);
    });
    dispatch_resume(_timer);//dispatch_source默认是Suspended状态,通过dispatch_resume函数开始它

其中的dispatch_source_set_timer的最后一个参数,是最后一个参数(leeway),他告诉系统我们需要计时器触发的精准程度。所有的计时器都不会保证100%精准,这个参数用来告诉系统你希望系统保证精准的努力程度。如果你希望一个计时器每5秒触发一次,并且越准越好,那么你传递0为参数。另外,如果是一个周期性任务,比如检查email,那么你会希望每10分钟检查一次,但是不用那么精准。所以你可以传入60,告诉系统60秒的误差是可接受的。他的意义在于降低资源消耗。


一次性的timer方式的GCD模式

 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_after enter timer");
    });

另一种dispatch_after方式的定时器

这个是使用上面的dispatch_after来创建的,通过递归调用来实现

- (void)dispatechAfterStyle {
    __weak typeof (self) wself = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"dispatch_after enter timer,thread = %@", [NSThread currentThread]);
        [wself dispatechAfterStyle];
    });
}

利用GCD的弱引用型的timer

MSWeaker 实现了一个利用GCD的弱引用的timer
原理是利用一个新的对象,在这个对象中

NSString *privateQueueName = [NSString stringWithFormat:@"com.mindsnacks.msweaktimer.%p", self];
        self.privateSerialQueue = dispatch_queue_create([privateQueueName cStringUsingEncoding:NSASCIIStringEncoding], DISPATCH_QUEUE_SERIAL);
        dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue);

        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER,
                                            0,
                                            0,
                                            self.privateSerialQueue);

- (void)resetTimerProperties
{
    int64_t intervalInNanoseconds = (int64_t)(self.timeInterval * NSEC_PER_SEC);
    int64_t toleranceInNanoseconds = (int64_t)(self.tolerance * NSEC_PER_SEC);

    dispatch_source_set_timer(self.timer,
                              dispatch_time(DISPATCH_TIME_NOW, intervalInNanoseconds),
                              (uint64_t)intervalInNanoseconds,
                              toleranceInNanoseconds
                              );
}

- (void)schedule
{
    [self resetTimerProperties];

    __weak MSWeakTimer *weakSelf = self;

    dispatch_source_set_event_handler(self.timer, ^{
        [weakSelf timerFired];
    });

    dispatch_resume(self.timer);
}

创建了一个队列self.timer = dispatch_source_create,然后在这个队列中创建timer dispatch_source_set_timer
注意其中用到了dispatch_set_target_queue(self.privateSerialQueue, dispatchQueue); 这个是将dispatch队列的执行操作放到队列dispatchQueue 中去

这份代码中还用到了原子操作!!!值得好好研读,以便以后可以在自己的多线程设计中使用原子操作
为什么用原子操作呢,因为作者想的是在多线程的环境下设置定时器的开关与否

if (OSAtomicAnd32OrigBarrier(1, &_timerFlags.timerIsInvalidated))

if (!OSAtomicTestAndSet(7, &_timerFlags.timerIsInvalidated))
    {
        dispatch_source_t timer = self.timer;
        dispatch_async(self.privateSerialQueue, ^{
            dispatch_source_cancel(timer);
            ms_release_gcd_object(timer);
        });
    }

至于其中

 struct
    {
        uint32_t timerIsInvalidated;
    } _timerFlags;   

这里为什么要用结构体呢?为什么不直接使用一个uint32_t 的变量???


使用NSTimer方式创建的Timer,使用时候需要注意

由于

self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:1
                                                           target:self
                                                         selector:@selector(animationTimerDidFired:)
                                                         userInfo:nil
                                                          repeats:YES];

会导致timer 强引用 self,而animationTimer又是self的一个强引用,这造成了强引用的循环了
如果不手工停止timer,那么self这个VC将不能够被释放,尤其是当我们这个VC是push进来的时候,pop将不会被释放!!!
怎么解决呢??
当然了,可以采用上文提到的MSWeakerGCD的弱引用的timer

可是如果有时候,我们不想使用它,觉得它有点复杂呢?

  1. 在VC的disappear方法中应该调用 invalidate方法,将定时器释放掉,这里可能有人要说了,我直接在vc的dealloc中释放不行么

-(void)dealloc {
[_animationTimer invalidate];
}
```
很遗憾的告诉你,都已经循环引用了,vc压根就释放不了,怎么调dealloc方法!!

  1. 在vc的disappear方法中

-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[_animationTimer invalidate];
}
```
这样的确能解决问题,可是不一定是我们想要的呀,当我们vc 再push了一个新的页面的时候,本身vc没有释放,按理说,其成员timer不应该被释放呀,你可能会说,那还不容易,在appear方法中再重新生成一下呗....但是这样的话,又要增加一个变量,标识定时器在上一次disappear时候是不是启动了吧,是启动了,被invaliate的时候,才能在appear中重新启动吧.这样,是不是觉得很麻烦!!

  1. 你可能会说,那简单啊,直接若引用就可以了想想我们使用block的时候

@property (nonatomic, copy) void (^ myblock)(NSInteger i);
__weak typeof (self) weakSelf = self;
self.myblock = ^(NSInteger i){
[weakSelf view];
};
```
在其中,我们需要在block中引用self,如果直接引用,也是循环引用了,采用先定义一个weak变量,然后在block中引用weak对象,避免循环引用 你会直接想到如下的方式

```Objective-C

__weak typeof (self) wself = self;
self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:wself
selector:@selector(animationTimerDidFired:)
userInfo:nil
repeats:YES];
是不是瞬间觉得完美了,呵呵,我只能说少年,你没理解两者之间的区别.在block中,block是对变量进行捕获,意思是对使用到的**变量**进行拷贝操作,注意是拷贝的不是对象,而是变量自身,拿上面的来说,block中只是对变量wself拷贝了一份,也就是说,block中也定义了一个weak对象,相当于,在block的内存区域中,定义了一个__weak blockWeak对象,然后执行了blockWeak = wself;注意到了没,这里并没有引起对象的持有量的变化,所以没有问题,再看timer的方式,虽然你是将wself传入了timer的构造方法中,我们可以查看NSTimer的Objective-C

  • (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds target:(id)target selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)repeats

定义,其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,是要强应用这个变量的 也就是说,大概是这样的, __strong strongSelf = wself 强引用了一个弱应用的变量,结果还是强引用,也就是说strongSelf持有了wself所指向的对象(也即是self所只有的对象),这和你直接传self进来是一样的效果,并不能达到解除强引用的作用!! 看来只能换个思路了,我直接生成一个临时对象,让Timer强用用这个临时对象,在这个临时对象中弱引用self,可以了吧.

  1. 考虑引入一个对象,在这个对象中弱引用self,然后将这个对象传递给timer的构建方法 这里可以参考YYWeakProxy建立这个对象
@interface YYWeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
-(instancetype)initWithTarget:(id)target;
+(instancetype)proxyWithTarget:(id)target;
@end
-(instancetype)initWithTarget:(id)target {
  _target = target;
  return self;
}
+(instancetype)proxyWithTarget:(id)target {
  return [[YYWeakProxy alloc] initWithTarget:target];
}
//当不能识别方法时候,就会调用这个方法,在这个方法中,我们可以将不能识别的传递给其它对象处理
//由于这里对所有的不能处理的都传递给_target了,所以methodSignatureForSelector和forwardInvocation不可能被执行的,所以不用再重载了吧
//其实还是需要重载methodSignatureForSelector和forwardInvocation的,为什么呢?因为_target是弱引用的,所以当_target可能释放了,当它被释放了的情况下,那么forwardingTargetForSelector就是返回nil了.然后methodSignatureForSelector和forwardInvocation没实现的话,就直接crash了!!!
//这也是为什么这两个方法中随便写的!!!
-(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

使用的时候,将原来的替换为

```Objective-C

self.animationTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:[YYWeakProxy proxyWithTarget:self ]
selector:@selector(animationTimerDidFired:)
userInfo:nil
repeats:YES];
```

  1. block方式来解决循环引用
@interface NSTimer (XXBlocksSupport)
+(NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       block:(void(^)())block
                                     repeats:(BOOL)repeats;
@end
@implementation NSTimer (XXBlocksSupport)
+(NSTimer *)xx_scheduledTimerWithTimeInterval:(NSTimeInterval)interval
                                       block:(void(^)())block
                                     repeats:(BOOL)repeats
{
  return [self scheduledTimerWithTimeInterval:interval
                                        target:self
                                      selector:@selector(xx_blockInvoke:)
                                      userInfo:[block copy]
                                       repeats:repeats];
}
+(void)xx_blockInvoke:(NSTimer *)timer {
  void (^block)() = timer.userinfo;
  if(block) {
      block();
  }
}
@end
  ```
注意以上NSTimer的target是NSTimer类对象,类对象本身是个单利,此处虽然也是循环引用,但是由于类对象不需要回收,所以没有问题.但是这种方式要注意block的间接循环引用,当然了,解决block的间接循环引用很简单,定义一个weak变量,在block中使用weak变量即可




---

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

推荐阅读更多精彩内容

  • iOS中计时器常用的有两种方式 使用NSTimer类(Swift 中更名为 Timer) NSTimer 常用的初...
    superDg阅读 1,836评论 0 1
  • 说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:《招聘一个靠谱的 iOS》,其中共55题,除第一题为纠错...
    __Lex阅读 682评论 0 4
  • 一、深复制和浅复制的区别? 1、浅复制:只是复制了指向对象的指针,即两个指针指向同一块内存单元!而不复制指向对象的...
    iOS_Alex阅读 1,364评论 1 27
  • 《招聘一个靠谱的 iOS》—参考答案(下) 说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:《招聘一个靠...
    YuWenHaiBo阅读 4,043评论 0 16
  • 《招聘一个靠谱的 iOS》—参考答案(下) 说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:《招聘一个靠...
    Mominglaile阅读 1,226评论 0 1