RunLoop 01 - 原理

RunLoop 01 - 原理

RunLoop 的概念

  • 一个 RunLoop 就是一个处理事件的循环,用来不停的调度工作及处理输入事件。
  • RunLoop 是线程基础框架的一部分,可以让一个线程保持运行状态并能随时处理事件,当没有事件需要处理时进行休眠以避免占用资源。
  • iOS 程序主线程中的 RunLoop 可以保持程序的持续运行,并处理各种事件(触摸事件、定时器事件等)。

事件循环模型:

function loop() {
    initialize();
    do {
        var message = get_next_message();
    } while (message != quit);
}

RunLoop 应用范畴

  • Timer、PerformSelector
  • GCD Async Main Queue
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • AutoreleasePool

获取 RunLoop

  • NSRunLoop
    • 获取当前线程的 RunLoop:[NSRunLoop currentRunLoop]
    • 获取主线程的 RunLoop:[NSRunLoop mainRunLoop]
  • CFRunLoopRef(源码
    • 获取当前线程的 RunLoop:CFRunLoopGetCurrent()
    • 获取主线程的 RunLoop:CFRunLoopGetMain()

CFRunLoopRef 在 CoreFoundation 框架内,提供了一套纯 C 函数的 API,所有 API 都是线程安全的。
NSRunLoop 在 Cocoa 框架内,是对 CFRunLoopRef 的封装,提供了一套面向对象的 API,但是这些 API 不是线程安全的。

RunLoop 与线程

  • 线程与 RunLoop 之间是一一对应的,对应关系保存在一个全局的 Dictionary 中,key 为线程,value 为 RunLoop。
  • 线程刚创建时没有 RunLoop,第一次获取时会创建,iOS 程序启动后会默认为主线程创建 RunLoop。
  • RunLoop 的销毁发生在线程结束时。
  • 只能在线程内部获取其 RunLoop(主线程除外)。

CoreFoundation 中 RunLoop 相关的类

  1. CFRunLoopRef
  2. CFRunLoopModeRef(没有对外暴露,通过 CFRunLoopRef 的接口进行了封装)
  3. CFRunLoopSourceRef
  4. CFRunLoopTimerRef
  5. CFRunLoopObserverRef

它们之间的关系如下:


CFRunLoopRef & CFRunLoopModeRef 结构

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
    pthread_t _pthread;               // RunLoop 对应的线程
    CFMutableSetRef _commonModes;     // Set<String - Mode Name>
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current RunLoop Mode
    CFMutableSetRef _modes;           // Set<Mode>
};
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name,例如 "kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set<Source>
    CFMutableSetRef _sources1;    // Set<Source>
    CFMutableArrayRef _observers; // Array<Observer>
    CFMutableArrayRef _timers;    // Array<Timer>
};
  • Mode Item:
    • Source/Timer/Observer 被统称为 Mode Item。
    • 一个 Item 可以被同时加入多个 Mode。
    • 一个 Item 被重复加入同一个 Mode 时在某一次循环中是不会被重复处理的。
  • Mode:
    • CFRunLoopModeRef 代表 RunLoop 的运行模式。
    • 一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Mode Item。
    • 每次调用 RunLoop 主函数时只能指定其中一个 Mode 作为 _currentMode。
    • 如果需要切换 Mode,只能退出当前 Loop,再重新指定一个 Mode 进入。这样可以分隔开不同组的 Mode Item,让其互不影响。
    • 如果 Mode 中没有 Item,则 RunLoop 会直接退出,不进入循环。

Cocoa 预定义的 RunLoop Mode

  • Default:
    • NSDefaultRunLoopMode、kCFRunLoopDefaultMode。
    • 默认设置,一般情况下使用。
  • Event Tracking:
    • NSEventTrackingRunLoopMode、UITrackingRunLoopMode。
    • 用于处理拖拽、用户交互事件。
  • Connection:
    • NSConnectionReplyMode。
    • 用于处理 NSConnection 相关事件,开发者一般用不到。
  • Modal(Only macOS):
    • NSModalPanelRunLoopMode。
    • 用于处理 modal panels 事件。
  • Common:
    • NSRunLoopCommonModes、kCFRunLoopCommonModes。
    • 模式集合,包括 Default、Event Tracking、Modal(Only macOS),几乎可以处理所有事件。
    • 并不是一个具体的 Mode,相当于将第二条中提到的模式分别标记为 “Common”。

iOS 中公开提供的 Mode 有 Default 和 Event Tracking,可以通过 Mode Name 来操作对应的 Mode。

除以上列出的 Mode 外系统框架还自定义了很多 Mode,例如 GSEventReceiveRunLoopMode 处理系统事件,UIInitializationRunLoopMode 在 App 启动过程中使用。

更多系统框架 Mode 可以查看这里

Common Modes 补充

  • 一个 Mode 可以将自己标记为 “Common”,通过将其 Mode Name 添加到 RunLoop 的 _commonModes中。
  • 每当 RunLoop 的内容发生变化时,会自动将 _commonModeItems 里的 Source/Timer/Observer 同步到标记为 “Common” 的 Mode 里。

iOS 主线程的 RunLoop 里有两个预置的 Mode(Default 和 Event Tracking),且都已被标记为 “Common”。
Default 是 App 平时所处的 Mode,Event Tracking 是追踪 ScrollView 滑动时的 Mode。

  • 应用场景举例:
    • 问题:以 scheduledTimerWithTimeInterval 方式触发的 Timer(在主线程中),在滑动列表时为什么会暂停?
    • 原因:滑动列表时,主线程 RunLoop 的 Mode 由 Default 切换到了 Event Tracking,Timer 默认运行在 Default 中,Default 被退出后 Timer 自然就停止工作了。
    • 解决方法:
      1. 将 Timer 分别加入 Default 和 Event Tracking 两种 Mode 中。
      2. 将 Timer 加入 _commonModeItems 中,_commonModeItems 会被自动同步到所有标记为 “Common” 的 Mode 中。
      3. 将 Timer 放到子线程中,开启子线程的 RunLoop,可以保证与主线程互不干扰。

将 Timer 加入 _commonModeItems 中的方法:
CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopCommonModes);
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

RunLoop Mode 相关的 API

  • CFRunLoop 对外暴露的管理 Mode 的接口有两个:

    CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
    CFRunLoopRunInMode(CFStringRef modeName, ...);
    
  • Mode 暴露的管理 Mode Item 的接口有下面几个:

    CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
    CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
    CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
    CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
    CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
    CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
    

只能通过 Mode Name 来操作 RunLoop 内部的 Mode。
当传入一个新的 Mode Name 时,RunLoop会自动创建对应的 CFRunLoopModeRef。
RunLoop 内部的 Mode 只能增加不能删除。

CFRunLoopSourceRef

typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
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;
};

CFRunLoopSourceRef 是事件产生的地方。Source 有两个版本:Source0 和 Source1。

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

CFRunLoopTimerRef

typedef struct __CFRunLoopTimer * CFRunLoopTimerRef;
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval; /* immutable */
    CFTimeInterval _tolerance; /* mutable */
    uint64_t _fireTSR; /* TSR units */
    CFIndex _order; /* immutable */
    CFRunLoopTimerCallBack _callout; /* immutable */
    CFRunLoopTimerContext _context; /* immutable, except invalidation */
};

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒以执行回调。

CFRunLoopObserverRef

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
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 */
};

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

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

RunLoop 的运行逻辑

  1. 通知 Observers:即将进入 Loop
  2. 通知 Observers:即将处理 Timers
  3. 通知 Observers:即将处理 Sources0
  4. 执行 Blocks
  5. 处理 Sources0
  6. 执行 Blocks
  7. 如果存在 Sources1,跳转到第 11 步,goto handle_msg
  8. 通知 Observers,即将休眠
  9. 进入休眠,等待消息唤醒
  10. 通知 observers,结束休眠
  11. handle_msg: 处理接收到的消息
    • 处理 Timer
    • 处理 Dispatch 到 Main Queue 的 Block
    • 处理 Sources1
  12. 执行 Blocks
  13. 根据前面的执行结果,决定如何操作
    • 回到第 2 步
    • 退出 Loop
      • 进入 Loop 时通过参数指定了处理完事件就返回
      • 超出进入 Loop 时传入参数标记的超时时间
      • 被外部调用者强制停止
      • Mode 中没有 Source/Timer/Observer
  14. 通知 observers:退出 Loop
  • sources0
    • 处理触摸事件
    • performSelector:onThread:
  • sources1
    • 基于 Port 的线程间通信
    • 捕捉系统事件
  • timers
    • NSTimer
    • performSelector:withObject:afterDelay:
  • observers
    • 用于监听 RunLoop 的状态
    • UI 刷新(BeforeWaiting)
    • AutoreleasePool(BeforeWaiting)

RunLoop 休眠的实现原理

15662181178751.jpg

参考资料

官方 RunLoop 文档
ibireme - 深入理解RunLoop
reallychao - iOS刨根问底-深入理解RunLoop
MJ - iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 转自bireme,原地址:https://blog.ibireme.com/2015/05/18/runloop/...
    乜_啊_阅读 1,322评论 0 5
  • RunLoop 的概念 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线...
    Mirsiter_魏阅读 614评论 0 2
  • RunLoop的概念 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程...
    IOS学渣阅读 455评论 1 4
  • 转载:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling阅读 1,432评论 0 13
  • RunLoop 是 iOS 和 OS X 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,...
    iOS_Alex阅读 895评论 0 10