001-深入理解iOS RunLoop

请用一句话描述iOS开发中的 RunLoop。

RunLoop就是一个“do {}while;”负责给各个线程派“活”的。

目录

  • 什么是RunLoop
  • RunLoop的结构
  • RunLoop的实现
  • RunLoop什么时候用
  • RunLoop应用举例

  • 什么是RunLoop

一般来说,代块在执行的时候都是从上到下的运行的(函数或方法间相互调用的情况不是?呵呵),运行完了,这个线程就结束,退出了。我们先只考虑一个app进程中只有主线程的情况。那么问题来了,我们在主线程中将功能代码写好,用户启动app,主线程代码运行完了,线程退出,进程退出,然后程序会退出。也就是说,程序这样就不会等待用户的操作(点击,滑动等等),我们也许会想到scanf这样的函数,但是这样的函数也是一次性的啊,用户等着用户输入。用户输入完成之后呢?这句代码过掉,并且后续代码执行完,照样退出,不会长时间的等待用户的操作。

聪明的你可能已经想到办法了:写一个for或者while之类的循环不就可以了么?每次处理完用户的事件,就再循环回来,等待客户的下一次交互操作,收到用户的操作事件,就在循环中处理,然后进入下一次循环,等待用户的操作。

这就是RunLoop需要实现的最基本的功能。RunLoop需要做的事情如下:

1.处理和分发事件/消息
2.让线程有事做事,没事休息,节省资源
3.让线程不会马上退出(停在循环中)
----------------------------------

官方文档中RunLoop的描述.png

官方文档中将source和timer分开了,一种是输入源(input source),另一种是时间源(timer source),文档中另一个自然段还提了一下observer。官方目前提供了两个对象来让我们管理RunLoop,NSRunLoopCFRunLoopRef(好吧CFRunLoopRef是个结构体,NSRunLoop就不是吗?)NSRunLoop是基于CFRunLoopRef封装的。所以,下面我们只说CFRunLoopRef。


  • RunLoop的结构

先上图,借用bireme的图(感谢)


RunLoop内部结构.png

一个RunLoop内部可能包含若干个Mode,每个Mode内部可能包含若干个source,observer,timer。

  • RunLoop的定义
struct __RunLoop {
        CFMutableSetRef _commonModes;//包含被标记为common的mode
        CFMutableSetRef _commonModeItems;//一个set,包含source、observer、timer
        CFRunLoopModeRef _currentMode;//记录当前关联<监听的?>的mode
        CFMutableSetRef  _modes;//runloop中所有的modes
        ···
  }
//mode的定义
struct __CFRunLoopMode {
         CFStringRef _name;//本mode的名字
         CFMutableSetRef _source0;//source0组成的set
         CFMutableSetRef _source1;//source1组成的set
         CFMutableArrayRef _observers;//观察者们组成的数组
         CFMutableArrayRef _timers;//定时器数组
        ···
  }

先说RunLoop中的_commonModes,我们可以使用官方提供的

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);

来讲一个mode添加到_commonModes集合中,这样,每当RunLoop内容发生变化的时候,RunLoop会自动将_commonModeItems中的source、timer、observer同步到_commonModes中所有的mode中去。
这也解释了图片轮播器之类的Timer最好添加到NSRunLoopCommonModes中去,而不是NSDefaultRunLoopMode。因为如果将timer加入NSDefaultRunLoopMode,即只有这个NSDefaultRunLoopMode包含这个timer,正常情况下,mainLoop是run in NSDefaultRunLoopMode,而在我们滑动屏幕的时候,mainLoop会切换到UIEventTrackingRunLoopMode中运行,该mode不包含图片轮播器的timer,所以滑动时timer不被监听,图片轮播也就会暂停(其实在编辑textField的时候也会停,道理一样),直到mainLoop再次切换到包含timer的mode下运行。而如果将timer加入到NSRunLoopCommonModes中,内部相当于将timer加入到了_commonModeItems集合中,mainLoop会将该集合中所有的items分发(添加或关联,官方文档用的是associated)_commonModes集合中的每个mode中,UIEventTrackingRunLoopMode就是其中之一。

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(test) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

CoreFoundation中对应的函数是:

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

RunLoop中的_currentMode就不解释了,modes是所有添加到当前loop中的集合。
在CoreFoundation框架中,mode是通过名字(一个字符串)来区分的。

"Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。"

source1是基于 mach port的,用来监听我们app的mach ports的收到的事件和消息。它能够主动触发事件和回调,并且唤醒runloop。
关于source,官方文档将source分为两类:Port-Based Sources 和Custom Input Sources。

Input sources deliver events asynchronously to your threads. The
source of the event depends on the type of the input source, which is generally one of two categories. Port-based input sources monitor your
application’s Mach ports. Custom input sources monitor custom sources of events.As far as your run loop is concerned,it should not matter whether an input source is port-based or custom. The system typically implements input sources of both types that you can use as is.

The only difference between the two sources is how they are signaled. Port-based sources are signaled automatically by the kernel, and custom sources must be signaled manually from another thread.

两种输入源的唯一区别就是:基于port的源通过内核自动通知的(不用我们手动发消息),而自定义的源需要在其他线程中手动发送消息(通知)。

一个是内核完成通信,一个是我们手动发送。<后面举例说明>

当runloop在当前mode下运行,mode中的_observers会得到对应的通知。比如,马上进入runloop时,将要处理source、timer、block时,将要进入睡眠时,退出runloop时。就是一些行为和声明周期的通知。

_timers略,上面简单举例说明过了。

  • RunLoop的实现

官方文档是这么写的:

Each time you run it, your thread’s run
loop processes pending(暂挂)events and generates notifications for any
attached observers. The order in which it does this is very specific and is as follows:

1.Notify observers that the run loop has been entered.(通知observers已经进入runloop)

2.Notify observers that any ready timers are about to fire.(通知observers要处理准好的定时器)

3.Notify observers that any input sources that are not port based are about to fire.(通知observers要处理非基于port的输入源)

4.Fire any non-port-based input sources that are ready to fire.(处理无port的输入源)

5.If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9. (如果有基于port的输入源要处理,就马上处理其中的事件,并且跳到第9步)

6.Notify observers that the thread is about to sleep.(通知observers,线程要进入休眠了)

7.Put the thread to sleep until one of the following events occurs:(让线程休眠,直到下面任何一个事件发生:)

(1)An event arrives for a port-based input source.(基于port的输入源事件抵达)

(2)A timer fires.(定时器激活)

(3)The timeout value set for the run loop expires.(为runloop设置的超时值到期了,就是超时了)

(4)The run loop is explicitly woken up. (runloop被明确唤醒)

8.Notify observers that the thread just
woke up.(通知observers线程刚刚被唤醒)

9.Process the pending event.(处理暂挂的事件)

(1)If a user-defined timer fired, process the timer event and
restart the loop. Go to step 2.(如果处理了一个用户定义的定时器,重启runloop ,回到第2步)

(2)If an input source fired, deliver the event.(如果一个输入源激活,就提交事件)

(3)If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.(如果runloop被唤醒了,并且尚未超时,就重启loop回到第2步)

10.Notify observers that the run loop
has exited.(通知observer,runloop已经被退出了)

Because observer notifications for timer and input sources are delivered before those events actually occur, there may be a gap between the time of the notifications and the time of the actual events. If the timing between these events is critical, you can use the sleep and awake-from-sleep notifications to help you correlate(关联)the timing between the actual events.

因为通知的时间早于事件真实发生(被处理)时间,所以有中间有个很小的时间差(gap)。如果对事件之间的时间要求非常严格,那么你可以用sleep和awake-from-sleep通知俩关联时间。

好吧,反正我是不会画图的。。。但是网友画了,我就不重复造轮子了。

runloop的内部逻辑图.png

需要补充的是,图中第7步左边,唤醒线程的原因还差一个就是,设置的超时限制到了。

CoreFoundation中启动run loop的源代码如下:

// 用DefaultMode启动

void CFRunLoopRun(void){

    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode,1.0e10, false);

}


// 用指定的Mode启动,允许设置RunLoop超时时间

int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle){

    returnCFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName,seconds, returnAfterSourceHandled);

}

/// RunLoop的实现

int CFRunLoopRunSpecific(runloop,modeName, seconds,stopAfterHandle) {

  //给runloop加锁(所以是线程安全的)
    __CFRunLoopLock(rl);

    // 首先根据modeName找到对应mode

    CFRunLoopModeRef
currentMode
= __CFRunLoopFindMode(runloop, modeName,false);

    // 如果mode里没有source/timer/observer,直接返回。

    if(__CFRunLoopModeIsEmpty(currentMode))return;


    // 1. 通知Observers: RunLoop 即将进入 loop。

    __CFRunLoopDoObservers(runloop, currentMode,kCFRunLoopEntry);

    /// 内部函数,进入loop

    __CFRunLoopRun(runloop, currentMode,seconds, returnAfterSourceHandled){

        Boolean sourceHandledThisLoop = NO;

        int retVal = 0;

        do {
            /// 2. 通知 Observers: RunLoop 即将触发
Timer 回调。

            __CFRunLoopDoObservers(runloop,currentMode, kCFRunLoopBeforeTimers);

            /// 3. 通知 Observers: RunLoop 即将触发
Source0 (非port) 回调。

            __CFRunLoopDoObservers(runloop,currentMode, kCFRunLoopBeforeSources);

            /// 执行被加入的block

            __CFRunLoopDoBlocks(runloop, currentMode);

            /// 4. RunLoop 触发 Source0 (非port) 回调。

            sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode,stopAfterHandle);

            /// 执行被加入的block

            __CFRunLoopDoBlocks(runloop,currentMode);

            /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个Source1 然后跳转去处理消息。

            if (__Source0DidDispatchPortLastTime) {

                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort,&msg)

                if (hasMsg) goto handle_msg;

            }

            /// 通知Observers: RunLoop 的线程即将进入休眠(sleep)。

            if (!sourceHandledThisLoop){

                __CFRunLoopDoObservers(runloop,currentMode, kCFRunLoopBeforeWaiting);

            }

            /// 7. 调用 mach_msg 等待接受mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。

            /// • 一个基于port 的Source 的事件。
            /// • 一个Timer 到时间了
            /// • RunLoop 自身的超时时间到了
            /// • 被其他什么调用者手动唤醒

            __CFRunLoopServiceMachPort(waitSet,&msg, sizeof(msg_buffer),&livePort) {

                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg

            }
            /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。

            __CFRunLoopDoObservers(runloop,currentMode, kCFRunLoopAfterWaiting);

            /// 收到消息,处理消息。

            handle_msg:
            /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。

            if (msg_is_timer) {

                __CFRunLoopDoTimers(runloop,currentMode, mach_absolute_time())

            }
            /// 9.2 如果有dispatch到main_queue的block,执行block。

            else if (msg_is_dispatch) {

                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
            /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
            else {

                CFRunLoopSourceRef source1=__CFRunLoopModeFindSourceForMachPort(runloop, currentMode,livePort);

                sourceHandledThisLoop= __CFRunLoopDoSource1(runloop, currentMode,source1, msg);

                if (sourceHandledThisLoop) {

                    mach_msg(reply,MACH_SEND_MSG, reply);

                }

            }
            /// 执行加入到Loop的block

            __CFRunLoopDoBlocks(runloop,currentMode);

            if (sourceHandledThisLoop&& stopAfterHandle) {

                /// 进入loop时参数说处理完事件就返回。

                retVal = kCFRunLoopRunHandledSource;

            } else if (timeout) {

                /// 超出传入参数标记的超时时间了

                retVal = kCFRunLoopRunTimedOut;

            } else if (__CFRunLoopIsStopped(runloop)) {

                /// 被外部调用者强制停止了

                retVal = kCFRunLoopRunStopped;

            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)){

                /// source/timer/observer一个都没有了

                retVal = kCFRunLoopRunFinished;

            }

            /// 如果没超时,mode里没空,loop也没被停止,那继续loop。

        } while (retVal== 0);

    }

    /// 10. 通知Observers: RunLoop 即将退出。

    __CFRunLoopDoObservers(rl, currentMode,kCFRunLoopExit);

    __CFRunLoopUnlock(rl);//解锁
}
  • RunLoop的底层实现

从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。

iOS:OS X系统框架图.png

苹果官方将整个系统大致划分为上述4个层次:

  • 应用层包括用户能接触到的图形应用,例如Spotlight、Aqua、SpringBoard 等。
  • 应用框架层即开发人员接触到的Cocoa 等框架。
  • 核心框架层包括各种核心框架、OpenGL等内容。
  • Darwin 即操作系统的核心,包括系统内核、驱动、Shell等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

我们在深入看一下Darwin 这个核心的架构:

Darwin简图.png

其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了 XNU 内核。
XNU 内核的内环被称作 Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
BSD 层可以看作围绕 Mach 层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
Mach 本身提供的 API 非常有限,而且苹果也不鼓励使用 Mach 的 API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

typedef struct {

  mach_msg_header_t header;

  mach_msg_body_t body;

} mach_msg_base_t;


typedef struct {

  mach_msg_bits_t msgh_bits;

  mach_msg_size_t msgh_size;

  mach_port_t msgh_remote_port;

  mach_port_t msgh_local_port;

  mach_port_name_t msgh_voucher_port;

  mach_msg_id_t msgh_id;

} mach_msg_header_t;

一条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port,发送和接受消息是通过同一个 API 进行的,其 option 标记了消息传递的方向:

mach_msg_return_t mach_msg(

mach_msg_header_t *msg,//消息头

mach_msg_option_t option,//标明方向

mach_msg_size_t send_size,//发送消息时的数据大小

mach_msg_size_t rcv_size,//接收消息时的数据大小

mach_port_name_t rcv_name,//哪个port发来的消息

mach_msg_timeout_t timeout, //超时限制

mach_port_name_t notify); //通知哪个port

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:

mach_msg_trap()触发机制图.png

RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap()这个地方。

暂定iOS app时trap被调用.png
  • RunLoop什么时候用

知道了Run Loop 是什么,内部实现之后,这个问题就变得很简单了。官方文档如是说:

Use ports or custom input sources to communicate with other threads.
1、使用ports或者自定义的输入源来与其他线程通信;

Use timers on the thread.
2、使用了定时器

Use any of the performSelector… methods in a Cocoa application.
3、在cocoaapp中使用了performSelector方法

Keep the thread around to perform periodic tasks.
4、让线程做周期性任务的时候

对于子线程,需要使用runloop的时候,记得启动,不然添加再多的selector和定时器是没有软用的。

  • RunLoop应用举例

待续。。。

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

推荐阅读更多精彩内容