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."); }
|