我们都知道Timer只有在invalidate之后才会从runloop中被移除,repeat为NO的timer在执行一次之后就会自动移除;我们在使用重复的timer的时候,如果是想在dealloc中调用timer的invalidate方法,往往会造成泄漏,target的dealloc方法不会调用,放在界面
viewWillAppear
创建和viewWillDisappear
的时候invalidate,很多场景也不适用(eg: targetVC 跳转到另外界面再回来)
那我们使用weak来能否解决timer释放的问题了?
尝试1:使用weak修饰timer实例
@property (weak, nonatomic) NSTimer *testRepeatTimer;
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
self.testRepeatTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:self
selector:@selector(testTimerAction)
userInfo:nil
repeats:YES];
}
- (void)dealloc {
[_testRepeatTimer invalidate];
}
调试发现,dealloc方法也不会调用;为什么了?先看看函数的注释
- -(void)invalidate
Summary
Stops the timer from ever firing again and requests its removal from its run loop.
Declaration
- (void)invalidate;
Discussion
This method is the only way to remove a timer from an NSRunLoop object. The NSRunLoop object removes its strong reference to the timer, either just before the invalidate method returns or at some later point.
If it was configured with target and user info objects, the receiver removes its strong references to those objects as well.
从这里注释可知:runloop是强持有timer的,声明为weak只是vc不持有
再看看invalidate内部做了什么处理:
/**
* Marks the timer as invalid, causing its target/invocation and user info
* objects to be released.
* Invalidated timers are automatically removed from the run loop when it
* detects them.
*/
- (void) invalidate
{
/* OPENSTEP allows this method to be called multiple times. */
_invalidated = YES;
if (_target != nil)
{
DESTROY(_target);
}
if (_info != nil)
{
DESTROY(_info);
}
}
invalidate内部是将强持有的target和info进行释放,并且标记timer的invalidated为YES,runloop在检测到timer被invalidate之后会自动从runloop中移除
DESTROY的实现没有找到,不过在GUN的源码中发现类似的定义,作用就是release然后置为nil
/* Destroy holder for internal ivars.
*/
#define GS_DESTROY_INTERNAL(name) \
if (nil != _internal) { [_internal release]; _internal = nil; }
- +(NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
Summary
Creates a timer and schedules it on the current run loop in the default mode.
Declaration
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;
Discussion
After ti seconds have elapsed, the timer fires, sending the message aSelector to target.
Parameters
ti
The number of seconds between firings of the timer. If ti is less than or equal to 0.0, this method chooses the nonnegative value of 0.1 milliseconds instead.
target
The object to which to send the message specified by aSelector when the timer fires. The timer maintains a strong reference to target until it (the timer) is invalidated.
aSelector
The message to send to target when the timer fires.
The selector should have the following signature: timerFireMethod: (including a colon to indicate that the method takes an argument). The timer passes itself as the argument, thus the method would adopt the following pattern:
- (void)timerFireMethod:(NSTimer *)timer
userInfo
The user info for the timer. The timer maintains a strong reference to this object until it (the timer) is invalidated. This parameter may be nil.
repeats
If YES, the timer will repeatedly reschedule itself until invalidated. If NO, the timer will be invalidated after it fires.
Returns
A new NSTimer object, configured according to the specified parameters.
Open in Developer Documentation
从这段注释可知:timer是强持有target的直到timer invalidate
通过源码看看内部是如何实现的:
/**
* Create a timer which will fire after ti seconds and, if f is YES,
* every ti seconds thereafter. On firing, invocation will be performed.
* This timer will automatically be added to the current run loop and
* will fire in the default run loop mode.
*/
+ (NSTimer*) scheduledTimerWithTimeInterval: (NSTimeInterval)ti
invocation: (NSInvocation*)invocation
repeats: (BOOL)f
{
id t = [[self alloc] initWithFireDate: nil
interval: ti
target: invocation
selector: NULL
userInfo: nil
repeats: f];
[[NSRunLoop currentRunLoop] addTimer: t forMode: NSDefaultRunLoopMode];
RELEASE(t);
return t;
}
/**
* Initialise the receive, a newly allocated NSTimer object.
* The ti argument specifies the time (in seconds) between the firing.
* If it is less than or equal to 0.0 then a small interval is chosen
* automatically.
* The fd argument specifies an initial fire date copied by the timer...
* if it is not supplied (a nil object) then the ti argument is used to
* create a start date relative to the current time.
* The f argument specifies whether the timer will fire repeatedly
* or just once.
* If the selector argument is zero, then then object is an invocation
* to be used when the timer fires. otherwise, the object is sent the
* message specified by the selector and with the timer as an argument.
* The object and info arguments will be retained until the timer is
* invalidated.
*/
- (id) initWithFireDate: (NSDate*)fd
interval: (NSTimeInterval)ti
target: (id)object
selector: (SEL)selector
userInfo: (id)info
repeats: (BOOL)f
{
if (ti <= 0.0)
{
ti = 0.0001;
}
if (fd == nil)
{
_date = [[NSDate_class allocWithZone: NSDefaultMallocZone()]
initWithTimeIntervalSinceNow: ti];
}
else
{
_date = [fd copyWithZone: NSDefaultMallocZone()];
}
_target = RETAIN(object);
_selector = selector;
_info = RETAIN(info);
if (f == YES)
{
_repeats = YES;
_interval = ti;
}
else
{
_repeats = NO;
_interval = 0.0;
}
return self;
}
- (void) addTimer: (NSTimer*)timer
forMode: (NSString*)mode
{
GSRunLoopCtxt *context;
GSIArray timers;
unsigned i;
if ([timer isKindOfClass: [NSTimer class]] == NO
|| [timer isProxy] == YES)
{
[NSException raise: NSInvalidArgumentException
format: @"[%@-%@] not a valid timer",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
if ([mode isKindOfClass: [NSString class]] == NO)
{
[NSException raise: NSInvalidArgumentException
format: @"[%@-%@] not a valid mode",
NSStringFromClass([self class]), NSStringFromSelector(_cmd)];
}
NSDebugMLLog(@"NSRunLoop", @"add timer for %f in %@",
[[timer fireDate] timeIntervalSinceReferenceDate], mode);
context = NSMapGet(_contextMap, mode);
if (context == nil)
{
context = [[GSRunLoopCtxt alloc] initWithMode: mode extra: _extra];
NSMapInsert(_contextMap, context->mode, context);
RELEASE(context);
}
timers = context->timers;
i = GSIArrayCount(timers);
while (i-- > 0)
{
if (timer == GSIArrayItemAtIndex(timers, i).obj)
{
return; /* Timer already present */
}
}
/*
* NB. A previous version of the timer code maintained an ordered
* array on the theory that we could improve performance by only
* checking the first few timers (up to the first one whose fire
* date is in the future) each time -limitDateForMode: is called.
* The problem with this was that it's possible for one timer to
* be added in multiple modes (or to different run loops) and for
* a repeated timer this could mean that the firing of the timer
* in one mode/loop adjusts its date ... without changing the
* ordering of the timers in the other modes/loops which contain
* the timer. When the ordering of timers in an array was broken
* we could get delays in processing timeouts, so we reverted to
* simply having timers in an unordered array and checking them
* all each time -limitDateForMode: is called.
*/
GSIArrayAddItem(timers, (GSIArrayItem)((id)timer));
i = GSIArrayCount(timers);
if (i % 1000 == 0 && i > context->maxTimers)
{
context->maxTimers = i;
NSLog(@"WARNING ... there are %u timers scheduled in mode %@ of %@",
i, mode, self);
}
}
可以看到在初始化timer的时候,内部会对target和info进行retain操作;将timer添加到runloop中的时候会将timer添加到数组中也进行了retain操作
由于runloop是强持有timer的,并且只有在调用了invalidate的时候才会将repeat的timer移除,同时timer强持有了target,导致target不会被释放,那么timer的invalidate调用放到target的dealloc中也无济于事;所以只有当我们调用了invalidate之后,NSTimer内部持有的target和info会被释放,runloop检测到timer被置为invalidated之后将timer移除,这样就能正常释放了
尝试2:使用weakSelf初始化timer
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
__weak typeof(self) weakSelf = self;
self.testRepeatTimer = [NSTimer scheduledTimerWithTimeInterval:1
target:weakSelf
selector:@selector(testTimerAction)
userInfo:nil
repeats:YES];
}
- (void)dealloc {
[_testRepeatTimer invalidate];
}
调试发现,dealloc方法也不会调用;原因也同上,虽然target参数传递的是weak指针,但是timer还是强持有weak指针指向的对象,同样导致无法释放
如何解决
问题的根本:
- timer强持有target
- runloop强持有timer
- repeat的timer只有在调用了invalidate之后才会被runloop释放
- 为了timer和target的生命周期一致,我们在target的dealloc中invalidate timer
target被强持有了,不会走dealloc,就内存泄漏了
我们发现只要能使得timer不强持有target(eg:例子中的vc),那么target的dealloc就能正常执行,timer invalidate之后,timer就会被释放了,内存泄漏解决;我们需要一个中间者能够将timer的selector转发给target,同时让timer去持有这个中间者,那么问题就解决了
OC中刚好有这样一个类NSProxy
可以完成这个工作
@interface NSProxy {
Class isa;
}
+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
// - (id)forwardingTargetForSelector:(SEL)aSelector;
@end
我们子类化NSProxy,内部weak引用target,在消息转发的时候调用target去执行;这样timer持有的是proxy,target能正常释放,target释放了,timer就释放了、proxy也会释放,内存泄漏问题解决;
具体的实现可以参照YYWeakProxy,就不重复造轮子了;使用起来也比较简单
self.testRepeatTimer = [NSTimer timerWithTimeInterval:1.0 target:[YYWeakProxy proxyWithTarget:self] selector:@selector(testTimerAction) userInfo:nil repeats:YES];