五、Runloop
-
简介
- 一般来讲,一个线程只能执行一个任务,执行完之后就会退出。Runloop提供了一个入口函数,实现让事件随时处理消息而不退出的机制。一旦执行了这个函数之后,线程就会处于接受消息-处理-等待的循环当中。
- eventLoop的逻辑,伪代码演示
function loop() { initialize(); do { var message = get_next_message(); process_message(message); } while (message != quit); }
-
作用
- 使程序一直运行接受用户输入;
- 决定程序在何时应该处理哪些Event;
- 调用解耦;
- 节省CPU时间。
-
系统提供的两种Runloop:NSRunLoop、CFRunLoopRef
-
CFRunLoopRef
- CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
-
NSRunLoop
- NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
- NSRunLoop是一种更加高明的消息处理模式,在对消息处理过程进行了更好的抽象和封装,不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source中了。使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,可以大大节省系统资源。
-
-
Runloop的对外接口
-
Core Foundation中关于Runloop的接口有5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
-
CFRunLoopModeRef类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:
- 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
-
CFRunLoopSourceRef是事件产生的地方。Source有两个版本:Source0 和 Source1。
- Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
- Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
-
CFRunLoopTimerRef 是基于时间的触发器
- 它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。
- 当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
-
CFRunLoopObserverRef 是观察者,
- 每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
- (void)observerRunLoop { // 获得当前thread的Run loop // 凡是corefoundation里面的ref 代表 指针 CFRunLoopRef runloop = CFRunLoopGetCurrent(); //设置Run loop observer的运行环境 CFRunLoopObserverContext context = { 0, (__bridge void*)self,//这里填写你要传递的信息。 &CFRetain, &CFRelease, NULL }; //创建Run loop observer对象 //第一个参数用于分配observer对象的内存 //第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释 //第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行 //第四个参数用于设置该observer的优先级 //第五个参数用于设置该observer的回调函数 //第六个参数用于设置该observer的运行环境 CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context); //添加观察者 /** 第一个参数:(<#CFRunLoopRef rl#>)要监听哪个runloop 第二个参数:(<#CFRunLoopObserverRef observer#>)监听者 第三个参数:(<#CFStringRef mode#>)要监听runloop在哪种运行模式下的状态 */ CFRunLoopAddObserver(runloop, observer, kCFRunLoopCommonModes); } void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { switch (activity) { case kCFRunLoopEntry: NSLog(@"即将进入runloop"); break; case kCFRunLoopBeforeTimers: NSLog(@"即将处理 Timer"); break; case kCFRunLoopBeforeSources: NSLog(@"即将处理 Sources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即将进入休眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"从休眠中唤醒loop"); break; case kCFRunLoopExit: NSLog(@"即将退出runloop"); break; default: break; } }
- 每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
-
-
Runloop的mode:系统默认注册了5个mode
- kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
- UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
-
线程和Runloop
- 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
- 线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。
- RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)。
- 主线程的runloop默认是启动的。
- 对其它线程来说,runloop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个Cocoa程序的线程中,都可以通过:
NSRunLoop *runloop = [NSRunLoop currentRunLoop]获取到当前线程的runloop。
- 我们不能在一个线程中去操作另外一个线程的runloop对象,那很可能会造成意想不到的后果。但是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的runloop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:- (CFRunLoopRef)getCFRunLoop获取对应的CFRunLoopRef类,来达到线程安全的目的。
-
Runloop的管理并不完全是自动的。
-
我们仍必须设计线程代码以在适当的时候启动runloop并正确响应输入事件,当然前提是线程中需要用到runloop。而且,我们还需要使用while/for语句来驱动runloop能够循环运行,下面的代码就成功驱动了一个run loop:
BOOL isRunning = NO; do { isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; } while (isRunning);
-
-
NSTimer问题
- 此方法创建的定时器,必须加到NSRunLoop中。
NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(changeImage) userInfo:nil repeats:YES]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addTimer:timer forMode: NSRunLoopCommonModes];
- 此种创建定时器的方式,默认加到了runloop,且默认为第二个参数。
self.timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(changeImage) userInfo:nil repeats:YES];
- 此方法创建的定时器,必须加到NSRunLoop中。
-
main函数的运行
- UIApplicationMain() 函数会为main thread 设置一个NSRunLoop 对象,这就解释了app应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class])); } }
- UIApplicationMain() 函数会为main thread 设置一个NSRunLoop 对象,这就解释了app应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
-
辅助线程
- 对于辅助线程,你需要判断一个runloop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的runloop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动runloop。Runloop在你要和线程有更多的交互时才需要,比如以下情况:
- 使用端口或自定义输入源来和其他线程通信;
- 使用线程的定时器;
- Cocoa中使用任何performSelector...的方法;
- 使线程周期性工作;
- 对于辅助线程,你需要判断一个runloop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的runloop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动runloop。Runloop在你要和线程有更多的交互时才需要,比如以下情况: