一、NSTimer 使用
1.1 使用问题
static int num = 0;
@property (nonatomic, strong) NSTimer *timer;
self.timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
//立即触发一次
[self.timer fire];
- (void)timerAction {
num++;
NSLog(@"timerAction: %d",num);
}
- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}
对于上面使用timerWithTimeInterval
方式创建的timer
需要主动调用NSRunLoop
的addTimer
触发计时器,当然也可以通过scheduledTimerWithTimeInterval
方式来创建:
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
区别是不需要再调用NSRunLoop
的触发方法,scheduledTimerWithTimeInterval
内部会主动调用,汇编还原源码如下:
/* @class NSTimer */
+(int)scheduledTimerWithTimeInterval:(int)arg2 target:(int)arg3 selector:(int)arg4 userInfo:(int)arg5 repeats:(id)arg6 {
CFRunLoopAddTimer(CFRunLoopGetCurrent(), [[objc_allocWithZone(arg0, arg1, arg2, arg3, arg4, arg5) initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:r2] interval:arg2 target:arg3 selector:arg4 userInfo:arg5 repeats:r7] autorelease], **_kCFRunLoopDefaultMode);
r0 = r19;
return r0;
}
/* @class NSTimer */
+(int)timerWithTimeInterval:(int)arg2 target:(int)arg3 selector:(int)arg4 userInfo:(int)arg5 repeats:(id)arg6 {
r0 = [objc_allocWithZone() initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:r2] interval:arg2 target:arg3 selector:arg4 userInfo:arg5 repeats:r7];
r0 = [r0 autorelease];
return r0;
}
void -[NSCFTimer invalidate]() {
CFRunLoopTimerInvalidate(r0);
return;
}
以上timer
在使用时候的坑点是会造成循环引用,不会走dealloc
。在这个过程中self -> timer -> self
。我们都清楚timer
和self
弱引用并不能解决问题,在这个过程中是runloop
持有了timer
,timer
持有了self
。而即使self
使用__weak
修饰赋值给timer
的target
并不能解决问题是因为timer
持有target
是强持有,__weak
能解决block
的循环引用是因为block
捕获的是__weak
类型变量。
官方文档说明了
target
被timer
强持有:
在invalidate
的时候timer
将不再持有target
。
1.2 问题处理
既然invalidate
调用后timer
不再持有target
,那么就在合适的时机调用就可以了。
1.2.1 viewWillDisappear 中处理
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
viewWillDisappear
中有个问题是退出页面和进入下一个页面都会调用这个API
,如果只在退出页面的情况下要调用则需要做标记相关的处理。并且还有滑动返回等相关手势操作。
1.2.2 didMoveToParentViewController
更好的处理方式是在didMoveToParentViewController
中进行相关逻辑处理:
- (void)didMoveToParentViewController:(UIViewController *)parent {
if (parent == nil) {
[self.timer invalidate];
self.timer = nil;
}
}
以上在业务层面处理循环引用并不优雅,并且随着业务逻辑复杂度的增加处理成本也越来越高。
二、block 方式
在iOS10
以后系统提供了block
形式的API
供我们调用,这种形式timer
不会持有target
:
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
num++;
NSLog(@"timerAction: %d",num);
}];
需要在dealloc
中调用invalidate
。对于iOS10
之前的版本可以自己实现一个,给NSTimer
添加一个分类:
+ (NSTimer *)hp_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats
{
return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(hp_blockInvoke:) userInfo:[block copy] repeats:repeats];
}
+ (void)hp_blockInvoke:(NSTimer *)timer
{
void (^block)(void) = timer.userInfo;
if (block) {
block();
}
}
在这里将block
的实现传递给了userInfo
,在selector
中取到了block
进行了调用。target
传递的是NSTimer
类对象。这样self
和timer
就没有互相持有了。
调用:
self.timer = [NSTimer hp_scheduledTimerWithTimeInterval:3.0 block:^{
num++;
NSLog(@"timerAction: %d",num);
} repeats:YES];
同样的需要在dealloc
中调用invalidate
。
三、临时 Target
@property (nonatomic, strong) id target;
self.target = [[NSObject alloc] init];
class_addMethod([NSObject class], @selector(timerAction1), (IMP)timerActionFunc, "v@:");
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self.target selector:@selector(timerAction1) userInfo:nil repeats:YES];
void timerActionFunc(){
num++;
NSLog(@"timerActionFunc: %d",num);
}
既然是由于target
造成的循环引用,那么只要target
不是self
就可以解决循环引用了,使用NSObject
作为临时target
传递给timer
。NSObject
添加一个timerActionFunc
,最终timer
会调用到timerActionFunc
。timerAction1
是一个不存在的方法。同样需要主动调用invalidate
。
四、NSProxy 转发
@property (nonatomic, weak) id object;
+ (instancetype)proxyWithTransformObject:(id)object {
HPProxy *proxy = [HPProxy alloc];
proxy.object = object;
return proxy;
}
//方式一:备用消息接收者(消息快速转发)
//为了保证中间件能够响应外部self的事件,需要通过消息转发机制,让实际的响应target还是外部self。
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self.object;
}
////方式二:慢速消息转发
//- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
//
// if (!self.object) {
// NSLog(@"error");
// }
// return [self.object methodSignatureForSelector:sel];
//}
//
//- (void)forwardInvocation:(NSInvocation *)invocation {
//
// if (self.object) {
// [invocation invokeWithTarget:self.object];
// } else {
// NSLog(@"error");
// }
//}
在消息转发过程中,可以将消息的响应者调用回target
。这样就避免了互相持有(object
在这里是弱引用)。
调用:
@property (nonatomic, strong) HPProxy *proxy;
self.proxy = [HPProxy proxyWithTransformObject:self];
self.timer = [NSTimer scheduledTimerWithTimeInterval:3.0 target:self.proxy selector:@selector(timerAction) userInfo:nil repeats:YES];
同样的需要在dealloc
中调用invalidate
后target
就不被runloop
间接持有了。
五、Wapper 包装 NSTimer
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL aSelector;
@property (nonatomic, strong) NSTimer *timer;
- (instancetype)hp_initWithTimeInterval:(NSTimeInterval)timeInterval target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo {
if (self == [super init]) {
self.target = aTarget; // vc
self.aSelector = aSelector; // 方法 -- vc 释放
if ([self.target respondsToSelector:self.aSelector]) {
//本类添加 aSelector
Method method = class_getInstanceMethod([self.target class], aSelector);
const char *type = method_getTypeEncoding(method);
class_addMethod([self class], aSelector, (IMP)timeActionWapper, type);
//aSelector 是传递进来的。
self.timer = [NSTimer scheduledTimerWithTimeInterval:timeInterval target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
}
}
return self;
}
//提供给外界主动调用 timer 释放
- (void)hp_invalidate {
[self.timer invalidate];
self.timer = nil;
}
void timeActionWapper(HPTimerWrapper *warpper) {
//交给 target 自己处理 aSelector
if (warpper.target) {
void (*hp_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
hp_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
} else { // warpper target 不存在,直接释放 timer
[warpper hp_invalidate];
}
}
将NSTimer
包组成了HPTimerWrapper
,self
只与HPTimerWrapper
进行交互。
这样设计完全隔离了self
与timer
,timer
与wapper
构成循环引用,这里循环引用解除依赖target
。当主动调用hp_invalidate
解除或者target
(也就是vc
)消失后就主动解除了。
调用:
@property (nonatomic, strong) HPTimerWrapper *timerWapper;
self.timerWapper = [[HPTimerWrapper alloc] hp_initWithTimeInterval:1 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
这样做的好处是不需要在dealloc
中调用hp_invalidate
,在timeActionWapper
中会自动调用。
总结:
NSTimer
循环引用问题是由于runloop -> timer -> self -> timer
造成的,只要打破互相持有就能解决循环引用问题。
主要有3
种方式处理:
- 1.业务层处理循环引用
- 1.1
viewWillDisappear
中调用invalidate
。 - 1.2
didMoveToParentViewController
中调用invalidate
。
- 1.1
- 2.中介者处理
- 2.1 封装
block
形式,NSTimer
作为了中介者。iOS10
以后系统提供了block
形式的API
。 - 2.2 临时
target
形式,NSObject
动态添加方法,NSTimer
设置target
为NSObject
。 - 2.3
NSProxy
作为NSTimer
的target
通过消息快速/慢速转发回调用发。 - 2.4
warpper
封装NSTimer
让NSTimer
与self
彻底隔离,self
作为target
,warpper
中调用target
中的方法。相比其它方式,warpper
有自动调用invalidate
的逻辑,不需要在dealloc
中主动调用。
- 2.1 封装
- 3.
GCD
封装Timer
,具体可以查看GCD底层分析(三)4.3 章节
TimerDemo
HPTimer