1. 循环引用
1.1 常见问题
日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer造成循环引用
- (void)viewDidLoad {[super viewDidLoad];self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];}- (void)fireHome{num++;NSLog(@"hello word - %d",num);}- (void)dealloc{[self.timer invalidate];self.timer = nil;}
上述案例,一定会产生循环引用
创建
NSTimer时,将self传入target,导致NSTimer持有self,而self又持有timer使用
NSTimer的invalidate方法,可以解除NSTimer对self的持有但案例中,
NSTimer的invalidate方法,由UIViewController的dealloc方法执行。但self被timer持有,只要timer有效,UIViewController的dealloc方法就不会执行。故此双方相互等待,造成循环引用
1.2 target传入弱引用对象
对NSTimer的target参数传入一个弱引用的self,能否打破对self的强持有
- (void)viewDidLoad {[super viewDidLoad];__weak typeof(self) weakSelf = self;self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];}
- 肯定是不行的,因为在
timerWithTimeInterval内部,使用强引用对象接收target参数,所以外部定义为弱引用对象没有任何意义
这种方式,类似于以下代码:
__weak typeof(self) weakSelf = self;typeof(self) strongSelf = weakSelf;
在官方文档中,对target参数进行了明确说明:
target:定时器触发时指定的消息发送到的对象。计时器维护对该对象的强引用,直到它(计时器)失效
和Block的区别,Block将捕获到的弱引用对象,赋值给一个强引用的临时变量,当Block执行完毕,临时变量会自动销毁,解除对外部变量的持有
2. 常规解决方案
2.1 更换API
使用携带Block的方法创建NSTimer,避免target的强持有
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)intervalrepeats:(BOOL)repeatsblock:(void (^)(NSTimer *timer))block;
2.2 在适当时机调用invalidate
根据业务需求,可以将NSTimer的invalidate方法写在viewWillDisappear方法中
- (void)viewWillAppear:(BOOL)animated{[super viewWillAppear:animated];self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];}- (void)viewWillDisappear:(BOOL)animated{[super viewWillDisappear:animated];[self.timer invalidate];self.timer = nil;}
或者,写在didMoveToParentViewController方法中
- (void)viewDidLoad {[super viewDidLoad];self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];}- (void)didMoveToParentViewController:(UIViewController *)parent{if (parent == nil) {[self.timer invalidate];self.timer = nil;}}
3. 切断target强持有
除了常规解决方案,还可以通过切断target的强持有,解决循环引用的问题
3.1 中介者模式
- (void)viewDidLoad {[super viewDidLoad];NSObject *objc = [[NSObject alloc] init];class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(fireHome) userInfo:nil repeats:YES];}void fireHomeObjc(id obj){num++;NSLog(@"hello word - %d -%@",num, obj);}- (void)dealloc{[self.timer invalidate];self.timer = nil;}
创建
NSObject实例对象objc,通过Runtime对NSObject增加fireHome方法,IMP指向fireHomeObjc的函数地址创建
NSTimer,将objc传入target参数,这样避免NSTimer对self的强持有当页面退出时,由于
self没有被NSTimer持有,正常调用dealloc方法- 在
dealloc中,对NSTimer进行释放。此时NSTimer对objc的强持有解除,objc也跟着释放
- 在
3.2 封装自定义Timer
创建LGTimerWapper,实现自定义Timer的封装
打开LGTimerWapper.h文件,写入以下代码:
#import <Foundation/Foundation.h>@interface LGTimerWapper : NSObject- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;- (void)lg_invalidate;@end
打开LGTimerWapper.m文件,写入以下代码:
#import "LGTimerWapper.h"#import <objc/message.h>@interface LGTimerWapper()@property (nonatomic, weak) id target;@property (nonatomic, assign) SEL aSelector;@property (nonatomic, strong) NSTimer *timer;@end@implementation LGTimerWapper- (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{if (self == [super init]) {self.target = aTarget;self.aSelector = aSelector;if ([self.target respondsToSelector:self.aSelector]) {Method method = class_getInstanceMethod([self.target class], aSelector);const char *type = method_getTypeEncoding(method);class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];}}return self;}void fireHomeWapper(LGTimerWapper *warpper){if (warpper.target) {void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);}else {[warpper lg_invalidate];}}- (void)lg_invalidate{[self.timer invalidate];self.timer = nil;}@end
LGTimerWapper的调用代码:
- (void)viewDidLoad {[super viewDidLoad];self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];}- (void)fireHome{num++;}
在
LGTimerWapper中,定义初始化lg_initWithTimeInterval方法和lg_invalidate释放方法初始化方法
控件内部的
target使用weak修饰,对ViewController进行弱持有检测
target中是否存在该selector。如果存在,对当前类使用Runtime添加同名方法编号,指向自身内部fireHomeWapper的函数地址创建真正的
NSTimer定时器,将控件自身的实例对象传入target,避免NSTimer对ViewController强持有
当
NSTimer回调时,会进入fireHomeWapper函数- 函数内部不负责业务处理,如果
target存在,使用objc_msgSend,将消息发送给target自身下的selector方法
- 函数内部不负责业务处理,如果
当页面退出时,
ViewController可以正常释放。但LGTimerWapper和NSTimer相互持有,双方都无法释放由于双方都无法释放,
NSTimer的回调会继续调用当进入
fireHomeWapper函数,发现target已经不存在了,调用LGTimerWapper的lg_invalidate方法,内部对NSTimer进行释放当
NSTimer释放后,对LGTimerWapper的强持有解除,LGTimerWapper也跟着释放
3.3 NSProxy虚基类
NSProxy的作用:
OC不支持多继承,但是它基于运行时机制,可以通过NSProxy来实现伪多继承NSProxy和NSObject属于同一级别的类,也可以说是一个虚拟类,只实现了NSObject的协议部分NSProxy本质是一个消息转发封装的抽象类,类似一个代理人
可以通过继承NSProxy,并重写以下两个方法实现消息转发
- (void)forwardInvocation:(NSInvocation *)invocation;- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
NSProxy除了可以用于多继承,也可以作为切断强持有的中间人
打开LGProxy.h文件,写入以下代码:
#import <Foundation/Foundation.h>@interface LGProxy : NSProxy+ (instancetype)proxyWithTransformObject:(id)object;@end
打开LGProxy.m文件,写入以下代码:
#import "LGProxy.h"@interface LGProxy()@property (nonatomic, weak) id object;@end@implementation LGProxy+ (instancetype)proxyWithTransformObject:(id)object{LGProxy *proxy = [LGProxy alloc];proxy.object = object;return proxy;}- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{if (!self.object) {NSLog(@"异常收集-stack");return nil;}return [self.object methodSignatureForSelector:sel];}- (void)forwardInvocation:(NSInvocation *)invocation{if (!self.object) {NSLog(@"异常收集-stack");return;}[invocation invokeWithTarget:self.object];}@end
LGProxy的调用代码:
- (void)viewDidLoad {[super viewDidLoad];self.proxy = [LGProxy proxyWithTransformObject:self];self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];}- (void)fireHome{num++;NSLog(@"hello word - %d",num);}- (void)dealloc{[self.timer invalidate];self.timer = nil;}
LGProxy初始化方法,将传入的object赋值给弱引用对象在
UIViewController中,创建LGProxy对象proxy。创建NSTimer对象,将proxy传入target,避免NSTimer对ViewController强持有当
NSTimer回调时,触发LGProxy的消息转发方法methodSignatureForSelector:设置方法签名forwardInvocation:自身不做业务处理,将消息转发给object
当页面退出时,
ViewController可以正常释放- 在
dealloc中,对NSTimer进行释放。此时NSTimer对proxy的强持有解除,proxy也跟着释放
- 在
总结:
循环引用:
创建
NSTimer时,使用带有target参数的方法,会对传入的对象进行强引用。如果传入的是持有timer的对象,双发会相互持有,造成循环引用不能在
UIViewController的dealloc方法中释放timer。只要timer有效,UIViewController的dealloc方法就不会执行。故此双方相互等待,谁都无法释放对
NSTimer的target参数传入一个弱引用的self没有任何意义,因为在创建NSTimer的方法内部,使用强引用对象接收target参数
常规解决方案:
使用携带
Block的方法创建NSTimer,避免target的强持有根据业务需求,在适当时机调用
invalidate。例如:viewWillDisappear、didMoveToParentViewController
切断target的强持有:
中介者模式
封装自定义
Timer使用
NSProxy虚基类
