常驻线程是一种什么体验

简说 RunLoop

关于 iOS RunLoop 网上很多文章都有介绍过, 很多技术面试官也会问关于 RunLoop 的相关知识. 我把自己工作中遇到的问题和总结的经验分享出来(会做成一系列的文章), 也算是对自己的一个总结和沉淀, 欢迎大家交流.

网上的文章基本都是针对于 Apple Developer Doc - Run Loops 这篇来展开的, 所以建议大家认真的去通读这篇文章, 并写代码验证, 实践.

可以简单粗暴的这么理解一下 RunLoop, 基于事件驱动的死循环(由内核来调度和管理的), 在需要处理事情的时候就出来干点事, 否则休眠待命.
RunLoop 的核心是基于 machport 的,其进入休眠时调用的函数是 mach_msg().

类似下面的代码来说明一下:

1
2
3
4
5
6
7
8
BOOL stopRunning = NO;
do {
// 处理 App 中各种操作各种事件
// 点击屏幕, 触摸到硬件也会唤醒 RunLoop
} while(!stopRunning);

说到这里, 随便提及一下, 学习过 Android 开发的同事应该和好理解 RunLoop 了, iOS 的 RunLoop 跟 Android 的 Looper 机制几乎一样, 只是不同的系统之间实现有差异罢了!

有兴趣的朋友可以看一下我之前写的文章 Handler: 更新UI的方法.


今天跟大家分享如何在 iOS 中结合 RunLoop 和 machport 实现常驻线程, 先跟着实例走, 后续再去总结 RunLoop 的各种细节点.

神奇的 main

开发过 iOS 应用中的朋友, 对 main.m 再也熟悉不过了, main 函数正是应用的入口函数.

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

我们将 return 代码分开写, 看看有什么蛛丝马迹可寻.

1
2
3
4
5
6
7
8
9
10
11
int main(int argc, char * argv[]) {
@autoreleasepool {
int ret = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"veryitman--- UIApplicationMain end.");
return ret;
}
}

无论如何你也看不到日志 veryitman--- UIApplicationMain end. 的打印, 这说明 UIApplicationMain 一直在呵护着 APP 的运行, 哈哈.

我们不妨再改一次, 如下:

1
2
3
4
5
6
int main(int argc, char * argv[]) {
@autoreleasepool {
return 0;
}
}

再去运行 APP, 你会发现根本没有让 APP 运行起来, 再次说明没有了 UIApplicationMain 的呵护, APP 无法起死回生.

猜测在 UIApplicationMain 函数中,开启了和主线程相关的 RunLoop,使 UIApplicationMain 不会返回一直在运行中,从而保证了程序的持续运行, 最大的功臣就是 RunLoop.

普通线程

一般我们开启的线程在执行完任务后, 就会结束该线程. 除非你写了类似下面的代码:

1
2
3
while(1) {
// 业务处理
}

或者

1
2
3
while (条件满足) {
// 业务处理
}

开启一个线程

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;
}

执行对应的 asyncRun 函数, 如下:

1
2
3
4
5
6
7
8
9
10
11
12
- (void)asyncRun {
@autoreleasepool {
NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);
// 执行其他逻辑
//...
NSLog(@"veryitman--asyncRun. End Run.");
}
}

可以发现 asyncRun 很快就可以执行完成 (End Run).

1
2
veryitman--asyncRun. Current Thread: <NSThread: 0x60000066c400>{number = 3, name = veryitman-thread}
veryitman--asyncRun. End Run.

子线程开启 RunLoop

主线程是默认开启 RunLoop 的即 mainRunLoop 是系统默认开启的, 但是子线程中的 RunLoop 需要我们自己手动开启.

关于为什么子线程中需要手动开启, 后续文章结合源码给大家分析, 这里暂时可以理解为获取 RunLoop 对象是一种懒加载模式. 只不过主线程中, 系统帮我们开启了, 然而子线程中需要我们手动开启而已.

类似这样:

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

在控制台可以看到输出:

1

RunLoop 没有任何输入源(input source) 和定时器(timer), 这时即使开启了 RunLoop 也不会让其等待执行, 换句话说会立即结束当前的 RunLoop.

既然这样我们给子线程的 RunLoop 添加源或者定时器即可. 这里以添加 NSPort 为例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)asyncRun {
@autoreleasepool {
NSLog(@"veryitman--asyncRun. Current Thread: %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 添加 source
NSPort *port = [NSMachPort port];
[runLoop addPort:port forMode:NSRunLoopCommonModes];
NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);
// 执行其他逻辑
//...
// 手动开启 RunLoop
[runLoop run];
NSLog(@"veryitman--asyncRun. End Run.");
}
}

再次运行, 你会发现 End Run 这个 Log 是不会打印出来的. 对应当前的 RunLoop 也有了源和定时器, 如图所示:
1

关于定时器和 RunLoop 的结合, 下篇再分享.

现在有这样一个需求, 需要在指定的线程中执行某项任务, 显然使用上面的方法来满足需求, 下面进入今天的正题.

验证常驻线程

一定到 常驻 这个词, 就知道是能够让该线程随时待命, 保证其不挂掉.

iOS 中默认就有个主线程即 main 线程, 我们的 UI 线程指的就是主线程, 一般都是在主线程中操作 UI, 从某个角度来说, 主线程就是一个常驻线程.

我们开启其他线程, 目的是为了异步完成一些任务, 这些任务一般都比较耗时, 如果放在主线程当中完成这些任务就会导致主线程的卡顿, 用户体验极其差.

说了这么多, 也许你会问, 为什么要常驻线程呢?

频繁的创建和销毁线程,会造成资源(主要是内存)的浪费, 我们为什么不让频繁使用的子线程常驻在内存中, 想用的时候就用, 不用的时候让他休眠呢?!

上面已经使用 RunLoop 来实现了让线程长时间存活而不被销毁了.

touchesBegan 来模拟在指定线程中再次执行任务(runAnyTime)的方法.

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

对应上面的 asyncRun 实现即可, 你会发现在当前页面每次点击屏幕都会执行 runAnyTime.

附录

代码的完整实现

常驻线程, 可以参考具体的注释.

MZCreatePermanentThreadController.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
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
@implementation MZCreatePermanentThreadController
- (void)dealloc {
NSLog(@"veryitman---MZCreatePermanentThreadController dealloc.");
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor lightGrayColor];
self.navigationItem.title = @"创建常驻线程";
// 启动线程
[self permanentThread];
}
- (void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
// 取消线程
// 实际业务场景中自行决定 canCancel 的设置, 这里只是示例
BOOL canCancel = YES;
if (canCancel) {
[[self permanentThread] cancel];
}
}
- (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];
// 添加 source
NSPort *port = [NSMachPort port];
[runLoop addPort:port forMode:NSRunLoopCommonModes];
NSLog(@"veryitman--asyncRun. Current RunLoop: %@", runLoop);
// 执行其他逻辑
//...
// 手动开启 RunLoop
[runLoop run];
NSLog(@"veryitman--asyncRun. End Run.");
}
}
- (void)runAnyTime {
NSLog(@"veryitman--runAnyTime. Current Thread: %@", [NSThread currentThread]);
}

参考文档

Toll-Free Bridging

Run Loops

坚持原创技术分享!