长路漫漫,唯剑作伴--Runloop

时间:2022-04-08 05:34:59

五、Runloop

  1. 简介

    • 一般来讲,一个线程只能执行一个任务,执行完之后就会退出。Runloop提供了一个入口函数,实现让事件随时处理消息而不退出的机制。一旦执行了这个函数之后,线程就会处于接受消息-处理-等待的循环当中。
    • eventLoop的逻辑,伪代码演示
      function loop() {
          initialize();
          do { var message = get_next_message(); process_message(message); } while (message != quit); }
  2. 作用

    • 使程序一直运行接受用户输入;
    • 决定程序在何时应该处理哪些Event;
    • 调用解耦;
    • 节省CPU时间。
  3. 系统提供的两种Runloop:NSRunLoop、CFRunLoopRef

    1. CFRunLoopRef

      • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    2. NSRunLoop

      • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
      • NSRunLoop是一种更加高明的消息处理模式,在对消息处理过程进行了更好的抽象和封装,不用处理一些很琐碎很低层次的具体消息的处理,在NSRunLoop中每一个消息就被打包在input source或者是timer source中了。使用run loop可以使你的线程在有工作的时候工作,没有工作的时候休眠,可以大大节省系统资源。     
  4. Runloop的对外接口

    1. Core Foundation中关于Runloop的接口有5个类

      • CFRunLoopRef
      • CFRunLoopModeRef
      • CFRunLoopSourceRef
      • CFRunLoopTimerRef
      • CFRunLoopObserverRef
    2. CFRunLoopModeRef类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:长路漫漫,唯剑作伴--Runloop

      • 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
    3. CFRunLoopSourceRef是事件产生的地方。Source有两个版本:Source0 和 Source1。

      • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
      • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
    4. CFRunLoopTimerRef 是基于时间的触发器

      • 它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。
      • 当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
    5. 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; } }
          
  5. Runloop的mode:系统默认注册了5个mode

    • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
    • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
  6. 线程和Runloop

    • 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。
    • 线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。
    • RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其RunLoop(主线程除外)。
    • 主线程的runloop默认是启动的。
    • 对其它线程来说,runloop默认是没有启动的,如果你需要更多的线程交互则可以手动配置和启动,如果线程只是去执行一个长时间的已确定的任务则不需要。在任何一个Cocoa程序的线程中,都可以通过:NSRunLoop *runloop = [NSRunLoop currentRunLoop]获取到当前线程的runloop。
    • 我们不能在一个线程中去操作另外一个线程的runloop对象,那很可能会造成意想不到的后果。但是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的runloop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:- (CFRunLoopRef)getCFRunLoop获取对应的CFRunLoopRef类,来达到线程安全的目的。
  7. Runloop的管理并不完全是自动的。

    • 我们仍必须设计线程代码以在适当的时候启动runloop并正确响应输入事件,当然前提是线程中需要用到runloop。而且,我们还需要使用while/for语句来驱动runloop能够循环运行,下面的代码就成功驱动了一个run loop:

      BOOL isRunning = NO;
      do { isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; } while (isRunning);
  8. 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];
  9. main函数的运行

    • UIApplicationMain() 函数会为main thread 设置一个NSRunLoop 对象,这就解释了app应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
      int main(int argc, char *argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([appDelegate class])); } }
  10. 辅助线程

    • 对于辅助线程,你需要判断一个runloop是否是必须的。如果是必须的,那么你要自己配置并启动它。你不需要在任何情况下都去启动一个线程的runloop。比如,你使用线程来处理一个预先定义的长时间运行的任务时,你应该避免启动runloop。Runloop在你要和线程有更多的交互时才需要,比如以下情况:
      • 使用端口或自定义输入源来和其他线程通信;
      • 使用线程的定时器;
      • Cocoa中使用任何performSelector...的方法;
      • 使线程周期性工作;