iOS 浅谈 Runloop

RunLoop 是什么

强烈推荐 ibireme 大神的文章深入理解RunLoop

Runloop源码地址

关于 Runloop ,尽管早就知道它的本质实现是一个循环,但笔者还是一直很困惑它的作用是什么 ,不过最近整理相关知识总算是理解了。

代码的执行逻辑是自上而下的,如果没有 Runloop ,代码执行完毕后,程序就退出了,对应到实际场景就是 APP 一打开立马就退出了。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"程序执行中...");
    }
    return 0;
}
// log
程序执行中...
Program ended with exit code: 0

例如上面的代码,代码执行完毕后,main 函数返回,然后程序退出。

为什么工作中,好像没有编写 Runloop 相关的代码,程序还是能够稳定持续运行呢?

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这是因为程序自动帮我们在 UIApplicationMain… 中做了这个事情。

下面来看看 Runloop 的简化的伪代码,主要来自 sunnyxx 大神的一次视频分享:

function loop() {
    do {
        有事干了 = 我睡觉了没事别找我();
        if (搬砖) {
            搬砖();
        } else if (吃饭) {
            吃饭();
        }
    } while (活着)
}

这个伪代码看着还是有一点抽象,需要了解的一个知识点是线程和 RunLoop 之间是一一对应的,这里的睡觉了可以理解为线程休眠 [NSThread sleepUntilDate:...]],也就是说当应用没有任何事件触发时,就会停在睡觉那行代码不执行,这样就节约了 CPU 的运算资源,提高程序性能,直到有事件唤醒应用为止。例如上面的搬砖事件,吃饭事件。处理完后,又会进入睡觉状态直到下次唤醒,反复循环,这样就保证了程序能随时处理各种事件并能够稳定运行。

实际上触摸事件、屏幕 UI 刷新、延迟回调等等都是 Runloop 实现的。

Runloop 的结构

先来看看 Runloop 的结构源码:

struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;     
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    // ...
};

这里包含一个线程的成员变量 _pthread,可以看出 Runloop 确实和线程是息息相关的。还能看到 Runloop 拥有很多关于 Model 的成员变量,再来看看 Model 的结构:

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    // ...
};

先不管这些东西是干什么的,至少我们现在能够得出如下图所示的理解:

image

一个 Runloop 中包含若干个 Model ,每个 Mode 又包含若干个 Source/Timer/Observer

Runloop 的 Model

Model 代表 Runloop 的运行模式,Runloop 每次只能指定一个 Model 作为 _currentMode ,如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入。主线程的 Runloop 这里有两个预置的模式 ,并且这也是系统公开的两个 Model

  • kCFRunLoopDefaultModeAPP 的普通状态,通常主线程是在这个Mode下运行,已被标记为 Common

  • UITrackingRunLoopModeApp 追踪触摸 ScrollView 滑动时的状态,保证界面滑动时不受其他 Mode影响,已被标记为 Common

注意 Runloop 的结构中有一个 _commonModes 。这里是因为一个 Mode 可以将自己标记为 Common (通过将其 ModeName 添加到 RunLoopcommonModes 中 ),标记为 CommonModel 都可以处理事件,可以理解为变相的实现了多个 Model 同时运行。同时系统也提供了一个操作 Common 标记的字符串->kCFRunLoopCommonModes。如果我们想要上面两种模式下都能处理事件,就可以使用这个字符串。

Model 中的 Item

Source/Timer/Observer 被统称为 mode item,不同 ModelSource0/Source1/Timer/Observer 被分隔开来,互不影响,如果 Mode 里没有任何Source0/Source1/Timer/ObserverRunLoop 会立马退出。

Source

Source 是事件产生的的地方,它对应的类为 CFRunLoopSourceRefSource 有两个版本:Source0Source1

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。例如屏幕触摸、锁屏和摇晃等。

Timer

Timer 对应的类是 CFRunLoopTimerRef,它其实就是 NSTimer,当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

Observer

Observer 对应的类是 CFRunLoopObserverRef,当 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
};

Runloop 的内部逻辑

打开开头的 Runloop 的源码,面对众多代码,让人毫无头绪,但是前文中已经讲到,屏幕的触摸事件是 Runloop 来处理的。于是打个断点,来查看程序的函数调用栈:

image

从图中能看到,Runloop 是从 11 开始的,于是从源码中搜索 CFRunLoopRunSpecific 函数,这里只探究内部主要逻辑,其他细节不看,下面是精简后的函数:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    // 根据 modeName 获取currentMode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    // 设置 Runloop 的 Model
    CFRunLoopModeRef previousMode = rl->_currentMode;
    rl->_currentMode = currentMode;
    // 通知 Observers: 即将进入 RunLoop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 进入 runloop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    // 通知 Observers: RunLoop 即将退出
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    return result;
}

然后再进入 __CFRunLoopRun(...) 函数查看内部精简后的主要逻辑源码:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        // 通知 Observers: 即将处理 Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知 Observers: 即将处理 Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 处理 Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        // 处理 Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理 Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }

        // 判断有无 Sources1
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            // 跳转到 handle_msg 处理 Sources1soso
            goto handle_msg;
        }
        // 通知 Observers: 即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 开始休眠
        __CFRunLoopSetSleeping(rl);

        // 等待消息唤醒当前线程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
        // 结束休眠
        __CFRunLoopUnsetSleeping(rl);
        // 通知 Observers: 结束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

    // 处理
    handle_msg:;
        // 被 timer 唤醒
        if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
            // 处理 timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        }
        // 被 gcd 唤醒
        else if (livePort == dispatchPort) {
            // 处理 gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        // 被source1唤醒
        } else {
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
        }

        // 处理 Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 设置返回值
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }
    } while (0 == retVal);
    return retVal;
}

可以看到 Runloop 内部确实是一个循环,并且,唤醒 RunLoop 的方式有 mach portTimerdispatch

。笔者最初在疑惑一个问题,上面的函数调用栈是一个点击屏幕后的响应事件,可以看出这里是 sources0 ,明明是一个触摸事件为什么不是 sources1 呢,笔者猜测 sources1 这里唤醒了 Runloop ,因为 sources0 是无法唤醒 runloop 的,然后再在 sources0 的回调中处理的点击事件。

RunLoop 中的 mach port

这里由于目前笔者水平有限,只能够理解到 mach port 是一个可以控制硬件和接受硬件反馈的一个系统,然后可以通过它将来自硬件的操作转化成熟知的 UIEvent 事件等等。

总结

这篇文章主要讲解了 Runloop 到底是一个什么东西,当然 Runloop 的知识不仅仅只有这篇文章这点。例如实际用处中的线程保活(AFNetworking 2.x 版本中),滑动时 Timer 怎么不被停止,自动释放池的实现等等都用到了 Runloop

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

推荐阅读更多精彩内容