在循环引用的问题中,面试官最喜欢问到的就是Block和Timer的循环引用问题,今天就Timer的循环引用问题,进行分析。
一般问题的场景
需要用到Timer的场景一般是UI页面需要做定时的操作,比如定时翻动轮播图,或者定时刷新后台数据并显示等,我们大致可以得到的结论是:
- 需要用到Timer的是UI视图,在其内部使用;
- 响应的是UI操作,在主线程操作。
问题分析
针对大多数的情况,页面(VC)对视图是强引用,Timer对其target是强引用,再加上Timer会被添加到RunLoop中,因此RunLoop对Timer也是强引用,就算视图对Timer是弱引用,我们也可以得到这样的引用分析图:

在上图中,我们可以看到,就算页面退出,断开VC与View之间的引用,可是RunLoop-Timer-View的引用仍会存在,仍会造成view无法释放,导致VC无法释放,进而产生内存泄露的问题。
这里要排除两种情况,第一种情况是Timer是非重复定时器时,可以在特定的时候设置[timer invalidate]并将timer置为nil,此时便不会产生问题;第二种情况是VC退出时,逐个通知View关闭并移除定时器,如此也可以避免问题产生。
为什么RunLoop-Timer-View引用仍会存在
参考之前的文章:【iOS面试题】1.谈一谈AFNetworking线程保活。在此文章中提到,为子线程RunLoop添加Timer会使得子线程保活;同时由于主线程是随App一直存在并保活的,因此不论Timer是跑在子线程还是主线程,RunLoop对Timer的强引用都是存在的。
加上Timer-View的强引用,便构成了RunLoop-Timer-View的完整引用链条。
如何破解循环引用
破解循环引用的方法有两个:
- 避免产生循环引用;
- 在适当时机断开循环引用。
前面提到的循环引用的两种排除情况,充分说明了如何在适当时机断开循环引用,具体可以参考前面的内容。
避免产生循环引用
由于Timer的功能受RunLoop影响,因此要避免产生循环引用,唯一有可能的地方便是在Timer和View之间断开强引用链条。
没有什么是中间对象做不到的,如果有,那就再加一个中间对象
在这里,我们可以使用中间对象将Timer和View之间的强引用断开,分析如下图:

通过中间对象的方式,让Timer强引用中间对象,让中间对象分别弱引用Timer和View,便可以达到目的。代码如下:
中间Timer:
#import <Foundation/Foundation.h>
@interface NSTimer (Intermediate)
+ (NSTimer *)scheduledIntermediateTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
@end
----------------------------
#import "NSTimer+Intermediate.h"
#import "TimerIntermediateObject.h"
@implementation NSTimer (Intermediate)
+ (NSTimer *)scheduledIntermediateTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo {
TimerIntermediateObject *obj = [[TimerIntermediateObject alloc] init];
obj.target = aTarget;
obj.selector = aSelector;
// 注意这里的target和selector,是指向中间对象和其对应方法的
obj.timer = [NSTimer scheduledTimerWithTimeInterval:ti target:obj selector:@selector(fire:) userInfo:userInfo repeats:yesOrNo];
return obj.timer;
}
@end
复制代码
中间对象:
#import <Foundation/Foundation.h>
@interface TimerIntermediateObject : NSObject
@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selector;
- (void)fire:(NSTimer *)timer;
@end
----------------------------
#import "TimerIntermediateObject.h"
@implementation TimerIntermediateObject
- (void)fire:(NSTimer *)timer {
if (self.target) {
if ([self.target respondsToSelector:self.selector]) {
[self.target performSelector:self.selector withObject:timer.userInfo];
}
} else {
// 这里利用了weak修饰的对象释放后会自动置为nil的特性进行处理
[self.timer invalidate];
}
}
@end
复制代码
注意使用时直接调用NSTimer的分类方法
代码分析
NSTimer的分类方法接收了原生方法的全套参数,并将指向对象和方法转发给中间对象。
中间对象TimerIntermediateObject的弱引用属性分别接收了timer和target,并在fire:方法内将方法进行中转,并利用了weak修饰的对象释放后会自动置为nil的特性,在target不为nil时调用对应方法,在target为nil时自动将timer失效。
这样便将Timer和View进行了分隔,仍旧搭建了完整业务链条,并且在View被释放后,自动将Timer也失效,将所有资源全部释放,完美的处理了循环引用的问题。
























![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)