文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 问题的根源
  4. 4. 原理
  5. 5. 代码
  6. 6. 使用

文档更新说明

  • 最后更新 2019年08月22日
  • 首次更新 2019年08月22日

前言

  近期去过几家公司, 发现iOS开发中大家”讨论”的都离不开NSTimer的引用循环问题, 所以我在这里也总结一下最舒适的设计方案并且引入自己的项目中, 实际效果还是挺不错的, 不用再担心NSTimer的释放代码写在何处好: )   

问题的根源

  NSTimer在iOS开发中还是用得非常多的, 是一个常用的定时器, 但是他又存在一个致命缺点, 稍微不小心就会导致引用循环问题进而出现内存泄漏. 当A持有NSTimer对象, 同时A也是NSTimer对象的target时, 就会出现双方互相持有进而引发引用循环问题, 这样就不得不找个地方向NSTimer发送- (void)invalidate;消息结束这种情况.

  Apple在iOS10中给开发者带来了防止引用循环的API, 如下:

1
2
3
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

不过如果想要支持iOS10以下的系统以及使用Target-Action的设计模式的话, 还是只能使用老的API. 其实这个问题很早就有解决方案了, 依托OC强大的Runtime机制, 实现起来还是非常简单优雅的.

原理

  想象一下, 如果我们的A对象持有NSTimer对象, 但是NSTimer对象不持有A对象呢? 直接看是做不到的, 因为我们不能去修改NSTimer的代码呀, 但是换一个角度思考, 如果NSTimer持有Proxy对象, 然后Proxy对象不持有A对象, 只是拥有A对象的weak引用, 这个时候NSTimer向Proxy对象发送定时事件时, Proxy再把对应事件转发给A对象;

这就是弱引用代理的原理了, 非常简单, 不过上面还没有解决一个问题, 那就是NSTimer还是强引用Proxy对象, Proxy对象也强引用NSTimer对象, 这样定时事件还是不停发送, 这个是我们不愿意看到的. 好在这个问题也有很好的解决方案, 当A对象销毁的时候, 我们顺便向NSTimer对象发送invalidate消息, 销毁定时器, 这样就完美解决问题了谁也不会泄漏.

另一个问题就是上面提到的消息转发, 这个可以借助Runtime消息转发机制实现, Proxy可以将所有没有实现的消息都交给A对象去响应即可. 具体的Runtime消息转发原理网上找找有很多资料这里就省略了.

代码

  Proxy的实现其实很简单, 下面我直接引入YYKit中的一个小工具

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
//
// CCWeakProxy.h
// FlowMiner
//
// Created by Cocos on 2019/8/22.
// Copyright © 2019 YunFan. All rights reserved.
//

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface YYWeakProxy : NSProxy

@property (nonatomic, weak, readonly) id target;

- (instancetype)initWithTarget:(id)target;

+ (instancetype)proxyWithTarget:(id)target;



@end

@interface CCTimerWeakProxy: YYWeakProxy

/**
获取定时器, 可重复计时, 需要在持有Timer的对象被dealloc的时候调用[returnValue invaild]方法释放定时器

@param target 目标
@param selector 方法
@param fireDate 触发时间, nil表示立即触发
@param interval 触发间隔
@return NSTimer
*/
+ (NSTimer *)timerTriggerForTarget:(id)target sel:(SEL)selector fire:(NSDate * _Nullable)fireDate interval:(NSTimeInterval)interval userInfo:(nullable id)userInfo;

@end

NS_ASSUME_NONNULL_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
99
100
101
102
//
// CCWeakProxy.m
// FlowMiner
//
// Created by Cocos on 2019/8/22.
// Copyright © 2019 YunFan. All rights reserved.
//

#import "CCWeakProxy.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;
}

// 如果target为nil, 则简单实现转发消息, 让程序不崩溃即可.
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
void *null = NULL;
[invocation setReturnValue:&null];
}


// 其他NSObject方法, 都交由target处理
- (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

@implementation CCTimerWeakProxy

+ (NSTimer *)timerTriggerForTarget:(id)target sel:(SEL)selector fire:(NSDate *)fireDate interval:(NSTimeInterval)interval userInfo:(id)userInfo {

CCTimerWeakProxy *proxy = [[CCTimerWeakProxy alloc] initWithTarget:target];

NSTimer *timer;
if (fireDate) {
timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:interval target:proxy selector:selector userInfo:userInfo repeats:YES];
}else {
timer = [NSTimer timerWithTimeInterval:interval target:proxy selector:selector userInfo:userInfo repeats:YES];
}

return timer;
}

@end

其中CCTimerWeakProxy是我封装的用于快速创建NSTimer的一个子类, 使用的时候直接把两个文件的代码拖到项目里即可.

使用

  下面做个示范, 代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@interface FMAcutionDetailBannerCell()

@property (nonatomic, strong) NSTimer *timer;

@end

@implementation

// 当cell对象要被释放时, 顺带一起把timer销毁了
- (void)dealloc {
[self.timer invalidate];
}

// 可以这样启动定时器
- (void)startFireDateTimerForEnd {
self.timer = [CCTimerWeakProxy timerTriggerForTarget:self sel:@selector(countedTimerFire:) fire:nil interval:1 userInfo:nil];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addTimer:self.timer forMode:NSRunLoopCommonModes];
}
@end

这样Cell对象可以正确释放, 也不用担心Timer没有被销毁了.

文章目录
  1. 1. 文档更新说明
  2. 2. 前言
  3. 3. 问题的根源
  4. 4. 原理
  5. 5. 代码
  6. 6. 使用