Run Loops基础概念篇一

Run Loops

下面会用一些陌生的或者容易让人混淆的字符,我们先来统一概念再继续,这样能够让你更加愉快的阅读:
runloop:iOS一个底层机制的专业术语。
run loop:一种运行的循环。
Handler:handlePort:customSrc:mySelector:timerFired:,指开发者希望当进入run loop的时候要执行的操作
run-loop observer:观察runloop行为的观察者
run-loop mode:每个runloop都要指定一种mode,一种mode可以对应多个input source
runloop object:runloop相关的对象,这里分Cocoa和CoreFoundation中的

Runloop是线程架构相关的一部分,是事件处理的循环。你可以用它来安排循环执行任务,协调相关的事件(kCFRunLoopEntry、kCFRunLoopExit等)。
用几行示意代码来展示:

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

Runloop实际上是一个管理要处理的事件和消息的对象,并为开发者提供了一个入口来执行run loop的逻辑部分。一直处于类似于“接受消息-->等待-->处理”这样的一个循环汇总

Runloop的目的:一般我们自己写的线程在执行完相应的任务就会退出,而runloop让你的线程在有事干的时候干活,没事干的时候休息,节省系统资源

Runloop的管理不全是自动的,对于自己的线程,必须要自己添加代码,并在适当的时候启动Run loop来响应接受的事件。
Cocoa和Core Foundation提供了runloop objects来帮助你配置和管理线程的run loop。你的Application不需要自己创建这些对象(像alloc、init这些方法)。每个thread,包括Application的主线程,都已经有runloop object,有方法可以直接获取这这些对象,类似于这样的[NSRunLoop currentRunLoop]

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

苹果提供了CFRunLoopGetMainCFRunLoopGetMain这两个方法来创建runloop,如果它不被调用,线程中就不会有runloop。

Cocoa中的NSRunLoop,和Core Foundation中的CFRunLoopRef等Run loop相关的类。它们的方法一般也是一一对应的。

唯一不同的是:你自己创建的线程需要明确的代码来让runloop跑起来,比如[runLoop run]这样的方法。但是对于主线程的runloop,系统会在App起来的时候自动设置并跑主线程里面的runloop,这是Application运行起来的一部分。

Anatomy of Run Loop(剖析runloop)

“一个运行的循环”,看它名字的意思就能猜得到。它会循环不断的进入线程,在runloop进入之后做一些自己的事情。你的code提供了状态的控制用来实现runloop真正的循环部分。—— 换句话说,你的code提供whilefor循环来驱动run loop。在你的循环中,你使用一个runloop object来运行事件处理的code,runloop接受事件并调用你已经准备好的方法。

/*
 这里doFireTimer方法就是你要处理的事件的code,*runloop*在从sleep状态被wakeup之后会执行改方法。
*/
[NSTimer scheduledTimerWithTimeInterval:0.016
                                     target:self
                                   selector:@selector(doFireTimer:)
                                   userInfo:nil
                                    repeats:YES];

上面我们提到“你使用一个runloop object来运行事件处理的code”,这里的事件是从哪里来呢?即事件源是什么?

事件源分两类:

  • Input source所驱动的异步事件,一般它用来从一个线程向另一个线程发送事件或者从一个Application向另一个Application。
  • Timer sources所驱动的同步事件,事件发生在一个已经预先设定好的时间,或者在重复的事件间隔发生。

当事件到达的时候,这两种source会按照系统指定的步骤来处理事件。

下图展示了一个runloop的概念结构和各种不同的sources。Input sources发送一个异步的事件来执行相对应的handler(例如,图中的hanlePortmySelector等),并通过runUntilDate:方法(调用thread相关的NSRunLoop对象)来退出。Timer sources发送事件到runloop的handler,但不会引起runloop的退出。

runloop-1.jpg

除了处理输入源对应的handler,Runloop生成关于run loop行为的通知。注册run-loop 的observer接受这些通知。使用它们在thread上做一些其他的操作。你可以在你的线程中使用Core Foundation里面的类来创建run-loop observer

Run Loop Modes

run-loop mode是被监听的Input sourcesTimers的集合,同时还是被通知的run-loop observer的集合,大家可能比较疑惑怎么是两种东西的集合(source和obaserver),下面是我的理解,从代码出发:
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
线程的每个Source都要指定一个Mode,这里是NSDefaultRunLoopMode
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopCommonModes);
观察者是观察该Runloop的某些Modes,kCFRunLoopCommonModes是mode的集合,每种mode都可以添加到Common中

每次你运行你的runloop的时候,要指定一个具体的mode在runloop里面跑。在进入runloop的时候,只有和这个mode相关联的source能被对应的run-loop observer监听到,被允许向它们传递事件。而其他mode相关的source会一直等待,直到run-loop mode和source指定的mode相对应的时候才开始传递事件。

在你的代码中,你通过具体的名称来定义mode(kCFRunLoopDefaultModekCFRunLoopCommonModes等)。Cocoa和Core Foundation都定义了默认的mode和一些经常使用的mode。

你也能通过简单的字符串来自定义自己的mode。尽管custom mode定义起来好像很随意,但使用起来可不能随意啊。对于任何mode,你必须要确保添加一个或者更多的sourcestimers或者run-loop observer,如果你没有做到,run loop就会直接退出。

//如果没有第二行代码,runloop会自动退出
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];

你可以使用mode过滤掉不想要的source。大多数情况下,你只需要用系统定义的“default”mode来运行你的runloop。

Note:Mode是根据事件source来匹配的而不是根据事件的type。例如,你不可以使用鼠标的按下事件或者键盘按下事件来匹配mode。

下面是一些系统定义好的Modes

Mode Name
Default NSDefaultRunLoopMode(Cocoa)kCFRunLoopDefaultMode(Core Foundation)
Connection NSConnectionReplyMode(Cocoa)
Modal NSModalPanelRunLoopMode(Cocoa)
Event tracking NSEventTrackingRunLoopMode(Cocoa)
Common modes NSRunLoopCommonModes(Cocoa)kCFRunLoopCommonModes(Core Foundation)

Input Sources

Input source通过异步的方式向你的线程传递事件。事件的源取决于input source的类型。input source大概分为两类:

  • Port-based port
    Port-based port监控你application的Mach port。
  • Custom input source
    Custom input source监控自定义的事件source。

runloop不关心是哪种input source,这两种input sources系统都有实现。这两个input source唯一的不同就是它们如何发送信号。Port-based port通过内核自动发送信号。Custom input source只能从其他线程接受手动发送的信号。

当你创建一个input source,你把它赋值到runloop的mode。mode会影响被监控的input source。大多数情况,你在NSDefaultRunLoopMode下运行runloop,但是你也可以指定自定义的mode。如果一个input source不在当前被监控的mode中,它产生的一些事件将会被暂时的放在一边,直到下一个run loop的mode和input source的mode相同才开始处理这个input source产生的事件。

接下去的部分描述了一些input source

Port-Based Sources

Cocoa和Core Foundation通过提供端口相关的对象和方法来创建Port-Based Source。例如,在Cocoa中,你没必要直接创建input source。你使用NSPort的方法[NSPort port],添加port到run loop中。Port object会为你创建和配置所需的Port-Based Sources

在 Core Foundation中,你必须要自己创建port和它的run loop source。用 CFMachPortRef, CFMessagePortRef, CFSocketRef这些不透明指针创建相应的对象。

如何配置和设置自定义的Port-Based Sources,请看 Configuring a Port-Based Input Source

Custom Input Sources(Core Foundation)

为了创建自定义的input source,你必须使用Core Foundation中的CFRunLoopSourceRef不透明类相关的方法。你需要在回调方法中配置这个Custom Input Sources。Core Foundation会在配置Custom Input Sources、处理输入事件和当source要从runloop中移除的时候调用这几个回调。

对于如何创建一个custom input source请看这里Defining a Custom Input Source

Cocoa Perform Selector Sources(Cocoa)

除了Port-Based Sources,Cocoa定义了一个custom input source,这允许你向任何线程中发送选择子。和Port-Based Sources一样,执行选择子selector的请求在同一个线程上是连续的,当多个方法同时向线程发送请求的时候可能会导致同步的问题。和Port-Based Sources不同的是:一个perform selector source执行完它的selector,它自己会从runloop中移除。

当向另一个线程发送selector请求时,目标线程必须有一个active的runloop。这意味着你必须等待,直到该线程中runloop的状态变成 active。因为主线程会自己开始一个runloop,只要系统一调用Application delegate的applicationDidFinishLaunching:(这时候系统的runloop就创建出来了),你就可以向主线程发送selector请求。每次Runloop会处理所有队列的selector请求,而不是只处理一个队列的请求。下面列出了一些perform方法

Methods
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:
Timer Sources

Timer sources会在将来的某个时间,发送同步事件到你的当前的线程。Timer是线程用来通知它自己的一种方式。
尽管它是基于时间的通知,一个timer并不能保证在准确的时间点执行事件。和其他的Input source一样,Timer和你run loop指定的mode有关系。如果Timer的mode不是你当前runloop正在跑的mode,它是不会被触发的,直到你的runloop运行的的mode是你的Timer所支持的。类似的,如果一个Timer被触发,但是当runloop在执行Handler,Timer将会一直等待,直到下次再次进入runloop的时候才会执行。如果这个runloop不再跑了,这个Timer将永远不会被触发。

你可以配置Timer来运行事件,一次或者不停重复。一个重复的Timer会基于设定好的触发时间,自动重新安排它自己到run loop中,但这个时间间隔并不一定准确。例如,如果一个Timer在一个特定时间被触发,每5秒重复一次。即使实际的触发时间被延迟了,被安排的触发时间总落在5秒的延迟时间间隔之内。如果触发时间被延迟太多,会导致它错过了一次或者更多被安排到run loop的机会。在一个不恰当的时机被触发之后,Timer则会被安排到下一次run loop。
关于配置timer sources的更多信息,请看Configuring Timer Sources。更多相关信息NSTimer Class ReferenceCFRunLoopTimer Reference

Run Loop Observers

一般的source是在一个异步或者同步的事件发生的时候被触发的,而run-loop observer是在runloop它执行的时候触发的。你可以通过run-loop observer来准备一些线程待处理的事件,或者在线程sleep之前,在线程中做一些事。run-loop observers可以观察到下面的一些事件:

  • 进入runloop的时候
  • runloop将要处理timer的时候
  • runloop将要处理input source的时候
  • runloop将要sleep的时候
  • runloop已经醒来,还没有开始处理event的时候
  • runloop退出的时候

你可以使用Core Foundation向Application添加run-loop observer。为了创建一个run_loop observer,你需要使用CFRunLoopObserverRef类。这个类会追踪你自定义callback函数、你感兴趣的CFRunLoopActivity

和Timer相似,run-loop observer可以只运行一次,或者不停重复。只运行一次的run-loop observer在一次run loop之后就会从runloop中移除。当你创建run-loop observer的时候,你要决定r是一次还是不停重复。

对于如何创建一个run-loop observer的例子,看 Configuring the Run Loop,更多相关信息CFRunLoopObserver Reference

The Run Loop Sequence of Events

每次你运行runloop,你线程的runloop会处理之前挂起的事件,并且向run-loop observer发送通知。runloop执行的顺序如下面所列出的:

  1. 通知run-loop observer,进入runloop
  2. 通知run-loop observer,已经准备好的Timer将要触发
  3. 通知run-loop observer,以及准备好的no-port-based input source将要被触发
  4. 触发no-port-based input source
  5. 如果一个port-based input source准备好等待触发,立即处理该事件,再从第九步开始执行
  6. 通知run-loop observer,线程将要sleep

-----------------------------------sleepping-------------------------------------

  1. 线程sleep直到下面的事件发生:

    • port-based input source事件到达
    • 一个Timer触发了
    • runloop超时了
    • runloop被手动唤醒
  2. 通知run-loop observer,线程将要被唤醒

  3. 处理挂起的事件

    • 如果一个用户定义的timer触发了,处理相关事件,重新开始循环,回到第二步继续执行
    • 如果input source触发,传递相应的事件
    • 如果runloop明确的被唤醒,但是还没有超时,重新开始循环,回到第二步继续执行
  4. 通知run_loop observerrunloop已经退出

Tip:no-port-based input source就是我们常说的source0,port-based input source就是source1

因为Timer和input source的通知是在事件被触发之前,所以这之间有一个过渡时间。如果你要在这个时间做一些事情,你可以使用sleepawake-from-sleep 通知来帮助你获得这段时间的控制权。

一个runloop可以通过使用run loop object被唤醒。其他的事件也可能引起runloop被唤醒。例如,添加no-port-based input source来唤醒runloop以至于input source可以被立即处理,而不是等待其他的事件发生。


iOS系统中有很多用到了RunLoop的地方:

  • AutoreleasePool:监听kCFRunLoopEntry事件,保证在所以回调之前创建自动释放池;监听BeforeWaiting事件,释放旧的线程池,创建新的线程池;监听kCFRunLoopExit事件,保证线程池最后被释放。
  • 事件响应:通过port-based input source监听用户的事件,并转发给App进程,如用UIControl的事件。
  • 手势识别:监听kCFRunLoopBeforeWaiting事件在事件的回调函数中标记待处理的手势,并处理手势。
  • 界面更新:setNeedsLayoutsetNeedsDisplay都会将该view标记为待处理,等到下一个runloop的时候会布局UI。
  • 定时器:就是一个CFRunLoopTimerRef
  • PerformSelecter:将方法扔到某个线程中,在下一个run loop的时候执行。
  • 关于GCD:dispatch_async(dispatch_get_main_queue(), block)向主线程的run loop发送消息,唤醒run loop。并执行block中的内容。

PPT总结一下:

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

推荐阅读更多精彩内容