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)interval
repeats:(BOOL)repeats
block:(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
虚基类