1. 循环引用

1.1 常见问题

日常开发中,经常会用到NSTimer定时器,一些不正确的写法,会导致NSTimer造成循环引用

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
  4. [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  5. }
  6. - (void)fireHome{
  7. num++;
  8. NSLog(@"hello word - %d",num);
  9. }
  10. - (void)dealloc{
  11. [self.timer invalidate];
  12. self.timer = nil;
  13. }

上述案例,一定会产生循环引用

  • 创建NSTimer时,将self传入target,导致NSTimer持有self,而self又持有timer

  • 使用NSTimerinvalidate方法,可以解除NSTimerself的持有

  • 但案例中,NSTimerinvalidate方法,由UIViewControllerdealloc方法执行。但selftimer持有,只要timer有效,UIViewControllerdealloc方法就不会执行。故此双方相互等待,造成循环引用

1.2 target传入弱引用对象

NSTimertarget参数传入一个弱引用的self,能否打破对self的强持有

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. __weak typeof(self) weakSelf = self;
  4. self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
  5. [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  6. }
  • 肯定是不行的,因为在timerWithTimeInterval内部,使用强引用对象接收target参数,所以外部定义为弱引用对象没有任何意义

这种方式,类似于以下代码:

  1. __weak typeof(self) weakSelf = self;
  2. typeof(self) strongSelf = weakSelf;

在官方文档中,对target参数进行了明确说明:
image.png

  • target:定时器触发时指定的消息发送到的对象。计时器维护对该对象的强引用,直到它(计时器)失效

Block的区别,Block将捕获到的弱引用对象,赋值给一个强引用的临时变量,当Block执行完毕,临时变量会自动销毁,解除对外部变量的持有

2. 常规解决方案

2.1 更换API

使用携带Block的方法创建NSTimer,避免target的强持有

  1. + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval
  2. repeats:(BOOL)repeats
  3. block:(void (^)(NSTimer *timer))block;

2.2 在适当时机调用invalidate

根据业务需求,可以将NSTimerinvalidate方法写在viewWillDisappear方法中

  1. - (void)viewWillAppear:(BOOL)animated{
  2. [super viewWillAppear:animated];
  3. self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
  4. [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  5. }
  6. - (void)viewWillDisappear:(BOOL)animated{
  7. [super viewWillDisappear:animated];
  8. [self.timer invalidate];
  9. self.timer = nil;
  10. }

或者,写在didMoveToParentViewController方法中

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
  4. [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
  5. }
  6. - (void)didMoveToParentViewController:(UIViewController *)parent{
  7. if (parent == nil) {
  8. [self.timer invalidate];
  9. self.timer = nil;
  10. }
  11. }

3. 切断target强持有

除了常规解决方案,还可以通过切断target的强持有,解决循环引用的问题

3.1 中介者模式

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. NSObject *objc = [[NSObject alloc] init];
  4. class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
  5. self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:objc selector:@selector(fireHome) userInfo:nil repeats:YES];
  6. }
  7. void fireHomeObjc(id obj){
  8. num++;
  9. NSLog(@"hello word - %d -%@",num, obj);
  10. }
  11. - (void)dealloc{
  12. [self.timer invalidate];
  13. self.timer = nil;
  14. }
  • 创建NSObject实例对象objc,通过RuntimeNSObject增加fireHome方法,IMP指向fireHomeObjc的函数地址

  • 创建NSTimer,将objc传入target参数,这样避免NSTimerself的强持有

  • 当页面退出时,由于self没有被NSTimer持有,正常调用dealloc方法

    • dealloc中,对NSTimer进行释放。此时NSTimerobjc的强持有解除,objc也跟着释放

3.2 封装自定义Timer

创建LGTimerWapper,实现自定义Timer的封装

打开LGTimerWapper.h文件,写入以下代码:

  1. #import <Foundation/Foundation.h>
  2. @interface LGTimerWapper : NSObject
  3. - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
  4. - (void)lg_invalidate;
  5. @end

打开LGTimerWapper.m文件,写入以下代码:

  1. #import "LGTimerWapper.h"
  2. #import <objc/message.h>
  3. @interface LGTimerWapper()
  4. @property (nonatomic, weak) id target;
  5. @property (nonatomic, assign) SEL aSelector;
  6. @property (nonatomic, strong) NSTimer *timer;
  7. @end
  8. @implementation LGTimerWapper
  9. - (instancetype)lg_initWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo{
  10. if (self == [super init]) {
  11. self.target = aTarget;
  12. self.aSelector = aSelector;
  13. if ([self.target respondsToSelector:self.aSelector]) {
  14. Method method = class_getInstanceMethod([self.target class], aSelector);
  15. const char *type = method_getTypeEncoding(method);
  16. class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);
  17. self.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
  18. }
  19. }
  20. return self;
  21. }
  22. void fireHomeWapper(LGTimerWapper *warpper){
  23. if (warpper.target) {
  24. void (*lg_msgSend)(void *,SEL, id) = (void *)objc_msgSend;
  25. lg_msgSend((__bridge void *)(warpper.target), warpper.aSelector,warpper.timer);
  26. }
  27. else {
  28. [warpper lg_invalidate];
  29. }
  30. }
  31. - (void)lg_invalidate{
  32. [self.timer invalidate];
  33. self.timer = nil;
  34. }
  35. @end

LGTimerWapper的调用代码:

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
  4. }
  5. - (void)fireHome{
  6. num++;
  7. }
  • LGTimerWapper中,定义初始化lg_initWithTimeInterval方法和lg_invalidate释放方法

  • 初始化方法

    • 控件内部的target使用weak修饰,对ViewController进行弱持有

    • 检测target中是否存在该selector。如果存在,对当前类使用Runtime添加同名方法编号,指向自身内部fireHomeWapper的函数地址

    • 创建真正的NSTimer定时器,将控件自身的实例对象传入target,避免NSTimerViewController强持有

  • NSTimer回调时,会进入fireHomeWapper函数

    • 函数内部不负责业务处理,如果target存在,使用objc_msgSend,将消息发送给target自身下的selector方法
  • 当页面退出时,ViewController可以正常释放。但LGTimerWapperNSTimer相互持有,双方都无法释放

  • 由于双方都无法释放,NSTimer的回调会继续调用

    • 当进入fireHomeWapper函数,发现target已经不存在了,调用LGTimerWapperlg_invalidate方法,内部对NSTimer进行释放

    • NSTimer释放后,对LGTimerWapper的强持有解除,LGTimerWapper也跟着释放

3.3 NSProxy虚基类

NSProxy的作用:

  • OC不支持多继承,但是它基于运行时机制,可以通过NSProxy来实现伪多继承

  • NSProxyNSObject属于同一级别的类,也可以说是一个虚拟类,只实现了NSObject的协议部分

  • NSProxy本质是一个消息转发封装的抽象类,类似一个代理人

可以通过继承NSProxy,并重写以下两个方法实现消息转发

  1. - (void)forwardInvocation:(NSInvocation *)invocation;
  2. - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

NSProxy除了可以用于多继承,也可以作为切断强持有的中间人

打开LGProxy.h文件,写入以下代码:

  1. #import <Foundation/Foundation.h>
  2. @interface LGProxy : NSProxy
  3. + (instancetype)proxyWithTransformObject:(id)object;
  4. @end

打开LGProxy.m文件,写入以下代码:

  1. #import "LGProxy.h"
  2. @interface LGProxy()
  3. @property (nonatomic, weak) id object;
  4. @end
  5. @implementation LGProxy
  6. + (instancetype)proxyWithTransformObject:(id)object{
  7. LGProxy *proxy = [LGProxy alloc];
  8. proxy.object = object;
  9. return proxy;
  10. }
  11. - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
  12. if (!self.object) {
  13. NSLog(@"异常收集-stack");
  14. return nil;
  15. }
  16. return [self.object methodSignatureForSelector:sel];
  17. }
  18. - (void)forwardInvocation:(NSInvocation *)invocation{
  19. if (!self.object) {
  20. NSLog(@"异常收集-stack");
  21. return;
  22. }
  23. [invocation invokeWithTarget:self.object];
  24. }
  25. @end

LGProxy的调用代码:

  1. - (void)viewDidLoad {
  2. [super viewDidLoad];
  3. self.proxy = [LGProxy proxyWithTransformObject:self];
  4. self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.proxy selector:@selector(fireHome) userInfo:nil repeats:YES];
  5. }
  6. - (void)fireHome{
  7. num++;
  8. NSLog(@"hello word - %d",num);
  9. }
  10. - (void)dealloc{
  11. [self.timer invalidate];
  12. self.timer = nil;
  13. }
  • LGProxy初始化方法,将传入的object赋值给弱引用对象

  • UIViewController中,创建LGProxy对象proxy。创建NSTimer对象,将proxy传入target,避免NSTimerViewController强持有

  • NSTimer回调时,触发LGProxy的消息转发方法

    • methodSignatureForSelector:设置方法签名

    • forwardInvocation:自身不做业务处理,将消息转发给object

  • 当页面退出时,ViewController可以正常释放

    • dealloc中,对NSTimer进行释放。此时NSTimerproxy的强持有解除,proxy也跟着释放

总结:

循环引用:

  • 创建NSTimer时,使用带有target参数的方法,会对传入的对象进行强引用。如果传入的是持有timer的对象,双发会相互持有,造成循环引用

  • 不能在UIViewControllerdealloc方法中释放timer。只要timer有效,UIViewControllerdealloc方法就不会执行。故此双方相互等待,谁都无法释放

  • NSTimertarget参数传入一个弱引用的self没有任何意义,因为在创建NSTimer的方法内部,使用强引用对象接收target参数

常规解决方案:

  • 使用携带Block的方法创建NSTimer,避免target的强持有

  • 根据业务需求,在适当时机调用invalidate。例如:viewWillDisappeardidMoveToParentViewController

切断target的强持有:

  • 中介者模式

  • 封装自定义Timer

  • 使用NSProxy虚基类