Runloop源码

源码地址: https://opensource.apple.com/tarballs/CF/
官方文档介绍: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW3

RunLoop图解

  • 从下面这张图中可以看到一个RunLoop相关的元素主要有这些,下面的循环对应的就是RunLoop,在应用程序启动之后会首先在主线程开启一个RunLoop循环,不段来处理程序的timer事件,来自用户或系统的MatchPort的source事件,知道它超时或结束.

  • 它的左侧有一个容器形状的线程,很形象生动表达了当前的RunLoop被线程所拥有,线程销毁则这个RunLoop也会被销毁

  • runUntilDate:右边的黄色箭头断开后链接说明RunLoop超时会退出循环

  • 右边代表来自各个线程的sources事件

RunLoopMode

  • 它主要是为当前的运行循环提供一个特性的执行模式,所有的事件只有在他们所支持的模式下运行,默认情况下RunLoop是在kCFRunLoopDefaultMode下运行,RunLoop在微观上每次只能执行,如果系统有一些优先级较高的事件一直占用某个Mode那么就会阻塞其他Mode上注册的事件,RunLoop也提供了解决方案,将事件注册到CommonMode中那么它会在每次RunLoop循环周期都会去检查是否有未处理的事件需要执行,当某个事件需要在多个Mode下执行的时候可以考虑将其加入到CommonModes中。 C struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock; /* must have the run loop locked before locking this */
    CFStringRef _name; //Mode名称
    Boolean _stopped; //Mode是否已经停止工作
    char _padding[3];
    CFMutableSetRef _sources0; //Source0事件集合,用户相关的事件
    CFMutableSetRef _sources1; //Source1事件集合,来自MatchPort内核相关的事件,如IOKit的事件分发
    CFMutableArrayRef _observers; //观察者
    CFMutableArrayRef _timers; //timer事件
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet; //用于接收来自MatchPort的事件
    CFIndex _observerMask;
    mach_port_t _timerPort; //用于接收来自timerPort的事件
    Boolean _mkTimerArmed;
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
    };
  • 系统常用的Mode如下

InputSource

  • Input Source异步的发送事件到当前线程,输入源的事件由它的类型决定,通常情况下由两种类型。
    • 一种是监听应用程序Match端口的输入,另外一种则是监听用户自定义事件.
    • 对于运行循环而言,输入源是基于端口或者是自定义并不重,操作系统会实现两种输入源类型,取决于他们如何被触发,基于端口的source1是由kernel自动触发的
    • 自定义的事件source0只能由其他线程手动去触发
  • Port-Based Sources: Cocoa和CoreFunction框架提供了一些类支持我们创建基于端口的事件源,如NSPort,(CFMachPortRef, CFMessagePortRef, or CFSocketRef)
  • Custom Input Sources: CoreFunction提供了CFRunLoopSourceRef,可以通过它来创建自定义的输入源,需要设置如何处理将要执行的事件,怎样关闭source当它从RunLoop中移除时,同时也需要对它的事件机制进行定义
  • Cocoa Perform Selector Sources: Cocoa框架实现了一一个自定义的输入源允许直接在某个线程上执行方法,利用多线程协同处理的方式很大的程度上减少了主线程的阻塞

    • 需要主意的是,当在某个线程执行方法时,这个线程需要有自己的RunLoop,如果是子线程需要自己手动的开启RunLoop.
    • 可以在应用程序启动之后就开启这个线程的RunLoop,这样就能够处理队列上的所有方法
    • 常用的方法如下
      ```Objective-C
      performSelectorOnMainThread:withObject:waitUntilDone
      performSelectorOnMainThread:withObject:waitUntilDone:modes:

    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:

    performSelector:withObject:afterDelay:

    performSelector:withObject:afterDelay:inModes:

    cancelPreviousPerformRequestsWithTarget:
    cancelPreviousPerformRequestsWithTarget:selector:object:

    - __CFRunLoopSource
    ```Objective-C
    struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order; /* immutable */
    CFMutableBagRef _runLoops;
    union {
    CFRunLoopSourceContext version0; /* immutable, except invalidation */
    CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
    };

    TimerSources

    • 事件源会在指定的时间同步的发送一个事件到当前线程,它是线程自我通知的一种实现方式,通常可以用来做连续事件的过滤处理,比如频繁的点击,频繁的文本搜索输入,通过设定一定的时间buff,可以减少频繁冗余的方法调用
    • 尽管它实现了一个基于时间的通知,但它并不是一个真正的timer运行机制,和大都数的input source一样,Timer它也有自己运行的Mode和RunLoop,如果Timer运行的Mode不在当前的RunLoop的观察下,它将不会执行,同样的当一个计时器触发时,如果运行循环正在处理程序例程,timer将不会立即执行,直到下一个RunLoop再去回调它,如果这个RunLoop被销毁它将不会执行。
    • CFRunLoopTimer Objective-C struct __CFRunLoopTimer {
      CFRuntimeBase _base;
      uint16_t _bits;
      pthread_mutex_t _lock;
      CFRunLoopRef _runLoop;
      CFMutableSetRef _rlModes; //Timer可以支持在多个mode下运行
      CFAbsoluteTime _nextFireDate;
      CFTimeInterval _interval; /* immutable */
      CFTimeInterval _tolerance; /* mutable */
      uint64_t _fireTSR; /* TSR units */
      CFIndex _order; /* immutable */
      CFRunLoopTimerCallBack _callout; /* immutable */
      CFRunLoopTimerContext _context; /* immutable, except invalidation */
      };

Run Loop Observers

  • 监听RunLoop的生命周期, Objective-C struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities; /* immutable */
    CFIndex _order; /* immutable */
    CFRunLoopObserverCallBack _callout; /* immutable */
    CFRunLoopObserverContext _context; /* immutable, except invalidation */
    };
  • 对应6种状态的监听 Objective-C kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
  • 监听的流程也是RunLopp的运行流程
    1. 通知观察者运行循环已经进入__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    2. 通知观察者事件触发器已经就绪__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
    3. 通知观观察者非端口事件已经就绪__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
    4. 执行非端口且准备完成的事件源,(block事件)__CFRunLoopDoBlocks(rl, rlm);
    5. 如果一个基于口的输入源以及准备完毕并且等待执行,跳转到第9步if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL))
    6. 通知观察者RunLoop将要开始休眠__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting),__CFRunLoopSetSleeping(rl)
    7. 保持线程一直处于睡眠状态,知道接收到如下事件
      • 一个机基于端口的事件__CFRunLoopServiceMachPort
      • timer事件触发 if (livePort == rl->_wakeUpPort)
      • 为运行循环设置的超时值过期
      • RunLoop显示的被唤醒
    8. 通知观察者线程被唤醒
    9. 处理pending的事件
      • 处理timer事件
      • 处理input source事件
    10. 通知观察者RunLoop已经退出运行循环

RunLoop定义

struct __CFRunLoop {
    CFRuntimeBase _base; //不负责自动引用计数
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;       //RunLoop被唤醒时的port  // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData; //重制当前RunLoop的状态
    pthread_t _pthread;  //当前RunLoop所运行的线程
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems; 
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};
  • RunLoop运行的状态 _perRunData->stopped = 0x53544F50; // 'STOP'
    _perRunData->ignoreWakeUps = 0x57414B45; // 'WAKE'
    __CFRunLoopSetSleeping
    __CFRunLoopSetDeallocating
  • RunLoop自带一个互斥锁,用于锁定当前线程资源,避免多线程冲突 CF_INLINE void __CFRunLoopLockInit(pthread_mutex_t *lock) {
    pthread_mutexattr_t mattr;
    pthread_mutexattr_init(&mattr);
    pthread_mutexattr_settype(&mattr, PTHREAD_MUTEX_RECURSIVE);
    int32_t mret = pthread_mutex_init(lock, &mattr);
    pthread_mutexattr_destroy(&mattr);
    if (0 != mret) {
    }
    }

创建RunLoop

- (void)mainThread {
    // The application uses garbage collection, so no autorelease pool is needed.
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
    // Create a run loop observer and attach it to the run loop.
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
            kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
 
    if (observer)
    {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
 
    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
 
    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}

Configuring Run Loop Sources

  • 创建自定于源需要具备以下信息

    • 输入源的处理信息,CFRunLoopSource的事件
    • 调度函数,让客户端知道什么时候需要处理事件
    • 一个callback回调函数
    • 输入源销毁的函数(cancellation)
  • 下图描述了2个线程之间的RunLoop的通信过程,应用程序Main Thread维护了对Input source的引用,Main Thread的任务通过Command Buffer将事件传递给WorkerThread,(因为Worker Thread的Input Source和Main Thread都可以访问Command Buffer,所以必须同步访问。)一旦发出命令,Main Thread就向Input Source发出信号,并唤醒Worker Thread的Runloop。在接收到wake up命令后,Runloop调用Input Source的处理程序,该处理程序处理在Command Buffer中找到的命令。

RunLoop与GCD的关系

  • GCD是Grand Center Dispatch,由系统底层API调用,精度更高,在RunLoop的实现中,才用了GCD来监听RunLoop是否超时
  • 执行GCD派发的block任务时,如dispatch_async(dispatch_get_main_queue(), block)会向主线成的RunLoop发送信息,并在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE执行具体的block事件
  • 其他线程如果有设置RunLoop也是如此.主要是看GCD的block派发到哪个线程,子线程默认不会开启RunLoop

RunLoop与内存关系

  • 通过在RunLoop开启和结束注册2个不同Observers来实现的RunLoop创建和结束监听
  • 监听到开始事件时通过_objc_autoreleasePoolPush创建自动释放池,
  • 监听到结束事件时通过_objc_autoreleasePoolPop销毁释放池

RunLoop与事件响应

  • 系统注册了一个Source1的输入源,用来接收系统事件,其回调函数是__IOHIDEventSystemClientQueueCallback,当接收到来自硬件的事件时,首先由IOKit.framework生成一个IOHIDEvent事件,并交由SpringBoard接收,
  • SpringBoard接收按键(锁屏/静音等),触摸,加速,接近传感器等事件,通过mach port 转发给需要的App进程。
  • 进而触发Source1事件的回调,通过_UIApplicationHandleEventQueue将事件派发到应用程序的事件对类上,UIApplication接收到事件后再往下传递
  • _UIApplicationHandleEventQueue会将IOHIDEvent包装成UIEvent进行分发。

利用RunLoop保持线程常驻

  • 例如在AFNetworking中开启了一个常驻线程用来处理网络请求事件,避免了线程的频繁创建销毁所带来的开销
  • 在对应的线程开启一个RunLoop,设置MatchPort作为它的输入源,然后开启一个运行循环
  • addPort:forMode: 在RunLoop指定模式下添加一个输入源

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //通常情况其它线程访问这个pot就能给这个线程的RunLoop发送消息,此处只是为了让那个RunLoop有事件源被监听,避免退出
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
    }
    }
  • 通过dispatch once token,原子执行,安全的生成一个线程,下面的例子开启了一个信息的线程AFNetworking(在这个线程内部的bloc块中已经设置了它的名字)

    /// target
    ///     - 只接收消息的对象
    /// selector
    /// - 用于发送消息给对象(target),此方法只能有一个参数并且不能有返回值
    /// argument
    /// - 发送给target的参数
    /// - (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument;
    + (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
    //创建一个新的线程
    _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
    //异步开启一个新的线程,并在此线程上调用它的入口方法
    [_networkRequestThread start];
    });
    return _networkRequestThread;
    }
posted @ 2020-10-22 03:28  阿甘左  阅读(517)  评论(0编辑  收藏  举报