事件循环机制
分析UIApplicationMain如何启动主线程的RunLoop
打断点,通过lldb指令bt查看函数调用栈如下:
在UIApplicationMain函数中调用了Core Foundation框架下的CFRunLoopRunSpecific
函数
CFRunLoopRunSpecific函数实现:RunLoop的入口
源码如下:
1 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
删掉不重要的细节:
1 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ |
可知,RunLoop事件循环的实现机制体现在__CFRunLoopRun函数中。
__CFRunLoopRun函数实现:事件循环的实现机制
精简后的代码如下:
1 | /** |
从该函数实现中可以得知RunLoop主要就做以下几件事情:
- __CFRunLoopDoObservers:通知
Observers
接下来要做什么 - __CFRunLoopDoBlocks:处理
Blocks
- __CFRunLoopDoSources0:处理
Sources0
- __CFRunLoopDoSources1:处理
Sources1
- __CFRunLoopDoTimers:处理
Timers
- 处理 GCD 相关:
dispatch_async(dispatch_get_main_queue(), ^{ });
- __CFRunLoopSetSleeping/__CFRunLoopUnsetSleeping:休眠等待/结束休眠
- __CFRunLoopServiceMachPort -> mach-msg():转移当前线程的控制权
__CFRunLoopServiceMachPort函数实现:RunLoop休眠的实现原理
在__CFRunLoopRun
函数中,会调用__CFRunLoopServiceMachPort
函数,该函数中调用了mach_msg()
函数来转移当前线程的控制权给内核态/用户态。
- 没有消息需要处理时,休眠线程以避免资源占用。调用mach_msg()从用户态切换到内核态,等待消息;
- 有消息需要处理时,立刻唤醒线程,调用mach_msg()回到用户态处理消息。
这就是RunLoop休眠的实现原理,也是RunLoop
与简单的do...while
循环区别:
- RunLoop:休眠的时候,当前线程不会做任何事,CPU 不会再分配资源;
- 简单的do…while循环:当前线程并没有休息,一直占用 CPU 资源。
1 | static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) { |
RunLoop与线程
- RunLoop对象和线程是一一对应关系;
- RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 如果没有RunLoop,线程执行完任务就会退出;如果没有RunLoop,主线程执行完main()函数就会退出,程序就不能处于运行状态;
- RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
- RunLoop销毁时机:RunLoop会在线程结束时销毁;
- 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
- 主线程的RunLoop对象是在UIApplicationMain中通过
[NSRunLoop currentRunLoop]
获取,一旦发现它不存在,就会创建RunLoop对象。
未启动RunLoop的子线程
创建一个NSThread的子类HTThread并重写了dealloc方法来观察线程的状态。执行以下代码,发现线程执行完一次test任务就退出销毁了,没有再执行test任务,原因就是没有启动该线程的RunLoop
1 | - (void)viewDidLoad { |
开启子线程的RunLoop的过程
获取RunLoop对象
1 | // Foundation |
来看一下CFRunLoopGetCurrent()函数是怎么获取RunLoop对象的:
1 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
1 | // should only be called by Foundation |
启动子线程的RunLoop
1 | // Foundation |
来看一下CFRunLoopRun()/CFRunLoopRunInMode()函数是怎么启动RunLoop的:
1 | void CFRunLoopRun(void) { |
可以看到它通过调用CFRunLoopRunSpecific()函数来启动Runloop
实现一个常驻线程
- 好处:经常用到子线程的时候,不用一直创建销毁,提高性能;
- 条件:该任务需是串行的,而非并发;
- 步骤:
① 获取/创建当前线程的RunLoop;
② 向该RunLoop中添加一个Source/Port等来维持RunLoop的事件循环(如果 Mode 里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出);
③ 启动该RunLoop。
1 | // ViewController.m |
点击view,接着退出当前ViewController。输出如下:
1 | begin-----<HTThread: 0x600002b71240>{number = 6, name = (null)} |