0%

iOS定时器

NSTimer如何使用

方法1: Creates a timer and schedules it on the current run loop in the default mode.

1
2
3
4
5
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti 
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo;

方法2: Initializes a timer object with the specified object and selector.

You must add the new timer to a run loop, using addTimer:forMode:. Then, after ti seconds have elapsed, the timer fires, sending the message aSelector to target. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)

1
2
3
4
5
6
7
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti 
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)yesOrNo;

[[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

这两个方法是等价的,区别是第一个方法默认创建了一个NSTimer并自动添加到了当前线程的Runloop中去,第二个需要我们手动添加。如果当前线程是主线程的话,某些UI事件,比如UIScrollView的拖拽操作,会将Runloop切换成UITrackingRunLoopMode,这时候,默认的NSDefaultRunLoopMode模式中注册的事件是不会被执行的。所以为了设置一个不会被UI干扰的Timer,我们需要手动将timer的当前RunloopMode设置为NSRunLoopCommonModes,这个模式等效于NSDefaultRunLoopMode和UITrackingRunLoopMode的结合。

由于NSTimer会强引用Target,RunLoop会强引用NSTimer,若ViewController再强引用NSTimer,NSTimer找不到合适的时机释放的时候,很容易造成内存泄漏,这个问题如何解决?

如何解决NSTimer找不到释放时机的问题?

方案1,使用官方API带Block的方式来创建NSTimer

具体的创建方法如下,但是仅支持iOS 10以上的系统。

1
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

在ViewController中的使用方法:

1
2
3
4
5
6
__weak typeof(self)weakSelf = self;
self.timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"第%ld次执行", (long)weakSelf.count);
weakSelf.count ++;
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

方案2,使用Block的方式来创建NSTimer

具体实现方式是,需要创建一个NSTimer的分类,让外部传入一个Block,NSTimer每次到时间就执行传入进来的Block。新建一个分类:
NSTimer+BlockSupport.h

1
2
3
4
5
6
7
8
9
10
11
12
13
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSTimer (BlockSupport)

+ (NSTimer *)cd_timerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void(^)(void))block;

@end

NS_ASSUME_NONNULL_END

NSTimer+BlockSupport.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#import "NSTimer+BlockSupport.h"

@implementation NSTimer (BlockSupport)

+ (NSTimer *)cd_timerWithTimeInterval:(NSTimeInterval)interval
repeats:(BOOL)repeats
block:(void(^)(void))block {
return [self scheduledTimerWithTimeInterval:interval
target:self
selector:@selector(cd_blockInvoke:)
userInfo:[block copy]
repeats:repeats];
}

+ (void)cd_blockInvoke:(NSTimer *)timer {
void (^block)(void) = timer.userInfo;
if(block) {
block();
}
}

@end

在ViewController中使用方法:

1
2
3
4
5
6
__weak typeof(self)weakSelf = self;
self.timer = [NSTimer cd_timerWithTimeInterval:1.0 repeats:YES block:^{
NSLog(@"第%ld次执行", (long)weakSelf.count);
weakSelf.count ++;
}];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

方案3,自定义NSTimer的Target

封装自定义NSTimer的Target和NSTimer的工具类,让NSTimer在合适的时机释放。
CDTimerTarget.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CDTimerTarget : NSObject

// 目标对象
@property (nonatomic,weak) id target;
// 目标方法
@property (nonatomic,assign) SEL selector;

@end

NS_ASSUME_NONNULL_END

CDTimerTarget.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#import "CDTimerTarget.h"

@implementation CDTimerTarget

- (void)fire:(NSTimer *)timer {
// target为弱引用,所以当target对象销毁时,target 为nil
if (self.target) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self.target performSelector:self.selector withObject:timer.userInfo];
#pragma clang diagnostic pop
}else {
[timer invalidate];
}
}

@end

CDTimer.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CDTimer : NSObject

+ (NSTimer *)scheduleTimerWithTimerInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats;

@end

NS_ASSUME_NONNULL_END

CDTimer.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#import "CDTimer.h"
#import "CDTimerTarget.h"

@implementation CDTimer

+ (NSTimer *)scheduleTimerWithTimerInterval:(NSTimeInterval)interval
target:(id)aTarget
selector:(SEL)aSelector
userInfo:(id)userInfo
repeats:(BOOL)repeats {

CDTimerTarget *timerTarget = [[CDTimerTarget alloc]init];
timerTarget.target = aTarget;
timerTarget.selector = aSelector;
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:interval
target:timerTarget
selector:@selector(fire:)
userInfo:userInfo
repeats:repeats];
return timer;
}

@end

在ViewController中的用法:

1
self.timer = [CDTimer scheduleTimerWithTimerInterval:1.0 target:self selector:@selector(repeatAction) userInfo:nil repeats:YES];

方案4,自定义NSProxy子类,作为NSTimer的Target

TargetProxy.h

1
2
3
4
5
6
7
8
9
10
11
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface TargetProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@end

NS_ASSUME_NONNULL_END

TargetProxy.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#import "TargetProxy.h"

@interface TargetProxy()

//target, 这里必须要用weak, 因为某个对象会强引TimeProxy对象, TimeProxy对象不能再强引target, 否则会形成循环引用
@property (nonatomic, weak)id target;

@end

@implementation TargetProxy

+ (instancetype)proxyWithTarget:(id)target {
TargetProxy *proxy = [[self class] alloc];
proxy.target = target;
return proxy;
}

//获取target类中的sel方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation {
//判断target是否实现了该方法
if ([self.target respondsToSelector:invocation.selector]) {
//让target调用该方法
[invocation invokeWithTarget:self.target];
}else {
//找不到该方法
[invocation doesNotRecognizeSelector:invocation.selector];
}
}

@end

在ViewController中的使用方法:

1
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[TargetProxy proxyWithTarget:self] selector:@selector(repeatAction) userInfo:nil repeats:YES];

参考链接

NSTimer

NSProxy解决NSTimer和CADisplayLink的循环引用

《Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》