RunLoop: NSTimer 实现常驻线程的问题

可行性

常驻线程是一种什么体验 这篇文章中给大家分享了如何利用 RunLoop 的特性, 结合 NSMachPort 实现一个 常驻线程 的主题内容.

今天我们探讨一下使用 NSTimer 如何实现 常驻线程 以及注意事项.

从 RunLoop 的特性来看, 只要有 Source 或者 Timer 都会使其能自循环使用, 不会立即终止当前线程的执行, 所以从理论上来看 NSTimer 是可以达到创建 常驻线程 的目的的.

开始实践

完整的例子代码, 可以从文章的附录获取和查看, 这里只给出核心代码.

创建线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSThread *)permanentThread {
static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
thread = [[NSThread alloc] initWithTarget:self selector:@selector(asyncRun) object:nil];
[thread setName:@"veryitman-thread"];
// 同一个线程连续多次 start 会导致 crash
[thread start];
});
return thread;
}

线程执行的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)asyncRun {
@autoreleasepool {
NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 保持常驻线程: 使用 NSTimer
[self _attachTimerToRunLoop];
NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);
// 执行其他逻辑
//...
// 手动开启 RunLoop
[runLoop run];
NSLog(@"veryitman--asyncRun. End Run.");
}
}

创建定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)_attachTimerToRunLoop {
if (nil == self.timer) {
// scheduledTimerWithTimeInterval 这种方式
// 创建的 Timer 会默认加入到当前的 RunLoop 的 NSDefaultRunLoopMode 中
_timer = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:YES];
}
}

可以看出, _attachTimerToRunLoop 中是将 timer 加入到当前的 RunLoop 当中了. 这里注意, repeats 值被设置为 YES 了.

跟之前一样, 可以使用点击事件来模拟和验证常驻线程的有效性.

1
2
3
4
5
6
7
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");
[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

每点击一次屏幕, 都会对应执行 runAnyTime 里面的内容.

对 repeat 的思考

在上面的示例中, 我将 repeat 参数设置为 YES, 试想一下如果把 repeat 参数设置为 NO, 会不会造成常驻线程失效呢?

动手试试…

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)_attachTimerToRunLoop {
if (nil == self.timer) {
// scheduledTimerWithTimeInterval 这种方式
// 创建的 Timer 会默认加入到当前的 RunLoop 的 NSDefaultRunLoopMode 中
_timer = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:NO];
}
}

再次点击屏幕若干次, 同样会执行对应函数里面的内容. 这就说明了即使将 repeat 参数设置为 NO, 也不会影响常驻线程.

那我们再来点具有挑战的活动…

将当前页面加入 UIScrollview 这个视图, 还是保持 repeat 参数设置为 NO.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.navigationItem.title = @"NSTimer 创建常驻线程";
// 加入滚动视图
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.scrollView];
self.scrollView.contentSize = CGSizeMake(1000, 1000);
self.scrollView.delegate = self;
// 启动线程
[self permanentThread];
}

因为加入了滚动视图, 我们换一种方式来模式和验证常驻线程.

UIScrollview 代理中来模拟, 示例如下:

1
2
3
4
5
6
7
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");
[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}

运行后进入该页面, 可以发现常驻线程被终止了.

1
2
veryitman--timerRun.
veryitman--asyncRun. End Run.

当除我以为更换一下模式即使 将 repeat 参数设置为 NO, 也不会出现常驻线程被终止的问题. 如下面的代码:

1
2
_timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

这样更换模式为 NSRunLoopCommonModes 也不行.

在这种情况(有滚动视图的)下, 将 repeat 参数设置为 YES 就不会导致常驻线程被终止了, 无论哪种方式创建的 Timer.

总结

1.子线程创建中的 RunLoop 的模式不会与主线程中 RunLoop 的模式冲突, 各自运行在各自的 mode 当中.

2.使用 NSTimer 来创建常驻线程, 在有 UIScrollview 或者其子类的情况下, 需要将 repeats 设置为 YES, 否则不会创建常驻线程. 没有滚动视图的情况下, repeats 设置为 NO 也没有关系.

3.创建 NSTimer

下面两种创建 Timer 的效果是一致的.

1
2
3
4
5
[NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:YES];

scheduledTimerWithTimeInterval 默认会将 Timer 加入到当前的 RunLoop 中.

1
2
[NSTimer timerWithTimeInterval:2 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

附录

完整示例代码

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
#import "MZTimerPermanentThreadController.h"
@interface MZTimerPermanentThreadController () <UIScrollViewDelegate>
@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) UIScrollView *scrollView;
@end
@implementation MZTimerPermanentThreadController
- (void)dealloc {
NSLog(@"veryitman---MZTimerPermanentThreadController dealloc.");
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.navigationItem.title = @"NSTimer 创建常驻线程";
// 加入滚动视图
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:self.scrollView];
self.scrollView.contentSize = CGSizeMake(1000, 1000);
self.scrollView.delegate = self;
// 启动线程
[self permanentThread];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 取消线程
// 实际业务场景中自行决定 canCancel 的设置, 这里只是示例
BOOL canCancel = YES;
if (canCancel) {
[self.timer invalidate];
_timer = nil;
[[self permanentThread] cancel];
}
}
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {
// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");
[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 模拟在指定线程上面再次执行方法
SEL seltor = NSSelectorFromString(@"runAnyTime");
[self performSelector:seltor onThread:[self permanentThread] withObject:nil waitUntilDone:NO];
}
- (NSThread *)permanentThread {
static NSThread *thread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
thread = [[NSThread alloc] initWithTarget:self selector:@selector(asyncRun) object:nil];
[thread setName:@"veryitman-thread"];
// 同一个线程连续多次 start 会导致 crash
[thread start];
});
return thread;
}
- (void)asyncRun {
@autoreleasepool {
NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 保持常驻线程: 使用 NSTimer
[self _attachTimerToRunLoop];
NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);
// 执行其他逻辑
//...
// 手动开启 RunLoop
[runLoop run];
NSLog(@"veryitman--asyncRun. End Run.");
}
}
- (void)runAnyTime {
NSLog(@"veryitman--runAnyTime. Current Thread: %@", [NSThread currentThread]);
}
- (void)_attachTimerToRunLoop {
if (nil == self.timer) {
// scheduledTimerWithTimeInterval 这种方式
// 创建的 Timer 会默认加入到当前的 RunLoop 的 NSDefaultRunLoopMode 中
_timer = [NSTimer scheduledTimerWithTimeInterval:2
target:self
selector:@selector(runTimer)
userInfo:nil
repeats:YES];
#if 0
_timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
#endif
}
}
- (void)runTimer {
NSLog(@"--veryitman--timerRun.");
}
坚持原创技术分享!