文档更新说明
- 最后更新 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
|
#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
+ (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
|
#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; }
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; }
- (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL; [invocation setReturnValue:&null]; }
- (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
- (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没有被销毁了.