NSTimer解决循环引用的几种方式

NSTimer

NSTimer是日常常用控件,但在使用中需要注意避免循环引用的问题。NSTimer使用方式如下

1
2
3
4
5
6
7
8
- (void)initTimer {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(beginTimer) userInfo:nil repeats:YES];
}

- (void)dealloc {
[self.timer invalidate];
self.timer = nil;
}

由于timer与self相互强引用,导致走不到dealloc方法,造成内存泄露。
以下为解决NSTimer循环引用的几种方法。

选择合适时机手动释放

1
2
3
4
5
6
-(void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
_textLabel.text = @"请按住说话";
[_timer invalidate];
}

这种方案在有些情况下是不合理的,比如控制器push到下一个页面,返回时需要重新实例化timer。

在NSTimer分类中添加类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@implementation NSTimer (BlockTimer)

+ (NSTimer *)bl_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)(void))block repeats:(BOOL)repeats {

return [self scheduledTimerWithTimeInterval:interval target:self selector:@selector(bl_blockSelector:) userInfo:[block copy] repeats:repeats];
}

+ (void)bl_blockSelector:(NSTimer *)timer {

void(^block)(void) = timer.userInfo;
if (block) {
block();
}
}
@end

设置timer的target为自身,解决了循环引用的问题。使用时要注意block引起的循环引用。

1
2
3
4
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer bl_scheduledTimerWithTimeInterval:1 block:^{
[weakSelf doSomething];
} repeats:YES];

给target添加中间件NSProxy

在YYKit中,作者使用YYWeakProxy类作为NSTimer、CADisplayLink 的target,使得Timer得引用变为 VC->NSTimer->YYWeakProxy–>VC,YYWeakProxy弱引用VC,当触发timer的方法时,通过消息转发机制将消息转发至VC,避免了循环引用。
使用方式在头文件中可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#import <Foundation/Foundation.h>
/**
A proxy used to hold a weak object.
It can be used to avoid retain cycles, such as the target in NSTimer or CADisplayLink.
sample code:

@implementation MyView {
NSTimer *_timer;
}

- (void)initTimer {
YYWeakProxy *proxy = [YYWeakProxy proxyWithTarget:self];
_timer = [NSTimer timerWithTimeInterval:0.1 target:proxy selector:@selector(tick:) userInfo:nil repeats:YES];
}

- (void)tick:(NSTimer *)timer {...}
@end
*/
@interface YYWeakProxy : NSProxy
/**
The proxy target.
*/
@property (nullable, nonatomic, weak, readonly) id target;

/**
Creates a new weak proxy for target.

@param target Target object.

@return A new proxy object.
*/
- (instancetype)initWithTarget:(id)target;

/**
Creates a new weak proxy for target.

@param target Target object.

@return A new proxy object.
*/
+ (instancetype)proxyWithTarget:(id)target;

@end

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#import "YYWeakProxy.h"


@implementation YYWeakProxy

- (instancetype)initWithTarget:(id)target {
_target = target;
return self;
}

+ (instancetype)proxyWithTarget:(id)target {
return [[YYWeakProxy alloc] initWithTarget:target];
}

- (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

使用GCD timer

以下为YYTimer,实现了线程安全的GCD timer,不会被runloop的mode影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/**
YYTimer is a thread-safe timer based on GCD. It has similar API with `NSTimer`.
YYTimer object differ from NSTimer in a few ways:

* It use GCD to produce timer tick, and won't be affected by runLoop.
* It make a weak reference to the target, so it can avoid retain cycles.
* It always fire on main thread.

*/
@interface YYTimer : NSObject

+ (YYTimer *)timerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats;

- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats NS_DESIGNATED_INITIALIZER;

@property (readonly) BOOL repeats;
@property (readonly) NSTimeInterval timeInterval;
@property (readonly, getter=isValid) BOOL valid;

- (void)invalidate;

- (void)fire;

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#import "YYTimer.h"
#import <pthread.h>

#define LOCK(...) dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); \
__VA_ARGS__; \
dispatch_semaphore_signal(_lock);


@implementation YYTimer {
BOOL _valid;
NSTimeInterval _timeInterval;
BOOL _repeats;
__weak id _target;
SEL _selector;
dispatch_source_t _source;
dispatch_semaphore_t _lock;
}

+ (YYTimer *)timerWithTimeInterval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats {
return [[self alloc] initWithFireTime:interval interval:interval target:target selector:selector repeats:repeats];
}

- (instancetype)init {
@throw [NSException exceptionWithName:@"YYTimer init error" reason:@"Use the designated initializer to init." userInfo:nil];
return [self initWithFireTime:0 interval:0 target:self selector:@selector(invalidate) repeats:NO];
}

- (instancetype)initWithFireTime:(NSTimeInterval)start
interval:(NSTimeInterval)interval
target:(id)target
selector:(SEL)selector
repeats:(BOOL)repeats {
self = [super init];
_repeats = repeats;
_timeInterval = interval;
_valid = YES;
_target = target;
_selector = selector;

__weak typeof(self) _self = self;
_lock = dispatch_semaphore_create(1);
_source = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(_source, dispatch_time(DISPATCH_TIME_NOW, (start * NSEC_PER_SEC)), (interval * NSEC_PER_SEC), 0);
dispatch_source_set_event_handler(_source, ^{[_self fire];});
dispatch_resume(_source);
return self;
}

- (void)invalidate {
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
if (_valid) {
dispatch_source_cancel(_source);
_source = NULL;
_target = nil;
_valid = NO;
}
dispatch_semaphore_signal(_lock);
}

- (void)fire {
if (!_valid) return;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER);
id target = _target;
if (!target) {
dispatch_semaphore_signal(_lock);
[self invalidate];
} else {
dispatch_semaphore_signal(_lock);
[target performSelector:_selector withObject:self];
if (!_repeats) {
[self invalidate];
}
}
#pragma clang diagnostic pop
}

- (BOOL)repeats {
LOCK(BOOL repeat = _repeats); return repeat;
}

- (NSTimeInterval)timeInterval {
LOCK(NSTimeInterval t = _timeInterval) return t;
}

- (BOOL)isValid {
LOCK(BOOL valid = _valid) return valid;
}

- (void)dealloc {
[self invalidate];
}

@end

更精确的Timer

纳秒级精度的Timer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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);

}

参考文档

NSTimer使用详解
NSProxy使用