RunLoop: 移除常驻线程

概要

下面两篇文章:

都是在 iOS 平台上如何利用 RunLoop 的特性实现常驻线程.

可能你看到本篇文章的标题 移除常驻线程, 有点不理解甚至觉得作者是不是有点神经病, 都已经需要常驻线程了, 为什么还要去退出呢?

实际应用场景中的确几乎遇不到这种情况, 所以, 本篇只是从技术的角度给大家分享 RunLoop 其他的一些知识点.

且耐住性子往下看…

为了不让大家误解, 统一一下相关的词汇和语境.

  • 输入源, 包括 source 和 timer.
  • 退出 RunLoop 指的是在常驻线程的方法中立即返回.
  • 这里的 RunLoop 指的是子线程中的 RunLoop 不是主线程中的.
  • 结合之前介绍常驻线程的文章来看现在的文章, 不然不好理解.

运行 RunLoop 的方法

这里有三种方式开启运行 RunLoop, 分别如下:

1
2
3
4
5
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
  • run 方法

看一下 API 文档如何解释 run 方法的:

1
2
3
4
5
Puts the receiver into a permanent loop,
during which time it processes data from all attached input sources.
If no input sources or timers are attached to the run loop, this method exits immediately;
otherwise, it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.

大致意思讲的是, 在没有任何输入源的情况下, run 方法会立即执行后退出, 不会保持线程的持久性, 换句话说, 在有输入源的情况下, 该方法会进入一个无限循环当中. 本质上, 在 NSDefaultRunLoopMode 模式下, 该方法是反复调用 runMode:beforeDate: 方法的.

在之前文章实现常驻线程的代码中, 我们使用了 run 方法来开启运行 RunLoop.

可以看出, 我们之前实现的常驻线程使用 run 方法是无法退出常驻线程的.

  • runUntilDate:(NSDate *)limitDate 方法

该方法 API 释义:

1
2
Runs the loop until the specified date,
during which time it processes data from all attached input sources.

保证有输入源的情况下该方法启动的 RunLoop 可以在指定的日期内一直运行不会返回. 换句话说, 日期只要已到该方法就会立即退出 RunLoop.

示例代码:

1
2
3
4
5
6
7
8
//等同于 run, 可保证一直运行
[runLoop runUntilDate:[NSDate distantFuture]];
//立即返回, 退出 RunLoop
[runLoop runUntilDate:[NSDate date]];
//然后过12秒后返回
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:12.0f]];
  • runMode:(NSString )mode beforeDate:(NSDate )limitDate 方法

该方法有两个参数 modelimitDate, mode 就是 RunLoop 的运行模式, limitDate 就是上面方法一样的释义即在指定的日期内.

1
Runs the loop once, blocking for input in the specified mode until a given date.

可以这么理解, 该方法开启的 RunLoop 处理完之后会立即返回(once), 如果在指定日期内事件还没处理, 在该日期后会立即返回.
换句话说, 如果指定的日期是当前的, 该方法执行后 RunLoop 会立即退出.

示例代码:

1
2
3
4
5
//立即返回
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate date]];
//有事件到达处理后就返回,如果没有则过12秒返回
[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:12.0]];

这三个方法, 总结一下大概如下:

  • 这三个方法在没有任何输入源情况下会都立即返回(退出 RunLoop), 不会等待.
  • run 可保证 RunLoop 在有输入源的情况下一直运行.
  • runUntilDate 可以通过设置超时时间来退出 RunLoop. 超时时间一过就会立即退出 RunLoop.
  • 使用 runMode 方式启动的 RunLoop 会在处理完事件后或者超时后, 立即返回. 即可以通过设置超时时间或者使用 CFRunLoopStop 方法来退出 RunLoop.

移除常驻线程

先看一下, 在子线程执行的方法中如何实现的. 示例代码如下:

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
- (void)asyncRun {
@autoreleasepool {
NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
_threadRunLoop = runLoop;
// 保持常驻线程的方式1: source
NSPort *port = [NSMachPort port];
_threadPort = port;
[runLoop addPort:port forMode:NSRunLoopCommonModes];
NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);
// 执行其他逻辑
//...
// 手动开启 RunLoop
// [runLoop run];
while (!self.stopLoopRunning && [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantPast]]) {
// 这里是为了验证常驻线程是否已经退出
NSLog(@"--- asyncRun ----");
// 实际业务中, 建议使用空语句实现
; //实现为空语句
}
NSLog(@"veryitman--asyncRun. End Run.");
}
}

结合上面讲解的原理, 这里选择使用 runMode 方法来开启运行 RunLoop. 大家也可以自由发挥使用其他的方法.

注意: stopLoopRunning 是定义的一个属性.

移除的示例代码如下:

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
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 取消线程
// 实际业务场景中自行决定 canCancel 的设置, 这里只是示例
BOOL canCancel = YES;
if (canCancel) {
[[self permanentThread] cancel];
}
/// 停止常驻线程
{
self.stopLoopRunning = YES;
// 移除 port
// 如果是用 timer 的方式的常驻线程, 可以 invalid 对应的 timer
[self.threadRunLoop removePort:self.threadPort forMode:NSRunLoopCommonModes];
// 停止 RunLoop
if (nil != self.threadRunLoop) {
CFRunLoopStop([self.threadRunLoop getCFRunLoop]);
}
}
}

离开页面后, 可以发现 NSLog(@"--- asyncRun ----"); 停止了打印, 并且当前的 Controller 也 dealloc 了.


扫码关注,你我就各多一个朋友~

坚持原创技术分享!