美文网首页
RuntLoop 一: runloop 底层结构及源码分析

RuntLoop 一: runloop 底层结构及源码分析

作者: 小心韩国人 | 来源:发表于2019-12-07 16:11 被阅读0次

RunLoop就像尼斯湖水怪,只是听说过从来没见过.今天我们就来解开它神秘的面纱.
RunLoop顾名思义:运行循环.他是维持我们一个App能够运行的关键.比如我们创建的iOS项目,在main函数中系统就会自动帮我们创建runloop对象:return UIApplicationMain(argc, argv, nil, appDelegateClassName);.
RunLoop的基本作用就是:

  • 保证程序的基本运行.
  • 处理App中的各种事件 (比如:触摸事件,定时器事件 等等).
  • 节省 CPU 资源,提高程序性能: 该做事时做事,没有事的时候就休息.
    RunLoop工作原理的伪代码大概如下:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        int retVal = 0;
        do {
            //睡眠中等待消息
            int message = sleep_and_wait();
            //处理消息
            retVal = process_message(message);
        } while (retVal = 0);
        return 0;
    }
}

流程:条件成立的时候一直循环:有事情就处理事情,没有事情就休眠睡觉.

iOS中提供了两套API来访问RunLoop:

  • Foundation : NSRunLoop : OC 框架
  • Core Foundation : CFRunLoopRef : C 语言框架
    NSRunLoop是对CFRunLoopRef的一层封装.CFRunLoopRef源码的下载地址.我们下载好源代码后新建一个项目,把源代码拖到项目中,找到CFRunLoop.c文件 -> 然后找到CFRunLoopGetCurrent函数 -> 进入_CFRunLoopGet0函数,会发现下面几句代码:
    static CFMutableDictionaryRef __CFRunLoops = NULL; //字典
    // 获取 runloop 对象 参数:传入一个 字典 和 key (线程)
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //如果 runloop 不存在 , 就创建,并放到字典中
    if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }

从上面可以看出:RunLoop是在第一次获取的时候创建的,并且RunLoop和 线程 是 一一对应的关系,RunLoop是存放在一个全局字典中:以线程作为key,RunLoop作为value.

RunLoop 底层结构:

RunLoop 结构层级关系

RunLoop 的状态:

RunLoop有以下几种状态:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), //即将进入 RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),  //即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),  //即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  //即将进入 休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  //刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),  //即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  // 以上所有状态
};

下面我们写代码来验证一下这些状态的切换.
首先写代码测试一下,NStimer唤醒RunLoop:

//监听方法
void observerRunLoop(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    
    switch (activity) {
        case kCFRunLoopEntry:
        {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopEntry - %@",mode);
            break;
        }
        case kCFRunLoopBeforeTimers:
        {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopBeforeTimers - %@",mode);
            break;
        }
        case kCFRunLoopBeforeSources:
            {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopBeforeSources - %@",mode);
            break;
            }
        case kCFRunLoopBeforeWaiting:
                {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopBeforeWaiting - %@",mode);
            break;
                }
        case kCFRunLoopAfterWaiting:
                    {
                    CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopAfterWaiting - %@",mode);
            break;
                    }
        case kCFRunLoopExit:
        {
            CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
            NSLog(@"kCFRunLoopExit - %@",mode);
            break;
        }
        default:
            break;
    }
    
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    //创建 observer 方法二:
//    CFRunLoopObserverRef observer2 = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//        // 监听触发的操作
//    });
    
    // 创建 observer 方法一:
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0,  observerRunLoop, NULL);
    // 添加 observer
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放 observer
    CFRelease(observer);
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [NSTimer scheduledTimerWithTimeInterval:3 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"------定时器触发");
    }];
}

// 控制台打印:
2019-12-07 14:59:52.485737+0800 RunLoop状态监听[9050:2703905] ------定时器触发
2019-12-07 14:59:52.485925+0800 RunLoop状态监听[9050:2703905] kCFRunLoopBeforeTimers - kCFRunLoopDefaultMode
2019-12-07 14:59:52.486105+0800 RunLoop状态监听[9050:2703905] kCFRunLoopBeforeSources - kCFRunLoopDefaultMode
2019-12-07 14:59:52.486327+0800 RunLoop状态监听[9050:2703905] kCFRunLoopBeforeWaiting - kCFRunLoopDefaultMode

然后我们再创建一个UITextView,拖动UITextView看看RunLoop状态的切换情况,为了更清晰的看到RunLoop切换状态,我们把Switch语句中其他的状态都注释掉,只保留kCFRunLoopEntrykCFRunLoopExit两种状态,然后拖动TextView:

2019-12-07 15:03:34.797449+0800 RunLoop状态监听[9079:2707284] kCFRunLoopExit - kCFRunLoopDefaultMode
2019-12-07 15:03:34.797675+0800 RunLoop状态监听[9079:2707284] kCFRunLoopEntry - UITrackingRunLoopMode
2019-12-07 15:03:35.520897+0800 RunLoop状态监听[9079:2707284] kCFRunLoopExit - UITrackingRunLoopMode
2019-12-07 15:03:35.521162+0800 RunLoop状态监听[9079:2707284] kCFRunLoopEntry - kCFRunLoopDefaultMode
2019-12-07 15:03:35.832982+0800 RunLoop状态监听[9079:2707284] kCFRunLoopExit - kCFRunLoopDefaultMode

可以看到RunLoop频繁的在kCFRunLoopDefaultModeUITrackingRunLoopMode之间切换.

刚才我们用伪代码概括了RunLoop内部的工作流程,那RunLoop内部到底是如何工作的呢?我们通过源码看一下.

RunLoop 源码分析:

我们找到CFRunLoop.c源码,发现里面有很多函数,哪一个才是我们想要的RunLoop入口函数呢?很简单,我们可以写一个- (void)touchesBegan:withEvent:方法,然后再这个方法内部打一个断点,因为我们知道系统事件都是由RunLoop来捕捉管理的.走到断点后,我们使用LLDB指令bt打印所有函数调用栈:

runloop 入口函数
CFRunLoop中搜索CFRunLoopRunSpecific函数,简化后如下:
//  RunLoop 入口函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    
    //通知 observer: 进入 loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    //具体要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    //通知observer`: 退出 loop
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    return result;
}

所以我们重点研究__CFRunLoopRun函数内部做了什么事情.
我把__CFRunLoopRun函数精简了一部分,并且添加了注释:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    
    int32_t retVal = 0;
    do {
        //通知observer: 即将处理 Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        //通知observer: 即将处理 Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        //处理 blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        //处理Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            //处理 blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        //判断有无 Source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            //如果有 Source1 , 就直接跳转到 handle_msg
            goto handle_msg;
        }
        
        didDispatchPortLastTime = false;
        //通知observer: 即将休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        //进入休眠 (睡觉)
        __CFRunLoopSetSleeping(rl);
    
        //等待别的消息来唤醒当前线程,如果唤醒就继续往下走;如果没有被唤醒,就会阻塞在这个位置等待别人唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        //唤醒 (不睡觉)
        __CFRunLoopUnsetSleeping(rl);
        //通知observer: 结束休眠
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
    handle_msg:
        if (被 timer 唤醒) {
            //处理 Timers
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
        }else if (被 gcd 唤醒) {
            //处理 gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {
            //剩下的就是被 source1 唤醒
            //处理 source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
        }
        
        //处理 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;
        }
        voucher_mach_msg_revert(voucherState);
        os_release(voucherCopy);
        
    } while (0 == retVal);
    
    return retVal;
}

用下图总结一下RunLoop内部执行的流程:

RunLoop 内部执行的流程

线程阻塞的细节:

我们上面讲的如果没有事情做的时候RunLoop会进入休眠状态,会阻塞当前线程,不会继续往下执行.但是这里要搞清楚,RunLoop的阻塞线程和我们平常代码写的阻塞线程可不一样,比如说下面这种阻塞线程:

    while(1){
    NSlog(@"阻塞线程");
}

像这种就是个死循环,阻塞线程的时候线程并没有休息,而是一直在判断条件并执行代码.而RunLoop的阻塞是真正意义上的休息,什么事情也不管,一句代码都不执行,CPU不会分配任何资源.
那么RunLoop是如何做到这样的呢?
因为API的调用分为用户态和内核态.内核态的代码是非常底层的,不对外开放.如果RunLoop要进入休眠状态的时候,用户态的应用代码会调用mach_msg()函数,就会转到内核态,在内核态内部调用内核态的mach_msg ()函数进入真正的休眠状态.

相关文章

网友评论

      本文标题:RuntLoop 一: runloop 底层结构及源码分析

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