runloop - ShenYj/ShenYj.github.io GitHub Wiki

RunLoop

RunLoop 与线程一一对应,系统会有一个全局字典来存储 RunLoop,以线程为 key, RunLoop 为 value,会在第一次获取 RunLoop 时创建 RunLoop

  • CF-1153.18 源码中 __CFRunLoop 的定义

    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;			/* locked for accessing mode list */
        __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread;
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        
        // 重点 ----->
        CFRunLoopModeRef _currentMode;  // `struct __CFRunLoopMode *` 类型指针
        CFMutableSetRef _modes;         // 一个 `Mode` 集合
        // <----- 重点
    
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    
    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    
    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
        CFStringRef _name;
        Boolean _stopped;
        char _padding[3];
    
        // 四种 `Mode` 集合
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
    
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };

runloop 源码下载地址

RunLoopMode

  • CFRunLoopModeRef 代表着 RunLoop 的运行模式
  • 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 _sources0_sources1_observer_timer
    • 不同组的 _sources0_sources1_observer_timer 能分隔开来,互不影响
    • 每一个 _sources0_sources1_observer_timer 都是一个集合,可以添加多个 source
  • RunLoop 启动时只能选择其中一种 Mode,作为 curentMode
  • 如果要切换 Mode 只能退出当前 Loop,再重选择一个 Mode 进入 (切换 Mode 不会造成 App 退出)
  • 如果 Mode 里没有任何 _sources0_sources1_observer_timerRunLoop 会立马退出

Source

  • Source0
    • 触摸事件处理,比如常规的 touchBegan
    • FoundationperformSelector:onThread: 都是 Source0
  • Source1
    • 基于 port 通信
    • 系统捕捉事件,默认触摸事件就是先由Source1捕捉,在分发到Source0处理
  • Timers
    • NSTimer
    • performSelector:withObject:afterDelay:
  • Observers
    • 用于监听 RunLoop 的状态
    • UI 界面的刷新 BeforeWaiting
    • autoRelease pool

状态

  • RunLoop Observer 可以监听到的几种状态

    /* Run Loop Observer Activities */
    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
        kCFRunLoopAllActivities = 0x0FFFFFFFU   // 以上所有状态
    };

可以通过 CoreFoundation 提供的 api 监听 RunLoop 的状态

执行流程

  • RunLoop 执行流程

        01. 通知 Observers: 进入 Loop                       --> __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry)
    ┏━> 02. 通知 Observers: 即将处理 Timers
    ┃   03. 通知 Observers: 即将处理 Sources
    ┃   04. 处理 Blocks
    ┃   05. 处理 Source0 (可能会再次处理 Blocks)
    ┃   06. 如果存在 Source1, 就跳转到 `第8步`       ━━━━┓
    ┃   07. 通知 Observers,开始休眠(等待消息唤醒)        ┃
    ┃   08. 通知 Observers,结束休眠(被某个消息唤醒)       ┃   __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode)
    ┃      1. 处理 Timer                               ┃
    ┃      2. 处理 GCD Async To Main Queue             ┃
    ┃      3. 处理 Source1                          <━━┛
    ┃   09. 处理 Blocks
    ┃   10. 根据前面的执行结果,决定如何操作  
    ┗━     1. 回到 `第2步`
           2. 退出 Loop                           ━━━━┓
        11. 通知 Observers: 退出 Loop              <━━┛    --> __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit
  • RunLoop 调用细节

    • __CFRunLoopDoObservers 函数内最终会通过调用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 函数完成通知
    • __CFRunLoopDoBlocks 函数内最终会通过调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__ 函数 处理 blocks
    • __CFRunLoopDoTimers 函数内最终会通过调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 函数 执行定时器逻辑
    • __CFRunLoopDoSources0 函数内最终会通过调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 函数 执行 source0 逻辑
    • __CFRunLoopDoSource1 函数内最终会通过调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ 函数 执行 source1 逻辑
    • 通过 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ 函数处理 GCD Async To Main Queue
      • GCD 只有这种特殊情况交给 RunLoop 处理,GCD 有自己的处理逻辑,不依赖于 RunLoop
      • e.g. > > oc > dispatch_async(dispatch_get_global_queue(0,0), ^{ > // 这里子线程的任务并不是 RunLoop 来处理的 > dispatch_async(dispatch_get_main_queue(), ^{ > // 返回主线程做一些任务,这时是 RunLoop 来处理的 > }); > }); >

实际应用

  • RunLoop 在开发中的实际应用场景

    1. 控制线程生命周期(线程保活)
    2. 解决 NSTimer 在滑动时停止工作的问题
    3. 监控应用卡顿
    4. 性能优化
  • 线程保活在使用时的注意点

    • 如果保活的线程并非持续存在,有一定的生命周期
      1. 通过 NSThread 的 block 方式添加 Source,相对比 target ,不会造成对 target 的一层强持有

      2. 保证 Thread 的销毁,停止掉保活线程的 RunLoop (CFRunLoopStop(CFRunLoopGetCurrent()))

      3. [[NSRunLoop currentRunLoop] run] 默认开启一个无限循环,重复调用 runMode:beforeDate:,可以根据自己的标识调用 runMode:beforeDate: 方法

        • e.g.

          while(weakSelf && !weakSelf.isStop) {
              [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
          }
          
      4. 取消 NSThread 的强引用: self.thread = nil,停止 runloop 后即便 Thread 还存在没有被销毁,也不再能继续执行任务,再次添加任务就会 crash,所以在停止 RunLoop 后及时将 thread 置为 nil, 向线程中添加任务前进行判断

      5. 为了保证 RunLoop 被正常的停止, 并且 thread和 self(比如当前控制器)正常的销毁,停止 RunLoop 函数在执行时 performSelector:onThread:withObject:waitUntilDone: 最后的参数一定要传 YES, 等待 stop 执行完毕再向下执行, 不要让 self 销毁过早产生 crash 的同时,在 run 的判断条件中一定要检查 self != nil

      如果使用 CFRunLoop 来启动 RunLoop 会使控制逻辑更加的简单 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false) (最后一个参数 false 可以保证执行完一次任务后不退出 RunLoop ,这样就不需要 注意点3 中那层 while循环条件判断 )

⚠️ **GitHub.com Fallback** ⚠️