iOS-RunLoop

作者: xh_0129 | 来源:发表于2022-06-20 11:15 被阅读0次

    强烈推荐 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

        本文链接:https://www.haomeiwen.com/subject/drqvrqtx.html