iOS RunLoop

RunLoop普遍存在于各个系统中, 是比较底层、基础同时又很重要的一个机制.
所以对RunLoop的学习也是对技术的提高, 也能进一步帮助我们理解用户操作、界面刷新等.

RunLoop的基本概念

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出.
RunLoop就是一个机制,让线程能随时处理事件但并不退出.
用伪代码来写就是:

func runloop() {
    init();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

类似于这样, 永远处于一个循环中, 直到收到了quit的消息.
这样的循环, 称为Event Loop. Event Loop存在于很多操作系统中或者框架中.

关键点在于: 如何管理事件/消息, 如何让线程在没有处理消息时休眠(wait)以避免资源占用、在有消息到来时立刻被唤醒.

所以, RunLoop 实际上就是一个对象, 这个对象管理了其需要处理的事件和消息, 并提供了一个入口函数来执行上面 Event Loop 的逻辑. 线程执行了这个函数后, 就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中, 直到这个循环结束(比如传入 quit 的消息), 函数返回.

OSX/iOS 系统中, 提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef. CFRunLoopRef 是在 CoreFoundation 框架内的, 它提供了纯 C 函数的 API, 所有这些 API 都是线程安全的. NSRunLoop 是对 CFRunLoopRef 的封装, 提供了面向对象的 API, 但是这些 API 不是线程安全的.

RunLoop与线程的关系

线程和 RunLoop 之间是一一对应的, 其关系是保存在一个全局的 Dictionary 里.线程刚创建时并没有 RunLoop, 如果你不主动获取, 那它一直都不会有. RunLoop 的创建是发生在第一次获取时, RunLoop 的销毁是发生在线程结束时. 你只能在一个线程的内部获取其 RunLoop(主线程除外).

RunLoop对外的接口

在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露, 只是通过 CFRunLoopRef 的接口进行了封装.
一个RunLoop包含的元素, 以及存储所用数据结构, 以及它们之间的关系, 如下图所示:

RunLoop

一个 RunLoop 包含若干个 Mode, 每个 Mode 又包含若干个 Source/Timer/Observer. 每次调用 RunLoop 的主函数时, 只能指定其中一个 Mode, 这个Mode被称作 CurrentMode. 如果需要切换 Mode, 只能退出 Loop, 再重新指定一个 Mode 进入. 这样做主要是为了分隔开不同组的 Source/Timer/Observer, 让其互不影响.

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

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

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

有一些数据类型是能够在 Core Foundation Framework 和 Foundation Framework 之间交换使用的. 这意味着, 对于同一个数据类型, 你既可以将其作为参数传入 Core Foundation 函数, 也可以将其作为接收者对其发送 Objective-C 消息(即调用ObjC类方法), 这种在 Core Foundation 和 Foundation 之间交换使用数据类型的技术就叫 Toll-Free Bridging.

CFRunLoopObserverRef 是观察者, 每个 Observer 都包含着一个回调(函数指针), 当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
};

上面的 Source/Timer/Observer 被统称为 mode item, 一个 item 可以被同时加入多个mode. 但一个 item 被重复加入同一个 mode 时是不会有效果的. 如果一个 mode 中一个 item 都没有, 这个 RunLoop 就会直接退出, 不会进入到循环中.

RunLoop 的 Mode

CFRunLoopMode 和 CFRunLoop 的结构大致如下:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

commonModes: 一个 Mode 可以将其标记为 common (通过将其 ModeName 添加到 RunLoop 的 commonModes 中). 每当RunLoop 的内容发生变化时, RunLoop 都会自动将 _commonMOdeItems 里的 Source/Timer/Observer 同步到具有 common 标记的所有 Mode 里.

应用场景举例: 主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode. 这两个 Mode 都已经被标记为 Common 属性. DefaultMode 是 App 平时所处的状态, TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态, 当你创建一个 Timer 并加到 DefaultMode 时, Timer 会得到重复回调, 但此时滑动一个TableView时, RunLoop 会将 mode 切换为 TrackingRunLoopMode, 这时 Timer 就不会被回调, 并且也不会影响到滑动操作.

有时你需要一个 Timer, 在两个 Mode 中都能得到回调, 一种办法就是将这个 Timer 分别加入这两个 Mode. 还有一种方式, 就是将 Timer 加入到顶层的 RunLoop 的 commonModeItems 中. commonModeItems 被 RunLoop 自动更新到所有具有 Common 属性的 Mode 里去.

只能通过 mode name 来操作内部的 mode, 当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时, RunLoop会自动帮你创建对应的 CFRunLoopModeRef. 对于一个 RunLoop 来说, 其内部的 mode 只能增加不能删除.

苹果公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode, 可以用这两个 Mode Name 来操作其对应的 Mode.

同时苹果还提供了一个操作 Common 标记的字符串: kCFRunLoopCommonModes (NSRunLoopCommonModes), 你可以用这个字符串来操作 Common Items, 或标记一个 Mode 为 "Common". 使用时注意区分这个字符串和其他 mode name.

RunLoop 的内部逻辑

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

推荐阅读更多精彩内容