解决NSTimer的循环引用

解决NSTimer的循环引用

一、循环引用的原因

一般我们使用NSTimer,都是设置成控制器的属性@property (strong, nonatomic) NSTimer *timer;,控制器强引用timer。而timer对添加的target(一般是控制器本身,也就是self)也是强引用关系,这样引用关系就变成了self => timer => self,形成了循环引用。可能有些人认为用 __weak修饰self方式打破循环引用__weak typeof(self) weakSelf = self,其实这样做是无效的,timer对target是强引用,而__weak weakSelf只是影响它所引用的对象retainCount,并不影响timer对target的retainCount。另外,即使self对timer没有持有关系,由于runloop对timer有持有关系,则 runloop => timer => target(self控制器),可以看到self的引用计数除非主动断开timer对self的强引用,否则不可能为0。

二、探索解决思路

要想打破循环引用,就要破掉其中一个的引用关系使之不能形成循环,首先self => timer的引用关系必然是强引用,那就要针对timer => target的引用下手了。一个比较容易想到的思路是自定义一个类A,A中设置一个weak属性@property (weak, nonatomic) id target;,我们给timer设置target的时候,首先创建Aclass的实例a,然后设置a.target = self,最后将这个a作为timer的target,整个的引用关系就变成 self => timer => a --> self(注意这里用-->表示弱引用),可以看到没有形成循环,此思路可行。

接下来的问题就在于如何让self接收timer的回调而不是a对象。好在oc中有runtime,可以通过消息转发的方式将消息转发到我们指定的对象上(self,timer所在控制器)。

简单说下消息转发机制,首先我们给一个对象obj发送一个消息@selector(msg),但是这个对象并没有msg方法,程序不会立马抛出异常,而是有三步消息转发的机会。

第一步:

  • +(BOOL)resolveInstanceMethod:(SEL)sel
  • +(BOOL)resolveClassMethod:(SEL)sel

第二步:

  • (id)forwardingTargetForSelector:(SEL)aSelector

第三步:

  • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
  • (void)forwardInvocation:(NSInvocation *)anInvocation

所以我们要想讲timer的回调转发到self上可以在第二步return a.target,或者在第三步拿到target方法签名,交由target去执行方法。

三、解决方案

这里介绍一个OC专门用来消息转发的类NSProxy,这个类不是继承自NSObject,仅仅是用来做消息转发的,注意这个类没有实现init方法,我们只需alloc就行。

另外对于NStimer,需要注意的是它是类簇的方式实现的,我们不能直接继承NSTimer,所以这里采用继承NSObject,内部持有一个NSTimer实例的方式。

1、首先创建一个消息转发类

@interface KJWeakProxy : NSProxy

@property (weak, nonatomic) id target;

+ (instancetype)weakProxyWithTarget:(id)target;
- (instancetype)initWeakProxyWithTarget:(id)target;
@end

@implementation KJWeakProxy

+ (instancetype)weakProxyWithTarget:(id)target {
    KJWeakProxy *proxy = [[self alloc] initWeakProxyWithTarget:target];
    return proxy;
}
- (instancetype)initWeakProxyWithTarget:(id)target {
    self = [KJWeakProxy alloc];
    self.target = target;
    return self;
}

- (id)forwardingTargetForSelector:(SEL)aSelector {
    return self.target;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    void *nullValue = NULL;
    [invocation setReturnValue:&nullValue];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [NSObject instanceMethodSignatureForSelector:@selector(init)];
}

@end

主要是消息转发的步骤,直接通过消息转发的第二步,将方法转交给target去执行。注意我下面还针对消息转发第三步做了一些事情,因为target是weak引用,所以在forwardingTargetForSelector中可能会返回nil,此时走消息转发第三步。为了防止触发doesNotRecognizeSelector,在methodSignatureForSelector中返回NSObject实例的init方法签名,并在forwardInvocation中设置返回值为nil。

2、自定义timer类

.h文件:

@interface KJTimer : NSObject

@property (copy) NSDate *fireDate;
@property (readonly) NSTimeInterval timeInterval;

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep runloopMode:(NSRunLoopMode)mode;

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode;
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode;

- (void)fire;
- (void)invalidate;

@end

仿照NSTimer的API创建实例对象,初始化方法增加一个runloopMode参数,方便timer运行在不同mode下。

.m文件:

@interface KJTimer ()

@property (strong, nonatomic) NSTimer *timer;


@end

@implementation KJTimer

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

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(id)ui repeats:(BOOL)rep runloopMode:(NSRunLoopMode)mode {
    if (self = [super init]) {
        KJWeakProxy *weakProxy = [KJWeakProxy weakProxyWithTarget:t];
        
        _timer = [[NSTimer alloc] initWithFireDate:date interval:ti target:weakProxy selector:s userInfo:ui repeats:rep];
        [[NSRunLoop currentRunLoop] addTimer:_timer forMode:mode];
    }
    
    return self;
}

+ (instancetype)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode {
    KJTimer *timer = [[KJTimer alloc] initWithFireDate:[NSDate distantFuture] interval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo runloopMode:mode];
    return timer;
}
+ (instancetype)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo runloopMode:(NSRunLoopMode)mode {
    KJTimer *timer = [[KJTimer alloc] initWithFireDate:[NSDate distantPast] interval:ti target:aTarget selector:aSelector userInfo:userInfo repeats:yesOrNo runloopMode:mode];
    return timer;
    
}

- (void)fire {
    [self.timer setFireDate:[NSDate distantPast]];
}
- (NSDate *)fireDate {
    return self.timer.fireDate;
}
- (void)setFireDate:(NSDate *)date {
    [self.timer setFireDate:date];
}
- (NSTimeInterval)timeInterval {
    return self.timer.timeInterval;
}
- (void)invalidate {
    [self.timer invalidate];
     self.timer = nil;
}
@end

在初始化方法中,直接将timer添加到当前runloop中,另外需要注意在dealloc中[self.timer invalidate],将timer从当前runloop移除,其余的使用方式跟NSTimer基本一致。

以上就是完整的解决方案,如果文中出现不对的地方,欢迎各位指正。

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

推荐阅读更多精彩内容