NSTimer的使用问题
用NSTimer
做计时器循环事件的时候,很有可能会遇到以下两个问题:
- 正常启动的
timer
在滚动视图滚动的时候不能够接收事件消息了- 当前引用
timer
的类不能够得到释放,进而造成内存泄露的问题
所以针对于以上问题,进行记录与说明。
产生原因以及解决方法
正常启动的
timer
在滚动视图滚动的时候不能够接收事件消息了
因为系统的timer
记时器是通过iOS中的Runloop
实现的,每一个定时器timer
的实例都需要加入到Runloop
中才能够有效,由于Runloop
有五种模式,分别是NSDefaultRunLoopMode、NSEventTrackingRunLoopMode、NSModalPaneRunLoopMode、NSTrackingRunLoopMode、NSRunLoopCommonModes
。
这五种模式会在Runloop
的不同的场景下进行来回切换,而定时器timer
如果没有加入到切换对应的场景mode
中,则就会导致当前的mode
中不存在加入的timer
,也就会引发timer
接收不到定时器消息的问题。本质是runloop
因为切换mode
,且对应mode
中没有当前的timer
对象,在当前的mode
中,导致timer
收不到事件消息的问题。
解决方法其实很简单,在创建定时器的时候,将定时器加入到runloop
的不同的mode
中,这样就能确保runloop
在切换mode
的时候能够找到对应mode
中的定时器,也就能够发送定时器消息以保证定时器回调事件的正常了。
//注意,以下的方法会导致循环引用的发生,直接导致timer释放不掉,解决方案在第二个问题记录中
- (void)normalTimer{
self.timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
}
- (void)cycleTimer{
self.timer =
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
}
- (void)timerEvent{
NSLog(@"timer事件--%s",__func__);
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
当前引用
timer
的类不能够得到释放,进而造成内存泄露的问题
以上的定时器timer
虽然能够在Runloop
的各种mode
中完美运行,但是会导致当前的对象与timer
相互引用导致循环引用问题的产生。总结来说就是:
由于定时器timer
被当前的对象引用,而启动定时器的时候,又将当前对象作为参数传入到定时器中,二者相互引用导致循环引用的产生。如下图:
这里说一种错误的解决方法:将self
改成weak
类型后依旧会有循环引用,原因是修改weak
属性只对block
有效,对于timer
对象的内部Target
的strong
引用是没有效果的。
本质是循环引用导致的内存泄露,所以在相互引用上解除引用才是解决的根本。这里有两种方案去解决这样的问题:
- 如果是iOS10以上,我们可以直接使用
timer
的scheduledTimerWithTimeInterval:repeats:block:
方法进行设置
- (void)timerBlock{
if (__builtin_available(iOS 10, *)) {
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%s",__func__);
}];
} else {}
}
引入对象C来解除相互引入问题
- 可以引入新的对象C,将引用链由
A->B,B->A
改成A->C, C->B, B->A
引入新对象C
之后,三者引用关系就如上图,这样就不存在两个对象之间相互引用了,在销毁对象的时候,只需要消除其中一条引用,则可以全部消除引用关系。比如ObjectA
在销毁前,可以向Timer
发送invalidate
消息,消除对于ObjectC
的引用,这样就消除了一个引用关系,过程如下:
- 调用
timer
的invidate
方法结束定时器对对象C
的引用,让引入的新对象C
先dealloc
- 引入的新
对象C
的释放,结束了对于对象A
的引用,当前对象A
也紧接着dealloc
了- 当前
对象A
的释放,结束了对于定时器B
的引用,定时器对象B
也紧接着dealloc了
基于上述的问题,我们可以封装一个解除timer
引用的临时对象,对象的内容实现如下:
LCSafeObj.h
//
// LCSafeObj.h
// Timer
//
// Created by Leo on 2020/12/1.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface LCSafeObj : NSObject
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat obj:(id)object;
@end
NS_ASSUME_NONNULL_END
LCSafeObj.m
//
// LCSafeObj.m
// Timer
//
// Created by Leo on 2020/12/1.
//
#import "LCSafeObj.h"
@interface LCSafeObj ()
@property (nonatomic, strong) id target;
@property (nonatomic, assign) SEL selecter;
@end
@implementation LCSafeObj
- (instancetype)initWithTarget:(id)target selecter:(SEL)selecter{
if (self = [super init]) {
self.target = target;
self.selecter = selecter;
}
return self;
}
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat obj:(id)object{
//此时LCSafeObj单独引用外部对象
LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter];
//注意这里的Target传入的是LCSafeObj类型的,并不是外部对象,目的是让定时器timer引用新引入的对象C,
NSTimer *timer =
[NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:selecter userInfo:object repeats:repeat];
//返回给传入的对象,让其单引用定时器timer,且控制定时器的invalid的时间,至此完成单链的引用
return timer;
}
/// 使用消息转发来将SafeObj中没有的方法调用转移到传入的对象中
/// @param aSelector 方法转发
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == self.selecter) {
return self.target;
}
return [super forwardingTargetForSelector:aSelector];
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
@end
这里实现的过程中注意用到了运行时的消息转发机制
,以确保传入对象的正确方法调用,以及代码的简洁。
优化内容
上述的方法存在一些瑕疵,就是使用的时候可能还是需要在当前使用的类中去手动invalidDate timer计时器才能够将三者释放掉,这样在开发的过程中也是比较繁琐的,可以考虑将释放工作放到引入的三方对象C中,具体做法参考如下:
//
// LCSafeObj.m
// Timer
//
// Created by Leo on 2020/12/1.
//
#import "LCSafeObj.h"
@interface LCSafeObj ()
@property (nonatomic, weak) NSTimer *timer;
@property (nonatomic, weak) id target;
@property (nonatomic, assign) SEL selecter;
@property (nonatomic, copy) void (^timerEventBlock)(void);
@end
@implementation LCSafeObj
- (instancetype)initWithTarget:(id)target selecter:(SEL)selecter timerEventBlock:(void (^)(void))timerEventBlock{
if (self = [super init]) {
self.target = target;
self.selecter = selecter;
self.timerEventBlock = timerEventBlock;
}
return self;
}
- (void)dealloc{
NSLog(@"%s",__func__);
}
- (void)setTimer:(NSTimer *)timer{
_timer = timer;
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval target:(id)target selecter:(SEL)selecter isrepeat:(BOOL)repeat{
//此时LCSafeObj单独引用外部对象
LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:target selecter:selecter timerEventBlock:nil];
//注意这里的Target传入的是LCSafeObj类型的,并不是外部对象,目的是让定时器timer引用新引入的对象C,
safeObj.timer =
[NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
//返回给传入的对象,让其单引用定时器timer,且控制定时器的invalid的时间,至此完成单链的引用
return safeObj.timer;
}
- (void)targetAction{
if (!self.target) {
[self.timer invalidate];
}
if (self.target && [self.target respondsToSelector:self.selecter]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selecter];
#pragma clang diagnostic pop
}
if (self.timerEventBlock) {self.timerEventBlock();}
}
+ (NSTimer *)addTimerInterval:(NSTimeInterval)interval isRepeat:(BOOL)repeat eventBlock:(void (^)(void))eventBlock{
//此时LCSafeObj单独引用外部对象
LCSafeObj *safeObj = [[LCSafeObj alloc] initWithTarget:nil selecter:nil timerEventBlock:eventBlock];
//注意这里的Target传入的是LCSafeObj类型的,并不是外部对象,目的是让定时器timer引用新引入的对象C,
safeObj.timer =
[NSTimer scheduledTimerWithTimeInterval:interval target:safeObj selector:@selector(targetAction) userInfo:nil repeats:repeat];
//返回给传入的对象,让其单引用定时器timer,且控制定时器的invalid的时间,至此完成单链的引用
return safeObj.timer;
}
@end
优化方案两个要点:
- 对外部target的引用采取weak弱引用,以保证外部对象的正常释放
@property (nonatomic, weak) id target;
- 定时器事件方法中判断target引用是否依旧存在,不存在则使用invalidDate 去除定时器timer对于引入的对象C的引用
- (void)targetAction{
if (!self.target) {
[self.timer invalidate];
}
if (self.target && [self.target respondsToSelector:self.selecter]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selecter];
#pragma clang diagnostic pop
}
if (self.timerEventBlock) {self.timerEventBlock();}
}
网友评论