runloop之于thread
做一个技术方向久了,难免会沉溺其中,对当初开始接触这个方向的许多根本上的疑问渐渐都不了了之,意识上认为然,而不知其所以然。
最近重新梳理iOS的runloop,说说自己的理解,希望能说清楚。
先抛出一个问题:程序入口main函数只是一段代码,为什么只运行了一段代码,程序就可以一直运行?
main函数中起了一个main thread,main thread执行结束,则应用程序退出,为了能让线程一直运行,苹果引入了runloop,苹果这样介绍runloop:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
main thread中的runloop一直在跑圈圈(while循环),所以这个main thread不会立即执行结束,如果设定这个runloop跑圈的时间是1年,app就可以运行1年,1年后app自动退出。
这里面有两个问题:
- runloop如果一直在跑圈,会一直占用/消耗系统资源,怎么避免?
- 跑圈圈的同时,如果用户有输入等操作,怎么检测?
runloop是基于事件处理的,有事件需要处理,则runlopp开始跑圈检测需要处理的事件,处理结束后,进入休眠,当再次有事件需要处理时,再次被唤醒去跑圈,如此反复。
怎么知道有事件要去处理呢?事件包括许多,计时器、用户输入、点击、网络请求等,main thread中创建main runloop时,会将这些事件作为source加入到runloop中,这样,当其中一个source有变更时,激活runloop去跑圈。当然如果这些runloop中source都被移除了,runloop跑一圈之后就会被释放,线程也随即结束。如果main runloop的source可以全部移除,那就意味着app要退出了,但实际的情况是,main runloop被系统做了特殊处理,是无法真正把source都remove掉的,想通过移除source达到app exit的目的是不行的。
做移动开发的同学经常会被问到iOS系统为什么比Android系统流畅的问题,问题也出在runloop中。runloop中有几种模式,其中一种叫Event tracking模式,意为事件追踪,当用户滚动页面等操作时,runloop会被切换到追踪模式,这个模式会限制后续其他事件,比如在这个模式下,点击、长按等其他事件等都将会被限制,让系统只专注处理当前tracking的事件。整个系统只专注做一件事,当让流畅。
题为runloop之于thread,是说它们之间的关系,thread可以没有runloop,但runloop必须运行在线程中,也就是是apple说的The purpose of a run loop is to keep your thread busy
。thread和runloop是一一对应的,以键值对存储在底层,一个thread/runloop
中可以嵌套多个thread/runloop
。
iOS中非常著名的网络库AFNetworking(老版本,大家都喜欢拿这段代码进行解读)有这样一段代码:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
这段代码创建了一个守护线程,为了让这个线程能够一直在后台处理网络,在线程入口networkRequestThreadEntryPoint:
中通过 [NSRunLoop currentRunLoop]
创建了一个runloop;为了runloop能够一直跑圈,向runloop中加入了一个Mach port源,只要这个port源不移除,runloop就会一直存在。
这个时候的runloop其实是一直处于休眠状态,随后开始网络请求时向其中加入了connection源:
- (void)operationDidStart {
self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
for (NSString *runLoopMode in self.runLoopModes) {
[self.connection scheduleInRunLoop:runLoop forMode:runLoopMode];
}
[self.connection start];
}
}
connection收到网络响应后,runloop被激活,开始跑圈,然后检测到有网络响应数据,runloop和connection协调数据输出,以回调的形式将底层接收的数据抛出,
当所有数据传输结束后,connection被AFNetworking置为ni,从runloop中移除,这时runloop进入休眠,这样就完成了一次在后台发请求、接收数据的过程。
等重新开始一次网络请求时,这个过程将会被再次执行一遍。
参考资料
Threading Programming Guide: Run Loops
Threading Programming Guide: Thread Management