iOS多线程编程Part 1/3 - NSThread & Run Loop
Core Foundation层对应的是CFRunLoopRef:

两组接口差不多,不过功能上还是有许多区别的,例如CF层可以添加自定义Input Source事件源(CFRunLoopSourceRef)和Run Loop观察者Observer(CFRunLoopObserverRef),很多类似功能的接口特性也是不一样的。
Run Loop运行
Run Loop如何运行呢?在上一节NSThread的入口函数中使用了一种NSRunLoop的使用场景,再看一例:
从上图可以看出Run Loop就是处理事件的一个循环,不同的是Timer Source事件处理后不会使Run Loop结束,而Input Source事件处理后会让Run Loop退出。因此你需要自己的一个Loop去不断运行Run Loop来处理事件,就像本文开头的示例那样。
细分下Run Loop的事件源:
1) Timer Souce就是创建Timer添加到Run Loop中,没啥好说的,Cocoa或者Core Foundation都有相应接口实现。需要注意的是scheduledTimerWith****开头生成的Timer会自动帮你以默认NSDefaultRunLoopMode模式加载到当前的Run Loop中,而其他接口生成的Timer则需要你手动使用-addTimer:forMode添加到Run Loop中。需要额外注意的是Timer的触发不会让Run Loop返回。(Timer sources deliver events to their handler routines but do not cause the run loop to exit.) 具体实验可以看下面的Sample Code。
2) Input Source中的-performSelector:***API调用簇方法,有以下这些接口:
主线程持有包含子线程的Run Loop和Source的context对象,还有一个用于保存需要运行操作的数据buffer。主线程需要子线程干活时,首先将需要的操作数据添加到数据buffer,然后通知source,唤醒子线程Run Loop(因为子线程可能正在sleep状态,CFRunLoopWakeUp唤醒Run Loop可以通知线程醒来干活),由于子线程也持有这个source和数据buffer,因此在触发唤醒时可以使用这个数据buffer的数据来执行相关操作(需要注意数据buffer访问时的同步)。
具体实现参见本文最后的Sample Code。
Run Loop的Observer
Core Foundation层的接口可以定义一个Run Loop的观察者在Run Loop进入以下某个状态时得到通知:
- Run loop的进入
- Run loop处理一个Timer的时刻
- Run loop处理一个Input Source的时刻
- Run loop进入睡眠的时刻
- Run loop被唤醒的时刻,但在唤醒它的事件被处理之前
- Run loop的终止
Observer的创建以及添加到Run Loop中需要使用Core Foundation的接口:
};对应Run Loop的各种事件,kCFRunLoopAllActivities比较特殊,可以观察所有事件。具体样例代码请参考Sample Code。
总结
Run Loop就是一个处理事件源的循环,你可以控制这个Run Loop运行多久,如果当前没有事件发生,Run Loop会让这个线程进入睡眠状态(避免再浪费CPU时间),如果有事件发生,Run Loop就处理这个事件。Run Loop处理事件和发送给Observer通知的流程如下:
- 1) 进入Run Loop运行,此时会通知观察者进入Run Loop;
- 2) 如果有Timer即将触发时,通知观察者;
- 3) 如果有非Port的Input Sourc即将e触发时,通知观察者;
- 4)触发非Port的Input Source事件源;
- 5)如果基于Port的Input Source事件源即将触发时,立即处理该事件,跳转到步骤9;
- 6)通知观察者当前线程将进入休眠状态;
- 7)将线程进入休眠状态直到有以下事件发生:基于Port的Input Source被触发、Timer被触发、Run Loop运行时间到了过期时间、Run Loop被唤醒。
- 8) 通知观察者线程将要被唤醒。
- 9) 处理被触发的事件:
- 如果是用户自定义的Timer,处理Timer事件后重新启动Run Loop进入步骤2;
- 如果线程被唤醒又没有到过期时间,则进入步骤2;
- 如果是其他Input Source事件源有事件发生,直接处理这个事件;
- 10)到达此步骤说明Run Loop运行时间到期,或者是非Timer的Input Source事件被处理后,Run Loop将要退出,退出前通知观察者线程已退出。
什么时候需要用到Run Loop?官方文档的建议是:
- 需要使用Port或者自定义Input Source与其他线程进行通讯。
- 需要在线程中使用Timer。
- 需要在线程上使用performSelector*****方法。
- 需要让线程执行周期性的工作。
我个人在开发中遇到的需要使用Run Loop的情况有:
- 使用自定义Input Source和其他线程通信
- 子线程中使用了定时器
- 使用任何performSelector*****到子线程中运行方法
- 使用子线程去执行周期性任务
- NSURLConnection在子线程中发起异步请求
Sample Code
RunLoop刚开始用确实坑很多,理解概念最好的方式还是动手写代码,写了个例子放在GitHub上(工程NSThreadExample),欢迎大家讨论。
Apple官方也有一个基于Run Loop的异步网络请求示例程序SimpleURLConnections。
参考资料
Threading Programming Guide
NSRunLoop Class Reference
CFRunLoop Reference
CFRunLoopObserver Reference