NSTimer 循环引用的原因和解决方案

NSTimer 循环引用的原因和解决方案

造成循环引用的原因就是两个对象之间因为强引用无法释放。本文将通过NSTimer来剖析强引用,以及解决方法。

1. 强引用

举个例子,比如我们有两个ViewController,分别为AB,从A可以pushB,从B可以popAB中代码如下:


static int num = 0;

@property (nonatomic, strong) NSTimer       *timer;

- (void)viewDidLoad {
    [super viewDidLoad];

     self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
     // 加runloop
     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

- (void)fireHome{
    num++;
    NSLog(@"hello word - %d",num);
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}

当我们从B界面popA时,timer并不会停,那是为什么呢?显然是没有执行B界面的dealloc方法,导致B界面没有被释放。

既然没释放肯定是有循环引用,那么这个循环引用产生的在哪里呢?乍一看,我们的BViewController强引用了timer,那么如果说造成循环引用就是timer强引用了self,但是这里面没有block怎么产生的循环引用呢?这里面在初始化timer的时候有个target,我们查看一下这个初始化方法shift+command+0,搜索一下timerWithTimeInterval:target:selector:userInfo:repeats:关于target的描述如下:

image

可以看到timertarget保持强引用,直到timer失效。

所以说循环引用就产生了,B强引用着timertimer强引用着target也就是self,在这里self就是B的实例对象。此时就是:
self -> timer -> self构成的循环引用。

我们在iOS Objective-C Block简介这篇文章中介绍了使用weakSelf来解决循环引用,既然是这样,那么我们用weakSelf是否可以解决这层循环引用呢?

将代码修改为如下:

- (void)viewDidLoad {
    [super viewDidLoad];

    __weak typeof(self) weakSelf = self;
     self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
     // 加runloop
     [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
}

运行,依旧没有打破循环引用,timerpop后依旧运行。那么这是为什么呢?,在block中我们可以使用weakSelf来打破循环引用,那么在这里为什么不行呢?

此时我们使用__weak虽然打破了self -> timer -> self这个循环引用,使其变成了self -> timer -> weakSelf -> self

但是这里我们分析的并不全面,因为我们的timer需要加入到RunloopRunlooptimer是一个强持有,Runloop的生命周期比B界面更长,所以这才是导致timer无法释放的真正原因,timer无法释放,自然self也就无法释放。所以这个引用链最初应该是这样的:

self -> timer -> self
runloop -> timer -> self

画个图:

image

加上weakSelf之后,变成了这样:

self -> timer -> weakSelf -> self
runloop -> timer -> weakSelf -> self

image

那么虽然是这样weakSelf也是弱引用啊,为什么不能打破循环引用呢?在block中我们可以通过self -> block -> weakSelf -> self打破循环引用?为什么这里就不可以了呢?

这里我们就要稍微研究一下这行代码了:
__weak typeof(self) weakSelf = self;

我们想知道weakSelfself有什么区别,其实主要是这三点:

  1. weakSelf会对self的引用计数+1吗?
  2. weakSelfself的指针地址相同吗?
  3. weakSelfself是指向同一片内存空间吗?

下面我们验证一下,添加这样一段代码:

    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));
    __weak typeof(self) weakSelf = self;
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)self));

运行并通过lldb调试得到如下结果:

image

我们可以看到:

  • weakSelf并没有增加self的引用计数
  • weakSelfself指向同一内存区域
  • weakSelfself的指针地址是不同的

其实分析完这里我们也看不出什么,这里的引用关系还是这幅图:

image

下面我们在看看block中的weakSelf,添加如下代码:

@property (nonatomic, copy)  void(^myBlock)(void);
@property (nonatomic, copy) NSString *name;

- (void)test1 {
    __weak typeof(self) weakSelf = self;
    self.name = @"test1";
    self.myBlock = ^{
        NSLog(@"%@",weakSelf.name);
    };
    
    self.myBlock();
}

调用test1,通过lldb调试:

image

此时就很清晰了,block中的weakSelf与外面的weakSelf根本不是同一个对象,虽然他们指向的都是同一片内存区域,在这里就是<LGTimerViewController: 0x7fc275604b10>,下面我们在看看libclosure中的_Block_object_assign函数。

image

在这里我们看到都是取的对象的地址**,或者是通过_Block_copy拷贝一份,也就是说在block中都是临时变量,一份新的变量,所以说在block中其引用链并不存在对weakSelf持有,而是持有的weakSelf的指针地址,也就是*weakSelf,跟self没有任何关系。

然而在timer这里,timerweakSelf也就是target是强持有,所以不能打破循环引用。

所以对于blocktimer两个模型之间循环引用的区别如下:

timerself -> timer -> weakSelf -> self
blockself -> block -> *weakSelf

2. 解决Timer强引用

2.1 不使用带target的Timer

因为timer通过target强持有了self,那么我们不使用含有target的API不就就可以了,修改代码为如下:

self.timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"hello word - %d",num);
}];

2.2 提前销毁timer

因为timer通过target强持有了self,当我们需要pop的时候,提前销毁timer就可以打破这层循环引用,所以我们可以通过didMoveToParentViewController,但是无论是pop还是push都会调用该方法,所以我们加一层判断,代码如下:

- (void)didMoveToParentViewController:(UIViewController *)parent{
    // 无论push 进来 还是 pop 出去 正常跑
    // 就算继续push 到下一层 pop 回去还是继续
    if (parent == nil) {
       [self.timer invalidate];
        self.timer = nil;
        NSLog(@"timer 走了");
    }
}

此时当我们pop的时候就可以正常销毁timer了。

2.3 中介者模式

在这里我们关系的是fireHome能执行,并不关心timer捕获的target是谁,所以为了避免循环引用,我们可以把target换成其他对象,将fireHome交给target执行。所以修改代码为如下:

#import <objc/runtime.h>// 导入runtime

//* 定义一个id类型的对象属性 */
@property (nonatomic, strong) id            target;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 初始化target
    self.target = [[NSObject alloc] init];
    // 给NSObject添加方法
    class_addMethod([NSObject class], @selector(fireHome), (IMP)fireHomeObjc, "v@:");
    // 初始化timer
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self.target selector:@selector(fireHome) userInfo:nil repeats:YES];
}

void fireHomeObjc(id obj){
    num++;
    NSLog(@"hello word - %d",num);
}

- (void)dealloc{
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}

这里因为不在强引用selfself就可以正常dealloc,也就可以停掉timer。从而解除对target的强引用。

2.4 自定义封装timer

上面的解决方式其实需要考虑的方面比较多,需要定义target对象,添加方法,停掉和置空timer,步骤还是蛮多的,稍不注意就可能出错,所以我们自己封装一个timer,作为中间层,来解决调用者这些复杂的操作,来使调用显得简单、方便、安全。

首先我们提供两个方法,分别是初始化方法和销毁timer的方法,代码如下:

@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

然后我们提供了三个属性,分别用于存储targetselector以及自定义timer中的timer属性,代码如下:

#import <objc/message.h>

@interface LGTimerWapper()
// 定义一个target 用于存储传入的target 注意这里使用的是weak
@property (nonatomic, weak) id target;
// 存储 sel
@property (nonatomic, assign) SEL aSelector;
// timer
@property (nonatomic, strong) NSTimer *timer;

@end

下面是初始化方法的实现:

  1. 首先我们存储了targetaSelector
  2. 然后判断target能响应aSelector的时候
    1. 为中介添加方法,这里面的中介就是当前类
    2. 并把imp指向当前类的fireHomeWapper方法
    3. 初始化timer
  3. return self
- (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; // vc
        self.aSelector  = aSelector; // 方法 -- vc 释放
        
        if ([self.target respondsToSelector:self.aSelector]) {
            // 将中介的处理添加到这里,不去外面再次添加,这里面的中介就是当前类型
            // 通过Runtime 获取到方法
            Method method    = class_getInstanceMethod([self.target class], aSelector);
            // 获取方法的type
            const char *type = method_getTypeEncoding(method);
            // 为当前类添加这个方法
            class_addMethod([self class], aSelector, (IMP)fireHomeWapper, type);

            // runloop&self -> timer -> lgtimerwarpper
            self.timer      = [NSTimer scheduledTimerWithTimeInterval:ti target:self selector:aSelector userInfo:userInfo repeats:yesOrNo];
        }
    }
    return self;
}

下面我们看看fireHomeWapper方法的实现,这里是重点也是难点:

  1. 首先判断target属性是否有值,因为这个属性是weak的,如果有值说明能响应
    1. 这里通过objc_msgSend来调用存储的aSelector
  2. 如果不存在,说明不能响应了,停掉timer并置空就好了

关于lg_invalidate方法的实现就更简单了,在本示例中没有用到该方法,但是如果想要主动销毁可以调用,代码如下:

- (void)lg_invalidate{
    [self.timer invalidate];
    self.timer = nil;
}

这样编写后调用的时候就非常简单了,减少了很多需要处理的地方:

#import "LGTimerWapper.h"

@property (nonatomic, strong) LGTimerWapper *timerWapper;
//* 定义一个id类型的对象属性 */

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.timerWapper = [[LGTimerWapper alloc] lg_initWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
}

2.5 使用NSProxy虚基类的子类

上面的代码虽然使用起来比较简单,但是代码写起来少多了些,有时候也存在维护问题,对于调用者没有真正的去调用invalidate和置空timer,总是有些别扭的,其实解决timer循环引用的最好的方式还是使用NSProxy。下面我们来看看怎么实现:

首先我们定义一个NSProxy的子类,这个类里面通过一个weak属性,持有着target中需要强引用的实例对象。代码如下:

#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;
}

但是仅仅是这样还是不行的,还需要让实际的target响应消息,毕竟LGProxy不能真正响应timer中的消息。

/*
    仅仅添加了weak类型的属性还不够,为了保证中间件能够响应外部self的事件
    需要通过消息转发机制,让实际的响应target还是外部self,
    这一步至关重要,主要涉及到runtime的消息机制。
*/
-(id)forwardingTargetForSelector:(SEL)aSelector {
    return self.object;
}

下面我们看看怎么使用:

#import "LGProxy.h"

@property (nonatomic, strong) LGProxy       *proxy;

- (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;
    NSLog(@"%s",__func__);
}

此时使用起来还是直接使用NSTimer,只是对target的强引用的修改成了Proxy

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

推荐阅读更多精彩内容