RunLoop
# RunLoop 介绍
运行循环,在程序运行过程中,循环做一些事情。应用范畴:定时器(Timer)、performSelector、GCD Async Main Queue、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool 等等。
伪代码:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            int message = sleep_and_wait();
            retVal = process_message(message);
        } while (retVal == 0);
    }
    return 0;
}
2
3
4
5
6
7
8
9
10
有了 RunLoop 之后,程序不会马上退出,而是保持运行状态。RunLoop 的基本使用:
- 保持程序的持续运行
- 处理 App 中的各种事件(比如触摸事件、定时器事件等)
- 节省 CPU 资源,提高程序性能:该做事时做事,该休息时休息
# RunLoop 对象
iOS 中有 2 套 API 来访问和使用 RunLoop
- Foundation:NSRunLoop
- Core Foundation:CFRunLoopRef
NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop 对象,NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装。CFRunLoopRef 是开源的,https://opensource.apple.com/tarballs/CF/
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//获取主线程 runloop
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
2
3
4
5
6
# RunLoop 与线程
- 每条线程都有唯一的一个与之对应的 RunLoop 对象
- RunLoop 保存在一个全局的 Dictionary 里,线程作为 key,RunLoop 作为 value
- 线程刚创建时并没有 RunLoop 对象,RunLoop 会在第一次获取它时创建
- RunLoop 会在线程结束时销毁
- 主线程的 RunLoop 已经自动获取(创建),子线程默认没有开启 RunLoop
# RunLoop 实现
struct __CFRunLoop {
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    // ...
};
struct __CFRunLoopMode {
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    //...
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

CFRunLoopModeRef 代表 RunLoop 的运行模式,一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source0/Source1/Timer/Observer。RunLoop 启动时只能选择其中一个 Mode,作为 currentMode,如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入。不同组的 Source0/Source1/Timer/Observer 能分隔开来,互不影响。如果 Mode 里没有任何 Source0/Source1/Timer/Observer,RunLoop 会立马退出。
常见的 2 种 Mode:
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App 的默认 Mode,通常主线程是在这个 Mode 下运行 
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 
Source0:
- 触摸事件处理
- performSelector:onThread:
Source1:
- 基于 port 的线程间通信
- 系统事件捕捉
点击事件实际是通过 source1 捕捉,然后再包装成 source0 来处理的。(可以阅读《深入理解 iOS 事件机制》 (opens new window))
Timers:
- NSTimer
- performSelector:withObject:afterDelay:
Observers:
- 用于监听 RunLoop 的状态
- UI 刷新(BeforeWaiting)
- AutoreleasePool 处理(BeforeWaiting)
# RunLoop Observer
- (void)dealloc {
    if (runLoopObserver != NULL) {
        CFRunLoopRemoveObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopDefaultMode);
        CFRelease(runLoopObserver);
        runLoopObserver = NULL;
    }
}
- (void)startObserving {
    if (runLoopObserver != NULL) {
        return;
    }
    CFRunLoopObserverContext context = {
        0, (__bridge void *)self, NULL, NULL, NULL
    };
    runLoopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, true, 0, &runLoopObserverCallback, &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), runLoopObserver, kCFRunLoopDefaultMode);
    NSLog(@"RunLoop 观察者已添加");
}
// 观察者回调函数
void runLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"RunLoop Entry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"RunLoop Before Timers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"RunLoop Before Sources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"RunLoop Before Waiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"RunLoop After Waiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"RunLoop Exit");
            break;
        default:
            break;
    }
}
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
RunLoop 的运行逻辑:
- 通知 Observers:进入 Loop 
- 通知 Observers:即将处理 Timers 
- 通知 Observers:即将处理 Sources 
- 处理 Blocks(CFRunLoopPerformBlock 添加 
- 处理 Source0(可能会再次处理 Blocks) 
- 如果存在 Source1,就跳转到第 8 步处理 Source1 
- 通知 Observers:开始休眠(等待消息唤醒) 
- 通知 Observers:结束休眠(被某个消息唤醒) - 处理 Timer
- 处理 GCD Async To Main Queue
- 处理 Source1
 
- 处理 Blocks 
- 根据前面的执行结果,决定如何操作 - 回到第 02 步
- 退出 Loop
 
- 通知 Observers:退出 Loop 
补充:RunLoop 和 GCD
一般来说 GCD 和 RunLoop 是各管各的,但是如果 GCD 中使用了 dispatch_async(dispatch_get_main_queue(), block),那么 GCD 会将 block 放到主线程的 RunLoop 中执行。
dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"1");
});
2
3
补充:休眠
RunLoop 休眠是使用内核层面的 API mach_msg() 来实现的。在用户态调用 mach_msg() 会进入内核态,内核态会调用 mach_msg_trap() 来处理消息。mach_msg_trap() 会检查是否有消息,如果有消息,就会唤醒线程。
# Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"Timer");
}];
// NSDefaultRunLoopMode、UITrackingRunLoopMode 才是真正存在的模式
// NSRunLoopCommonModes 并不是一个真实的模式,它只是一个标记,表示在 RunLoop 结构中的CFMutableSetRef _commonModes里存放的所有模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 运行在 RunLoop 的默认模式下,在列表滚动时(UITrackingRunLoopMode),会停止计时。
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"Timer");
}];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 线程保活
必须要往 RunLoop 里添加 source/timer/observer,否则线程会退出。
    NSThread *thread = [[Thread alloc] initWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
        // 往 RunLoop 里添加 source/timer/observer
        [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }];
    [thread start];
2
3
4
5
6
7
8
9
这样写的话,这个线程会一直运行,可以使用 performSelector:onThread: 来唤醒线程。
// 参数:
// aSelector:方法
// thread:线程
// object:参数
// waitUntilDone:是否等待线程执行完毕,为 true 的话会等待
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
- (void)run {
    NSLog(@"run");
}
2
3
4
5
6
7
8
9
10
在创建线程的时候,有可能导致循环引用,例如:
@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // self 强引用了 thread,thread 又强引用了 self,导致循环引用
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}
- dealloc {
    // dealloc 方法不会被调用
    NSLog(@"dealloc");
}
- (void)run {
    NSLog(@"run");
    [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
可以使用 block 来解决这个问题:
self.thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"%@", [NSThread currentThread]);
    [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}];
[self.thread start];
2
3
4
5
6
在修改为 block 之后,我们会发现控制器可以正常释放了,但是线程并没有退出,这是因为我们开启了 RunLoop,RunLoop 会一直运行,线程不会退出。
停止子线程的 RunLoop:
首先,不能使用[[NSRunLoop currentRunLoop] run],而是使用[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]。因为 run 方法是无法停止的,它专门用来开启一个永不销毁的线程。
@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    // self 强引用了 thread,thread 又强引用了 self,导致循环引用
    self.thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"%@", [NSThread currentThread]);
        [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }];
    [self.thread start];
}
- dealloc {
    // dealloc 方法不会被调用
    NSLog(@"dealloc");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"%s", __func__);
    // 调用 test 方法,调用之后会发现线程会退出,但是我们没有调用 stopRunLoop 方法,为什么线程会退出呢?
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)test {
    NSLog(@"test");
}
- (void)run {
    NSLog(@"run");
    [[NSRunLoop currentRunLoop] addPort:[NSPort new] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}
- (void)stopRunLoop {
    // 停止子线程的 RunLoop
    [[NSRunLoop currentRunLoop] performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)stop {
    [[NSRunLoop currentRunLoop] stop];
}
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
在点击屏幕之后,会发现线程会退出,但是我们没有调用 stopRunLoop 方法,为什么线程会退出呢?
因为我们是使用 runMode 的方式开启 RunLoop,这种情况下,RunLoop 在执行完一次任务之后,就会退出。 可以通过添加一个状态变量来控制 RunLoop 的停止。
@property (nonatomic, assign, getter=isStopped) BOOL stopped;
- dealloc {
    // waitUntilDone 一定要设置为 YES,不然子线程会在控制器销毁之后还访问 self,导致 BAD ACCESS,触发崩溃
    [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 注意:weakSelf.isStopped 在 self 是 nil 的时候,会返回 NO,会导致循环还在一直执行
// 不要使用 StrongSelf,那样会导致循环引用
while (weakSelf && !weakSelf.isStopped) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
// ...
- (void)stop {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
整体上还是比较麻烦,其实直接使用 CFRunLoop 相关的 API 可以简单实现。
// 实现了保活的线程封装
@interface Thread ()
@property (nonatomic, strong) NSThread *innerThread;
@end
@implementation Thread
- (void)dealloc
{
    [self stop];
}
- (void)run {
    if (!self.innerThread) {
        __weak typeof(self) weakSelf = self;
        self.innerThread = [[NSThread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            // 创建 source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            // 最后一个参数returnAfterSourceHandled,设置为 false 就不会退出
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, DBL_MAX, false);
        }];
    }
    [self.innerThread start];
}
- (void)executeTask:(ThreadTask)task {
    NSAssert(self.innerThread != nil, @"thread is nil,please make sure call run method first");
    NSAssert(task != nil, @"task is nil");
    [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop {
    if (!self.innerThread) {
        return;
    }
    [self performSelector:@selector(__stopRunLoop) onThread:self.innerThread withObject:self waitUntilDone:YES];
}
// MARK: Private
- (void)__executeTask:(ThreadTask)task {
    task();
}
- (void)__stopRunLoop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}
@end
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
