iOS-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:

kCFRunLoopDefaultMode:APP 的普通状态,通常主线程是在这个Mode下运行,已被标记为 Common。
UITrackingRunLoopMode:App 追踪触摸 ScrollView 滑动时的状态,保证界面滑动时不受其他 Mode影响,已被标记为 Common。
注意 Runloop 的结构中有一个 _commonModes 。这里是因为一个 Mode 可以将自己标记为 Common (通过将其 ModeName 添加到 RunLoop 的 commonModes 中 ),标记为 Common 的 Model 都可以处理事件,可以理解为变相的实现了多个 Model 同时运行。同时系统也提供了一个操作 Common 标记的字符串->kCFRunLoopCommonModes。如果我们想要上面两种模式下都能处理事件,就可以使用这个字符串。

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

Source
Source 是事件产生的的地方,它对应的类为 CFRunLoopSourceRef。Source 有两个版本:Source0 和 Source1。

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

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 port 、Timer 和 dispatch

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

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

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

你可能感兴趣的:(iOS-RunLoop)