iniOS前阵子在整理公司项目的时候,发现老代码在使用NSTimer的时候有内存泄漏的问题。然后整理了一些NSTimer相关的内容。比较简单,开个玩笑。NSTimerfire我们先用NSTimer做一个简单的定时器,每5秒在控制台输出一次Fire。更理所当然的方法是:@interfaceDetailViewController()@property(nonatomic,weak)NSTimer*timer;@end@implementationDetailViewController-(IBAction)fireButtonPressed:(id)sender{_timer=[NSTimerscheduledTimerWithTimeInterval:3.0ftarget:selfselector:@selector(timerFire:)userInfo:nilrepeats:YES];[_timerfire];}-(void)timerFire:(id)userinfo{NSLog(@"Fire");}@end之后每3秒在控制台输出一次运行了一次Fire,但是当我们从这个界面跳转到其他界面时,发现控制台还在不断的输出Fire。似乎定时器没有停止。由于invalidate还没有停止,我们在DemoViewController的dealloc中添加invalidate方法:-(void)dealloc{[_timerinvalidate];NSLog(@"%@dealloc",NSStringFromClass([selfclass]));}再次运行,还是没有停止。原因是Timer添加到Runloop的时候,会被Runloop强引用:Noteinparticularthatrunloopsmaintainstrongreferencestotheirtimers,soyoudon'thavetomaintainyourownstrongreferencetoatimerafteryouhaveaddedittoarunloop.然后Timer又会有一个对Target的强引用(也就是self):TargetistheobjecttowhichtosendthemessagespecifiedbyaSelectorwhenthetimerfires.Thetimermaintainsastrongreferencetotargetuntilit(thetimer)isinvalidated.也就是说NSTimerself是强引用的,所以self不能一直释放,所以不能去到self的dealloc。在这种情况下,我们可以添加一个无效按钮:-(IBAction)invalidateButtonPressed:(id)sender{[_timerinvalidate];}好了,就是这样。(SOF上有人说_timer=nil应该在invalidate之后执行,但是没看懂为什么,知道原因的可以告诉我:)在invalidate方法的文档中有这么一段:Youmustsend此消息来自安装了计时器的线程。.NSTimer必须在创建它的线程上停止,否则资源将无法正确释放。看来坑还是很多的。dealloc那么问题来了:如果我只是想让这个NSTimer一直输出到DemoViewController被销毁,我该如何让它停止呢?NSTimer被Runloop强引用,要释放必须调用invalidate方法。但是我想在DemoViewController的dealloc中调用invalidate方法,self却被NSTimer强引用了。所以我仍然必须先释放NSTimer,但是如果不调用invalidate方法就无法释放它。但是,如果你不进入dealloc方法,我就无法调用invalidate方法。嗯...HWWeakTimerweakSelf问题的关键在于self被NSTimer强引用了。如果能破解这个强引用问题,自然就迎刃而解了。所以一个非常简单的想法是:然而,这是没有用的,这里__weak和__strong***的区别是:如果在这两行代码执行过程中释放了self,那么NSTimer的target就会变成nil。由于目标无法通过__weak提取自身,我们可以为NSTimer创建一个假目标。这个faketarget类似于一个中间代理,它唯一的工作就是挺身而出拿NSTimer的强引用。类声明如下:@interfaceHWWeakTimerTarget:NSObject@property(nonatomic,weak)idtarget;@property(nonatomic,assign)SELselector;@property(nonatomic,weak)NSTimer*timer;@end@implementationHWWeakTimerTarget-(void)fire:(NSTimer*)timer{if(self.target){[self.targetperformSelector:self.selectorwithObject:timer.userInfo];}else{[self.timerinvalidate];}}@end然后我们封装一个假的scheduledTimerWithTimeInterval方法,但是当calling已经改了:+(NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)intervaltarget:(id)aTargetselector:(SEL)aSelectoruserInfo:(id)userInforepeats:(BOOL)repeats{HWWeakTimerTarget*timerTarget=[[HWWeakTimerTargetalloc]init];timerTarget.target=aTarget;timerTarget.selector=aSelector;timerTarget.timer=[NSTimerscheduledTimerWithTimeInterval:intervaltarget:timerTargetselector:@selector(fire:)userInfo:userInforepeats:repeats];returntimerTarget.timer;}再次运行,问题解决。如果block能用block来调用NSTimer岂不是更好?我们可以这样做:+(NSTimer*)scheduledTimerWithTimeInterval:(NSTimeInterval)intervalblock:(HWTimerHandler)blockuserInfo:(id)userInforepeats:(BOOL)repeats{return[selfscheduledTimerWithTimeInterval:intervaltarget:selfselector:@selector(_timerBlockInvoke:)userInfo:@[[blockcopy],userInfo]repeats:repeats];}+(void)_timerBlockInvoke:(NSArray*)userInfo{HWTimerHandlerblock=userInfo[0];idinfo=userInfo[1];//或`!block?:block();`@sunnyxxif(block){block(info);}}这样我们就可以直接在block里面写相关的逻辑了:-(IBAction)fireButtonPressed:(id)sender{_timer=[HWWeakTimerscheduledTimerWithTimeInterval:3.0fblock:^(iduserInfo){NSLog(@"%@",userInfo);}userInfo:@"Fire"repeats:YES];[_timerfire];}就是这样。更简单的将以上代码封装成HWWeakTimer,欢迎尝试。
