NSTimer在iOS10之前只有如下两个常用方法
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
这两个方法都有一个target
参数,一般我们都把NSTimer对象作为一个属性,这个时候target
通常传递的是NSTimer属性所在的类对象,也就是Self
,由于NSTimer会强引用这个target
对象,所以导致出现一些问题。
销毁NSTimer计时器只能通过- (void)invalidate
方法,一般的需求下,我们期望计时器在视图的声明周期结束后停止计时器,所以我们可能想在视图类的- (void)dealloc
方法中调用[self.time invalidate]
来停止计时器。但是很遗憾,在视图消失的时候程序并不会调用- (void)dealloc
方法,所以计时器也一直在运行,造成了内存泄露。
为什么视图对象的- (void)dealloc
不会被调用,我们可以通过查看GNUstep的源码来看看。
Cocoa框架的源码是不开源的,但是GNUstep是Cocoa的互换框架,也就是说,GNUstep的源代码虽不能说与苹果的Cocoa实现完全相同,但是从使用者的角度上来看,两者的行为是一样的,或者说非常相似。理解了GNUstep源代码就相当于理解了苹果的Cocoa实现。
摘取自《Objective-C高级编程 iOS与OS X多线程和内存管理》
我们知道iOS的内存管理是采用引用计数算法来实现的,当一个对象的引用计数为0的时候系统会回收该对象,要让一个对象的引用计数为0,那么就只能通过- (void)release
方法,GNUstep中是这样实现的:
@implementation NSObject.m
- (void)release{
if(NSDecrementExtraRefCountWasZero(self)){
[self dealloc];
}
}
BOOL NSDecrementExtraRefCountWasZero(id anObject){
//获取对象引用计数减去1之后的值
int result = GSAtomicDecrement((gsatomic_t)&(((obj)anObject)[-1].retained));
if (result <= 0) {
return YES;
}else{
((obj)anObject)[-1].retained--;
return NO;
}
}
在上面的- (void)release
方法的实现代码中,我们可以得出结论,对象的- (void)dealloc
方法要在对象的引用计数值归零之后才会被调用,由于NSTimer计时器一直强引用着视图类对象,所以视图类对象的引用关系里面一直有计时器对象,导致引用计数不能归零,所以- (void)dealloc
不会被调用。
至于解决办法,那就是在不需要计时器的时候调用- (void)invalidate
方法,但是如果我们计时器的开关是交由外部程序员去实现的,那么我们就只能在文档中嘱咐对方在不需要的时候一定要记得停止计时器,这并不保险。
还有一种办法,那就是不要让计时器强引用Self
,或者说提供给计时器一个weakSelf
,苹果在iOS10之后便是这样解决,所以在iOS10的API中我们可以看到多出了如下两个方法。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block
在iOS10以下我们使用这种思想,用分类的方式解决这个问题,代码如下
#import <Foundation/Foundation.h>
@interface NSTimer (BP_BlockSupport)
+(NSTimer *)bp_timerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
block:(void(^)())block;
+(NSTimer *)bp_scheduledTimerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
block:(void(^)())block;
@end
#import "NSTimer+BP_BlockSupport.h"
@implementation NSTimer (BP_BlockSupport)
+(NSTimer *)bp_timerWithTimeInterval:(NSTimeInterval)ti
repeats:(BOOL)yesOrNo
block:(void(^)())block{
return [self timerWithTimeInterval:ti target:self selector:@selector(bp_blockInvoke:) userInfo:[block copy] repeats:yesOrNo];
}
+(NSTimer *)bp_scheduledTimerWithTimeInterval:(NSTimeInterval)ti repeats:(BOOL)yesOrNo block:(void (^)())block{
return [self scheduledTimerWithTimeInterval:ti target:self selector:@selector(bp_blockInvoke:) userInfo:[block copy] repeats:yesOrNo];
}
+ (void)bp_blockInvoke:(NSTimer *)timer{
void(^block)() = timer.userInfo;
if (block) {
block();
}
}
@end
使用的时候如下即可。
-(NSTimer *)timer{
if (!_timer) {
__weak typeof(self) weakSelf = self;
_timer = [NSTimer bp_timerWithTimeInterval:kAutoLocRequsetTimeInterval repeats:YES block:^{
[weakSelf p_startAutoLoc];
}];
}
return _timer;
}
/*在界面消失的时候停止计时器*/
-(void)dealloc{
if (_timer) {
[self.timer invalidate];
self.timer = nil;
}
}
在-(void)deallc
方法中,判断计时器是否存在需要用_timer
,而不能使用self.timer
,因为我们使用的是懒加载,当视图对象在销毁的时候计时器还没有创建的话,使用self.timer
访问会创建一个新的计时器,这是没有必要的,而且这样的话就相当于我们在-(void)dealloc
方法中执行__weak typeof(self) weakSelf = self;
这段代码,在运行的时候程序会崩溃,并且提示It is possible that this object was over-released, or is in the process of deallocation.
至于分类对象方法为什么要用前缀_方法名
开头,是为了避免和他人写的代码重复,很凑巧,iOS10的NSTimer带block参数的两个类方法名字和我的方法名字去掉前缀是一样的,如果我当初没有加上前缀,那么支持iOS10之后我的方法将会覆盖苹果的方法,而我自己还不知道,幸好我们的实现内容是一致的,不然可能会出现一些莫名其妙的问题,所以建议分类方法要加前缀!