iOS - RunLoop


Runloop 接收输入时间来自两种不同的来源:输入源(intput source)和定时源(timer source)。输入源传递一步时间。通常消息来自于其他线程或程序。定时源则传递同步时间,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定的处理历程来处理到达的时间。


一、什么是RunLoop

  • 基本作用
    • 保持程序的持续运行(一个死循环,使app不断运行)
    • 处理App中的各种事件(触摸、定时器、Selector)
    • 节省CPU资源、提高程序性能:该做事的时候做事,该休息的时候休息。
  • 如果没有RunLoop
    int main(int argc,char * argv[]){
      NSLog(@"execute main function");---->程序开始
      return 0; ------------------------->程序结束
    }
  • 有 RunLoop
    • 由于 main 函数里面启动了个 RunLoop,所以程序并不会马上退出,保持持续运行状态
      int main(int argc,char * argv[]){
      BOOL running = YES; -------->程序开始
      do {------------------------------
         // 执行各种任务,处理各种事件------持续运行
      }while(running);---------------------
      return 0;
      }

二、main 函数中的 RunLoop

  • UIApplicationMain函数内部就启动了一个RunLoop
  • 所以UIApplicationMain 函数一直没有返回,保持了程序的持续运行
  • 这个默认启动的 RunLoop 是跟主线程相关联的
    int main(int argc, char * argv[]) {
      @autoreleasepool {
          return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
      }
    }

三、RunLoop 的输入源

  • 输入源异步的发送消息给你的线程,时间的来源取决于输入源的种类:基于端口的输入员自定义输入源。基于端口的输入源监听程序相应的端口。自定义输入源则监听自定义的事件源。runloop,不关心输入源是基于端口的还是自定义的。系统会实现两种输入源供你使用。两类输入源的区别在于如何显示:基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送。
  • 当你创建输入源的时候,需要将其分配给 runloop 中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,runloop 运行在默认模式下,但是你也可以使器运行在自定义模式中。若某一源在当前模式下不被监听,那么任何器生成消息只在 runloop 运行在其关联的模式下才会被传递。
  • 基于端口的输入源
    • Cocoa 和 CoreFoundation 内置支持使用端口相关的对象和函数来创建基于端口的源。在 Cocoa 里面你从来不需要直接创建输入源。只要简单的创建对象,并使用 NSPort 的方法将该端口天极大到 ruhnloop 中。端口对象会自己处理创建和配置的输入源。
  • 自定义输入源
    • 为了自定义输入源,必须使用 Core Foundation里面的 CGRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。Corefondation 会在配置源的不同地方调用回调函数,处理输入时间,在源从 runloop 移除的时候清理它。
    • 除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并源并通知它处理数据。消息传递机制的定义取决于你,但是最好不要过于复杂。
  • Cocoa 执行 Selector 的源
    • 除了基于端口的源,Cocoa 定义了自定义的输入源,允许你在任何线程中执行 seletor。和基于端口的源一样,执行 selector 请求会在目标线程上序列化,减缓许多咋线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个 selector 执行完后会自动从 runloop 里面移除。
    • 当在其他线程上面执行 selector 时候,目标线程需有一个活动的 runloop,对于你创建的线程,这意味着线程在你显示的启动 runloop 之前处于等待状态。由于主线程自己启动它的 runloop,那么在程序通过委托调用 applicationDidFinishlaunching:的时候你会遇到线程调用的问题。因为 RunLoop 通过每次循环来处理所有队列的 selector 的调用,而不是通过 loop 的迭代来处理 selector。

四、RunLoop 对象

  • iOS 中有2套 API 来访问和使用 RunLoop
    • Foundation
      • NSRunLoop
    • CoreFoundation
      • CFRunLoopRef
  • NSRunLoop 和 CGRunLoopRef 都代表着 RunLoop 对象
  • NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装, 所以要了解 RunLoop内部结构,需要研究 CFRunLoopRef 层面的 API (Core Foundation层面)

五、RunLoop 与线程

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop 自动创建好了,子线程的 RunLoop 需要主动创建
  • RunLoop 在第一次获取时创建,在线程结束时销毁

六、获取RunLoop 对象

  • Foundation
    //获得当前线程的 RunLoop 对象
    [NSRunLoop currentRunLoop];
    //获得主线程的 RunLoop 对象
    [NSRunLoop mainRunLoop];
  • Core Foundation
    //当前RunLoop
    CFRunLoopGetCurrent();
    //主线程 RunLoop
    CFRunLoopGetMain();

七、NSRunLoop 相关类

  • CoreFoundation中关于RunLoop的5个类
    • CFRunLoopRef(运行循环对象)
    • CFRunLoopModeRef(1个runLoop可以有很多个Mode,1个Mode可以有很多个Source Observer Timer,但是在同一时刻只能同时执行一种Mode关于更多种类的Mode
    • CFRunLoopSourceRef(处理事件)
    • CFRunLoopTimerRef(处理定时器相关)
    • CFRunLoopObserverRef(观察者,观察是否有事件)

      RunLoop.png
  • CFRunLoopModeRef 代表 RunLoop 的运行模式
    • 一个 RunLoop 包含若干个 Mode,每个Mode 又包含若干个 Source/Timer、Observer
    • 每次 RunLoop 启动时,只能制定其中一个 Mode,这个 Mode 被称作 CurrentMode
    • 如果需要切换 Mode,只能退出 Loop,再重新制定一个 Mode 进入
    • 这样做主要是为了分割开不同组的 Source/Timer/Observer,让其互不影响
  • 系统默认注册了 5个Mode:
    • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个 Mode 下运行的
    • UITrackingRunLoopMode:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响
    • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用。
    • GSEventReceiveRunLoopMode:接收系统时间的内部 Mode,通常用不到。
    • kCFRunLoopCommonModes(比较特殊):这时一个占位用的 Mode,不是一种真正的 Mode。
  • CFRunLoopSourceRef 是事件源(输入源)
    • Source0:非基于Port的
      • Custom Input Sources
      • Cocoa Perform Selector Sources
    • Source1:基于Port的
      • Port- Based Sources
    • 举例:输出点击事件的调用栈,我们可以清楚的看到runloop中做的是__CFRunLoopDoSource0

      Source0调用栈.png
  • CFRunLoopTimerRef 处理定时器
    • NSTimer 定时器调用栈:__CFRunLoopDoTimer

      timer.png
    • 注意:使用不同种类的 Mode 会对定时器的效果有不同的展现
      • NSDefaultRunLoopMode:将NSTimer添加到主线程NSRunLoop的默认模式下,只有主线程是默认模式下才能执行NSTimer(滚动scrollView,RunLoop默认进入Tracking模式,所以NSTimer不会有效果)。
      • UITrackingRunLoopMode:将NSTimer添加到主线程NSRunLoop的追踪模式下,只有主线程是追踪模式下才能执行NSTimer。(例如滚动scrollView的时候就会监听到计时器)
      • NSRunLoopCommonModes:Common是一个表示,它是将NSDefaultRunLoopMode 和 UITrackingRunLoopMode标记为了Common
        所以,只要将 timer 添加到 Common 占位模式下,timer就可以在Default和UITrackingRunLoopMode模式下都能运行
  • 如果用GCD创建计时器:
    • GCD 创建的好处,不受 RunLoopMode 的影响。
      //1、创建timer
      //dispatchQueue:定时器将来回调的方法在哪个线程中执行
      dispatch_queue_t queue = dispatch_get_main_queue();
      dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
      self.timer = timer;
      //2.设置timer
      /*
      第一个参数:需要设置哪个timer
      第二个参数:指定定时器开始的时间
      第三个参数:指定间隔时间
      第四个参数:定时器的精准度,如果传0代表要求非常精准(系统会让计时器执行时间变得更加准确,性能消耗也会提高),如果传入一个大于0的值,代表我们允许的误差
      //例如传入60,就代表允许误差有60秒
      */
      //设置第一次执行的时间
      dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
      //DISPATCH_TIME_NOW
      dispatch_source_set_timer(timer,start , 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
      //3、设置timer的回调
      dispatch_source_set_event_handler(timer, ^{
        NSLog(@"%@",[NSRunLoop currentRunLoop]);
      });
      dispatch_resume(timer);
    • 在 RunLoop 底层默认会调用这里
      /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
      if (msg_is_timer) {
      __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
      } 
      /// 9.2 如果有dispatch到main_queue的block,执行block。
      else if (msg_is_dispatch) {
      __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
      }
  • CFRunLoopObserverRef观察者,能够监听RunLoop状态改变
    • 监听的时间点:
      typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
      kCFRunLoopEntry    = (1UL << 0), // 即将进入LOOP
      kCFRunLoopBeforeTimers    = (1UL << 1), // 即将处理Timer
      kCFRunLoopBeforeSources    = (1UL << 2), // 即将进入处理Source
      kCFRunLoopBeforeWaiting    = (1UL << 5), // 即将进入休眠
      kCFRunLoopAfterWaiting    = (1UL << 6), // 刚才休眠中唤醒
      kCFRunLoopExit    =  (1UL << 7),       // 即将退出Loop
      }
    • 监听的代码:
      - (void)viewDidLoad{
      [super viewDidLoad];
      //1、创建监听对象
      /*
      第一个参数:告诉系统如何给Observer对象分配存储空间
      第二个参数:需要监听的类型
      第三个参数:是否需要重复监听
      第四个参数:优先级
      第五个参数:监听到对应的状态之后的回调
      typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
      kCFRunLoopEntry = (1UL << 0),
      kCFRunLoopBeforeTimers = (1UL << 1),
      kCFRunLoopBeforeSources = (1UL << 2),
      kCFRunLoopBeforeWaiting = (1UL << 5),
      kCFRunLoopAfterWaiting = (1UL << 6),
      kCFRunLoopExit = (1UL << 7),
      kCFRunLoopAllActivities = 0x0FFFFFFFU
      };
      */
      CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"进入RunLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入睡眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"即将醒来");
                break;
            case kCFRunLoopExit:
                NSLog(@"退出");
                break;
            default:
                break;
        }
      });
      //2、给主线程的RunLoop添加监听
      /*
      第一个参数:需要监听的 RunLoop 对象
      第二个参数:给指定的 RunLoop 对象添加的监听对象
      第三个参数:在哪种模式下监听
      */
      CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes);
      NSTimer *timer = [NSTimer   scheduledTimerWithTimeInterval:2 target:self   selector:@selector(demo) userInfo:nil repeats:YES];
      }
      - (void)demo{
      NSLog(@"%s",__func__);
      }
      我们会看到这几行打印会重复执行
      2015-09-06 17:02:04.848 RunLoop观察者[35817:418636] 即将醒来
      2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] -[ViewController demo]
      2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将处理Timer
      2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将处理source
      2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将进入睡眠
      2015-09-06 17:02:06.848 RunLoop观察者[35817:418636] 即将醒来
      2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] -[ViewController demo]
      2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将处理Timer
      2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将处理source
      2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将进入睡眠

八、关于 RunLoop 的理解

  • 理解图(引用了一张网上很好的图片):

    RunLoop理解图.png
  • 一条线程对应一个 RunLoop,主线程的 RunLoop 只要程序已启动就会默认创建并与主线程绑定好,RunLoop 底层的实现是通过字典的形式来将 线程 和 RunLoop 来绑定的,RunLoop 可以理解为懒加载,子线程的 RunLoop 可以调用 currentRunLoop,先从字典里面根据子线程取,如果没有就会去创建并与子线程绑定,保存到字典当中。每个 RunLoop 里面有很多的 Mode,每个 Mode 里面又有很多的source、timer、observer。RunLoop 在同一时刻只能执行一种 Mode,当执行这种 Mode 的时候,只有这种 Mode 中的source、timer、observer 有效,别的 Mode 无效,这样做是为了避免逻辑的混乱。
  • 执行流程:先进入 RunLoop,处理系统默认事件,触发事件的时候,RunLoop 醒来处理 timer、source0、source1,处理完再睡觉。
  • RunLoop 死掉的情况:
    • RunLoop 有个默认的超时时间.
      seconds = 9999999999.0
    • 线程挂了。

九、RunLoop 应用场景

  • NSTimer
    • 就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
    • 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
    • CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。
  • ImageView显示
  • PerformSelector:
    • 当调用 NSObject 的 performSelector:afterDelay:后,世纪上期内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以如果当前线程没有 RunLoop,则这个方法会失效。
    • 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
  • 常驻线程
    • 创建一个线程来处理耗时且频繁的操作,例如即时聊天音频的压缩,或者经常下载,避免频繁开启线程以便提高性能, AFNetWorking就是如此。
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
  • 自动释放池
    • 系统在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
    • 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
    • 第二个 Observer 监视了两个事件BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
    • 打印 currentRunLoop 来获取autoreleasePool 的状态
      NSLog(@"%@",[NSRunLoop currentRunLoop]);
    • 只有两种状态
      _wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1  = 1
      _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160
    • 对比 RunLoop 的活动状态:
      对比runLoop
      typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){
      kCFRunLoopEntry            = (1UL << 0), // 即将进入LOOP    =1
      kCFRunLoopBeforeTimers     = (1UL << 1), // 即将处理Timer = 2
      kCFRunLoopBeforeSources    = (1UL << 2), // 即将进入处理Source = 4
      kCFRunLoopBeforeWaiting    = (1UL << 5), // 即将进入休眠 = 32
      kCFRunLoopAfterWaiting     = (1UL << 6), // 刚才休眠中唤醒 = 64
      kCFRunLoopExit             = (1UL << 7), // 即将退出Loop = 128
      }
    • 得出结论:
      + _wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1 = 1 = 即将进入RunLoop创建一个自动释放池
      + _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160 = 128+32
      + 32: 即将进入休眠 1、销毁一个自动释放池 2、创建一个新的自动释放池
      + 128:即将退出RunLoop 销毁一个自动释放池
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值