NSTimer循环引用的问题

前言

我们在开发中,经常会用到NSTimer这个类的+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo方法,但是NSTimer会对传入的target对象进行强引用,如果target又对timer进行了强引用,那么就会出现循环引用,今天我们就研究一下如何解决这个问题。

例如:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}
@end

当控制器pop出去的时候,会执行dealloc方法,看上去好像没什么问题,但是我们实际的运行结果是这样的:

2019-08-14 20:26:39.636581+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:40.636660+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:41.636513+0800 定时器-01[16487:12091593] -[ViewController timerTest]
2019-08-14 20:26:42.636270+0800 定时器-01[16487:12091593] -[ViewController timerTest]

当我们的控制器pop消失以后控制台还在一直打印timerTest的方法,dealloc方法也没有执行。这是为什么?原因就是控制器timer进行了强引用,而timer又会对传入的target也就是控制器进行了强引用,这就造成了循环引用。

这时候我们就会想,将self通过__weak修饰一下不就可以了,那么我们来试一下,代码经过改造后如下:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:weakSelf selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}
@end

这时候我们再次运行代码,结果还是一样的,控制器消失了,但是控制台还在一直打印timerTest方法,这是为什么呢?

其实原因还是上面所说的,timer对传入的terget也就是self进行了强引用,虽然我们在外部传入的是一个弱引用的weakSelf,但是timer内部还是有一个强指针指向了self的内存地址。

那么我们应该怎么解决这个问题呢?在这里我提供了三种解决方式:

  • 方案1:在- (void)viewWillDisappear:(BOOL)animated方法中调执行[_timer invalidate]方法

改造后的代码如下:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    //在此处调用,可以避免循环引用导致的内存问题
    [_timer invalidate];
}

- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end

运行结果如下:

2019-08-14 20:54:36.662354+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:37.662384+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:38.662300+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:39.662305+0800 定时器-01[16508:12095299] -[ViewController timerTest]
2019-08-14 20:54:40.210602+0800 定时器-01[16508:12095299] -[ViewController dealloc]

我们看到,当 控制器消失的时候,执行了dealloc方法,为什么呢?原因是在[_timer invalidate]执行以后,释放了对target也就是self的强引用,循环引用被破坏,因此控制器可以被释放。

这种方式仅限于控制器中,当我们的传入的target不是一个控制器,而是一个普通的继承自NSObject的对象的时候,就不能用这种方式,因此我们来看下面的方法

  • 方案2:使用+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block方法

改造后的代码如下:

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    __weak typeof(self) weakSelf = self;
    if (@available(iOS 10.0, *)) {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
            [weakSelf timerTest];
        }];
    } else {
        // Fallback on earlier versions
    }
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}

执行结果如下:

2019-08-14 20:44:26.588273+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:27.588241+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:28.588314+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.588270+0800 定时器-01[16505:12094230] -[ViewController timerTest]
2019-08-14 20:44:29.611601+0800 定时器-01[16505:12094230] -[ViewController dealloc]

这时候,当控制器消失的时候,执行了dealloc方法,原因是虽然timerblock是强引用,但是block内部对于self是弱引用的,这个时候,破坏了循环引用,控制器没有被timer强引用,所以可以被释放。但是我们发现,这个方法只有在iOS10以后才有的,所以,如果我们的app的兼容版本是iOS 10以后的,那么我们可以这么写,如果要兼容更低版本,那么我们只能载想其他的办法。

  • 方案3:使用代理对象

首先我们创建一个代理对象,代码如下:

#import <Foundation/Foundation.h>

@interface TimerProxy : NSObject
@property (weak, nonatomic) id target;

+ (instancetype)proxyWithTarget:(id)target;

@end
#import "TimerProxy.h"
@implementation TimerProxy

+ (instancetype)proxyWithTarget:(id)target {
    TimerProxy *proxy = [[TimerProxy alloc] init];
    proxy.target = target;
    return proxy;
}

//利用消息转发机制,将方法转发给target处理
- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}
@end

然后改造原有的代码:

#import "TimerProxy.h"

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //此时,传入的target就不是self,而是TimerProxy对象
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}

执行结果如下:

2019-08-14 21:17:47.476647+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定时器-01[16527:12097738] -[ViewController dealloc]

这种方案,我们采用的是代理对象的方式,控制器对于timer是强引用,timer对于代理对象是强引用,而代理对象对于控制器是弱引用,这样我们就破坏了循环引用,最终调用了dealloc方,从而解决问题。

  • 方案4:使用NSProxy(推荐方案)

首先,我们自定义一个继承自NSProxy的类

#import <Foundation/Foundation.h>

@interface TimerProxy2 : NSProxy
@property (weak, nonatomic) id target;

+ (instancetype)proxyWithTarget:(id)target;
@end
#import "TimerProxy2.h"

@implementation TimerProxy2
+ (instancetype)proxyWithTarget:(id)target {
//    这句代码会报错,因为NSProxy无init方法,直接alloc即可
//    TimerProxy2 *proxy = [[TimerProxy2 alloc] init];
    TimerProxy2 *proxy = [TimerProxy2 alloc];
    proxy.target = target;
    return proxy;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    
    return [self.target methodSignatureForSelector:sel];
}

//消息转发
- (void)forwardInvocation:(NSInvocation *)invocation {
    invocation.target = self.target;
    [invocation invokeWithTarget:self.target];
}
@end

改造原有代码

#import "TimerProxy2.h"

@interface ViewController ()
@property (strong, nonatomic) NSTimer *timer;
@end
@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //此时,传入的target就不是self,而是TimerProxy对象
   self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:[TimerProxy2 proxyWithTarget:self] selector:@selector(timerTest) userInfo:nil repeats:YES];
}

- (void)timerTest {
    NSLog(@"%s", __func__);
}

- (void)dealloc {
    NSLog(@"%s", __func__);
//类销毁的时候调用`timer`的`invalidate`方法进行停止
    [_timer invalidate];
}

执行结果如下:

2019-08-14 21:17:46.476786+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:47.476647+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:48.475937+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:49.476636+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:50.476643+0800 定时器-01[16527:12097738] -[ViewController timerTest]
2019-08-14 21:17:51.120013+0800 定时器-01[16527:12097738] -[ViewController dealloc]

这种方案也可以解决循环引用的问题,但是大家可能觉得,这种写法相比方案3更麻烦一些,代码写的要更多,但是为什么更推荐这种实现方式呢?原因还要从oc方法调用分析。

oc的方法调用最终会转换为objc_msgSend函数进行调用,objc_msgSend的执行流程分为三大阶段

  • 消息发送
  • 动态解析
  • 消息转发

其中消息发送阶段,就会进行多次方法查找。首先从自身的方法缓存中进行查找,找不到会在本类的方法列表进行查找,找不到再去父类的缓存中进行查找,找不到再去父类的方法列表中进行查找...一直超找到根类,如果还没有找到的话,就会进行动态方法解析,没有实现动态方法解析的话,最后就会进入消息转发阶段。

而使用NSProxy会先从自身方法列表进行查找,找不到的话,会直接进入消息转发阶段,省去了去父类一层层查找和动态方法解析这些操作,因此效率更高。

自此,我们关于NSTimer引起的循环引用问题就算是彻底解决了,其实方法4也是给我们提供了一种思路,如果以后开发中遇到了类似于NSTimer这种的循环引用问题,我们完全可以通过代理对象这种方式来解决这种问题。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,922评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,591评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,546评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,467评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,553评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,580评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,588评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,334评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,780评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,092评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,270评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,925评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,573评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,194评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,437评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,154评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352