RunLoop整理

437 阅读6分钟

本文主要回答三个问题:

  1. RunLoop是什么,目的是什么
  2. 数据结构,生命周期
  3. 和线程、Timer、GCD、AutoReleasePool的关系。
  4. 如何使用observer观察RunLoop

1 RunLoop是什么

  1. RunLoop,是一个消息处理模式。(包括接收、分发、处理)
  2. 对于main RunLoop来说,他做了一个保活,同时节省资源消耗。
  3. 主线程需要RunLoop来处理各种中断事件(如用户点击等)

上图只是为了演示方便,RunLoop并非基于自旋锁。

1-1 目的

目的是,使线程只在有工作的时候工作,没有工作的时候休眠。 再高一个层面来看,就是为了cpu利用率。

2 工作流程

  1. 线程中定义一个RunLoop,以及一些关心的事件源
  2. 将事件源加入到RunLoop中,进行观察
  3. 所观察的事件发生后,RunLoop会调用事件处理函数

2-1 输入源有哪些

  1. 基于端口的输入源 系统内核内置的源 a. 用户输入(点击等UI操作) b. 网络事件(收到数据等)
  2. 自定义输入源 手动添加的源 a. Timer事件 b. 其他手动加入的源

2.2 Runloop模式

  1. RunLoop包括输入源、Timer、observers、mode。
  2. RunLoop同一时刻只能处于一种mode
  3. 只有与当前mode相关的输入源,事件发生时,才通知observers

3 生命周期

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
};

3-1 唤醒

可以被source0/source1,Timer,dispatch block唤醒

3-2 RunLoop事件处理流程 以及 source0和source1

  • Source0:非基于Port的 Source1:基于Port的,跨进程通信

  • Source0: event事件,只含有回调,需要手动调用RunLoop。(先标记为待处理,然后唤醒RunLoop)

CFRunLoopSourceSignal(source)
CFRunLoopWakeUp(runloop) 
  • Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒 RunLoop的线程。

以上是概念性的东西,下面是我测试及推测的东西:

  1. 就我的理解,RunLoop可以直接接受source1的事件,但更多时候,接受的是source1创建的source0事件(如触摸屏幕的source1事件,会创建UI相关的source0事件)
  2. RunLoop一次循环,会依次处理Timer、source0、dispatch block中的每一类事件,并不只处理一类。(至少通知是每轮所有通知都会发一遍。) 通过添加一个timer,及observer来看:
    1. 一次timer触发,会经历图中2-5的所有周期,并最终到6。
    2. 一次点击触发,会经历图中2-5的周期多次,并最终到6.
  3. 步骤5,不只是source1,也包括dispatch block和Timer。

补充一个图

4 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

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;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};


struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    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 */
};

4-1 Timer

  • NSTimer(CADisplayLink也是加到RunLoop),受RunLoop的Mode影响
  • GCD的定时器不受RunLoop的Mode影响
  • 推测:RunLoop的常规周期应该是小于60Hz,否则无法满足CADisplayLink的需求

5 RunLoop与线程的关系

子线程,没有执行时没有RunLoop,执行起来不会自动创建RunLoop。

  1. 一个线程有1个或0个RunLoop
  2. 延迟创建
  3. 线程结束时销毁RunLoop。 引申:如果一个线程有RunLoop,不主动销毁线程的情况下,RunLoop是否能保活?有observer不能保活;有port可以保活;有待办的task,暂时保活。
  4. 私有(mainThread的RunLoop除外)
  5. 子线程发送网络请求后,如果没有进行保活,就结束并销毁了。 收到并处理响应的并非子线程,虽然响应block的代码和子线程中发送网络请求的代码在一起。
//应用场景:经常在后台进行耗时操作,如:监控联网状态等 不希望线程处理完事件就销毁,保持常驻状态

//开启
- (void)run
{
  //addPort:添加端口(就是source) forMode:设置模式
   [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
  //启动RunLoop
    [[NSRunLoop currentRunLoop] run];
 /*
  //另外两种启动方式
    [NSDate distantFuture]:遥远的未来 这种写法跟上面的run是一个意思
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    不设置模式
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
  */
}

//退出-退出当前线程
[NSThread exit];

6 RunLoop和AutoReleasePool的关系

  1. 进入RunLoop时,创建AutoReleasePool
  2. beforeWaiting时,因为所有事都处理完了,所以释放旧池,并创建新池。(这里没有延后创建新池,可能是因为现在最空,先创建出来,因为是高频使用的)
  3. 线程退出时,也就是RunLoop即将销毁时,释放AutoReleasePool。

7 RunLoop和GCD的关系

GCD的queue就相当于往RunLoop中添加dispatch block任务

  1. 对于主线程中的task queue,相当于加入到了main RunLoop的dispatch block
  2. 对于并发队列中的task,我的理解是,

相当于加入到了子线程的任务队列中

  1. 如果这些并发队列是特定的并发队列,并且是系统在保活,那么是有RunLoop的。
  2. 如果这些并发队列,系统并没有保活,而task最终也没有明确的使用port,那么应该就不涉及RunLoop

dispatch_after 3秒后交给RunLoop,但是RunLoop什么时候处理,就不一定了。

8 创建RunLoop & 创建RunLoopObserver

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

// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0
    , ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);

参考文章

深入理解RunLoop - ibireme

[转]iOS 事件处理机制与图像渲染过程

解密——神秘的RunLoop