NSRunLoop

作者: Dezi | 来源:发表于2020-04-09 09:11 被阅读0次

    前言

    RunLoop的初期学习总结,后续会持续研究更新。

    一、Runloop定义及作用

    1. 什么是Runloop?

    RunLoop:顾名思义,消息运行循环。首先我们看下苹果API解释:

    The RunLoop class declares the programmatic interface to objects that manage input sources. An RunLoop object processes input for sources such as mouse and keyboard events from the window system, Port objects, and NSConnection objects. An RunLoop object also processes Timer events.
    中文:RunLoop类是用来管理输入源的编程接口对象。RunLoop对象输入源来自桌面系统的鼠标和键盘事件,端口对象,NSConnection对象。一个RunLoop对象也用来处理计时器事件。

    RunLoop和线程关系如下图,Input sourcesTimer sources接受事件,然后通知线程进行处理。

    • 每个线程都有一个RunLoop,主线程的RunLoop会在App运行的时自动运行,子线程需要手动获取运行,第一次获取时,才会去创建。
    • 每个RunLoop都会以一个模式mode来运行,可以使用NSRunLoop的方法运行在某个特定的mode。

    2. Runloop的定义

    Runloop实际上就是一个do...while循环,有任务时开始,无任务时休眠:

    3. Runloop的作用

    • 保持程序的持续运行
    • 处理APP中的各种事件(触摸、定时器、performSelector)
    • 节省cpu资源、提供程序的性能:该做事就做事,该休息就休息

    二、Runloop Mode

    1. Runloop Mode是什么

    一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source、Timer、Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。
    RunLoop只会运行在一个模式下。想要切换模式,就要暂停当前模式,重新启动一个运行模式。

    2. Runloop Mode的五个Mode

    系统默认注册了5个Mode:

    • kCFRunLoopDefaultMode:App的默认 Mode,通常主线程是在这个 Mode 下运行的。
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到。
    • kCFRunLoopCommonModes:这是一个占位的 Mode,没有实际作用。

    主线程RunLoop有两个预置Model:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。

    3. CommonModes

    官方解释:

    commonModes
    Objects added to a run loop using this value as the mode are monitored by all run loop modes that have been declared as a member of the set of “common” modes with CFRunLoopAddCommonMode(_: _:)

    • kCFRunLoopCommonModes是一个特别的模式,它是一个伪模式,可以在标记为CommonModes的模式下运行。
    • 一个 Mode 可以将自己标记为 Common 属性(通过将其 ModeName 添加到 RunLoop 的 commonModes 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems里的 Source、Observer、Timer 同步到具有 Common 标记的所有 Mode 里。

    以tableViewCell中加入timer场景为例:
    当创建一个 Timer 加到 DefaultMode 时,Timer 会一直得到回调,但如果此时滑动TableView,RunLoop 会将 mode 切换为 TrackingRunLoopMode,此时timer停止回调,因为同时只有一种Mode存在,当停止滑动,mode切换回kCFRunLoopDefaultMode,timer又重新开始回调,但此时的timer回调已经是被延迟的错误时间的回调。

    如果想让tableView滑动时timer可以正常调用,一是手动将这个 timer 分别加入这两个 Mode,二是将 Timer 加入到CommonModes 里。

    NSLog(@"Current AllModes == %@", CFRunLoopCopyAllModes(CFRunLoopGetCurrent()));
        NSLog(@"Current Mode == %@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"Current AllModes -- %@", CFRunLoopCopyAllModes(CFRunLoopGetCurrent()));
        NSLog(@"Current Mode -- %@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    /** 打印
    Current AllModes == (
        UITrackingRunLoopMode,
        GSEventReceiveRunLoopMode,
        kCFRunLoopDefaultMode
    )
    Current Mode == kCFRunLoopDefaultMode
    
    Current AllModes -- (
        UITrackingRunLoopMode,
        GSEventReceiveRunLoopMode,
        kCFRunLoopDefaultMode,
        kCFRunLoopCommonModes
    )
    Current Mode -- kCFRunLoopDefaultMode
    */
    
    • current mode:runloop当前运行模式。
    • common modes:存储的被标记为common modes的模式。

    打印CFRunLoopGetCurrent()会获取更多信息,后续进行深入研究。

    CFRunLoopGetCurrent()

    三、Runloop底层原理

    1. Runloop的item — 六大事件

    • block应用__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
    • 调用timer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    • 响应source0__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    • 响应source1(端口信息)__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
    • GCD主队列__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    • observer源__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

    source0:执行performSelectors方法,假如你在主线程performSelectors一个任务到子线程,这时候就是在代码中发送事件到子线程的runloop,这时候如果子线程开启了runloop就会执行该任务,注意该performSelector方法只有在你子线程开启runloop才能执行,如果你没有在子线程中开启runloop,那么该操作会无法执行并崩溃。一个selector执行完后会自动从run loop里面移除,如UIEvent(Touch事件等),在一次触发之后就会被runloop移除。
    source1: 苹果创建用来接受系统发出事件,当手机发生一个触摸,摇晃或锁屏等系统,这时候系统会发送一个事件到app进程(进程通信),这也就是为什么叫基于port传递source1的原因,port就是进程端口嘛,该事件可以激活进程里线程的runloop,比如你点击一下app的按钮或屏幕,runloop就醒过来处理触摸事件。

    2. 从堆栈信息探索源码调用

    通过 bt 命令打印出堆栈信息,我们可以看到来自于CoreFoundation中的 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ 其它item同理可自行验证:

    查看CFRunloop.c中的源码

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline));
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) {
        if (func) {
            func(timer, info);
        }
        asm __volatile__(""); // thwart tail-call optimization
    }
    

    我们可以看到,源码中先调起__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__之后才会调用 func 调用 timer ,所以又一次可以验证,为什么 timer 需要加到 Runloop 中去了。

    3. 通过主线程探究Runloop内部机制

    // 主线程循环

    CFRunLoopRef mainRunloop = CFRunLoopGetMain();
    
    // CFRunLoop.c
    CFRunLoopRef CFRunLoopGetMain(void) {
        CHECK_FOR_FORK();
        static CFRunLoopRef __main = NULL; // no retain needed
        if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
        return __main;
    }
    
    // should only be called by Foundation
    // t==0 is a synonym for "main thread" that always works
    CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
        if (pthread_equal(t, kNilPthreadT)) { 
            t = pthread_main_thread_np(); // 默认给主线程
        }
        __CFSpinLock(&loopsLock);
        if (!__CFRunLoops) {
            __CFSpinUnlock(&loopsLock);
            CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
            CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    
            // 主线程、mainLoop、通过dict进行绑定:dict[@"pthread_main_thread_np"] = mainLoop
            CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
            if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
                CFRelease(dict);
            }
            CFRelease(mainLoop);
            __CFSpinLock(&loopsLock);
        }
        
        // 其它线程与runloop绑定
        CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        __CFSpinUnlock(&loopsLock);
        if (!loop) {
            CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            __CFSpinLock(&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
            __CFSpinUnlock(&loopsLock);
            CFRelease(newLoop);
        }
        if (pthread_equal(t, pthread_self())) {
            _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
            if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
            }
        }
        return loop;
    }
    

    创建RunLoop

    /*
    _CFRuntimeCreateInstance 这个创建runloop实例方法是关键,
    但是看不到内部方法,所以我们主要探究RunLoop对象结构关系。
    */
    static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
        CFRunLoopRef loop = NULL;
        CFRunLoopModeRef rlm;
        uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
        loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopTypeID, size, NULL);
        if (NULL == loop) {
            return NULL;
        }
        (void)__CFRunLoopPushPerRunData(loop);
        __CFRunLoopLockInit(&loop->_lock);
        loop->_wakeUpPort = __CFPortAllocate();
        if (CFPORT_NULL == loop->_wakeUpPort) HALT;
        __CFRunLoopSetIgnoreWakeUps(loop);
        loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
        loop->_commonModeItems = NULL;
        loop->_currentMode = NULL;
        loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        loop->_blocks_head = NULL;
        loop->_blocks_tail = NULL;
        loop->_counterpart = NULL;
        loop->_pthread = t;
    #if DEPLOYMENT_TARGET_WINDOWS
        loop->_winthread = GetCurrentThreadId();
    #else
        loop->_winthread = 0;
    #endif
        rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
        if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
        return loop;
    }
    

    继续找CFRunLoopRef也就是RunLoop结构体组成:

    typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
    
    struct __CFRunLoop {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;            /* locked for accessing mode list */
        __CFPort _wakeUpPort;            // used for CFRunLoopWakeUp
        Boolean _unused;
        volatile _per_run_data *_perRunData;              // reset for runs of the run loop
        pthread_t _pthread; // 线程
        uint32_t _winthread;
        CFMutableSetRef _commonModes; // Mode集合
        CFMutableSetRef _commonModeItems; // ModeItem集合
        CFRunLoopModeRef _currentMode; 
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFAbsoluteTime _runTime;
        CFAbsoluteTime _sleepTime;
        CFTypeRef _counterpart;
    };
    

    找到CFRunLoopModeRef也就是RunLoopMode结构体组成:

    typedef struct __CFRunLoopMode *CFRunLoopModeRef;
    
    struct __CFRunLoopMode {
        CFRuntimeBase _base;
        pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
        CFStringRef _name;
        Boolean _stopped;
        char _padding[3];
        CFMutableSetRef _sources0;
        CFMutableSetRef _sources1;
        CFMutableArrayRef _observers;
        CFMutableArrayRef _timers;
        CFMutableDictionaryRef _portToV1SourceMap;
        __CFPortSet _portSet;
        CFIndex _observerMask;
    #if USE_DISPATCH_SOURCE_FOR_TIMERS
        dispatch_source_t _timerSource;
        dispatch_queue_t _queue;
        Boolean _timerFired; // set to true by the source when a timer has fired
        Boolean _dispatchTimerArmed;
    #endif
    #if USE_MK_TIMER_TOO
        mach_port_t _timerPort;
        Boolean _mkTimerArmed;
    #endif
    #if DEPLOYMENT_TARGET_WINDOWS
        DWORD _msgQMask;
        void (*_msgPump)(void);
    #endif
        uint64_t _timerSoftDeadline; /* TSR */
        uint64_t _timerHardDeadline; /* TSR */
    };
    

    Runloop机制关系图

    通过上面源码追踪,我们可以总结如下关系:
    1. Runloop线程是一对一的关系
    2. RunloopRunloopMode是一对多的关系
    3. RunloopModeRunloopSource是一对多的关系
    4. RunloopModeRunloopTimer是一对多的关系
    5. RunloopModeRunloopObserver是一对多的关系

    四. 分析runloop addTimer

    通过堆栈找到关键函数__CFRunLoopDoTimers、__CFRunLoopDoTimer,然后进行源码分析。

    源码分析:

    static Boolean __CFRunLoopDoTimers(CFRunLoopRef rl, CFRunLoopModeRef rlm, uint64_t limitTSR) {    /* DOES CALLOUT */
        Boolean timerHandled = false;
        CFMutableArrayRef timers = NULL;
        for (CFIndex idx = 0, cnt = rlm->_timers ? CFArrayGetCount(rlm->_timers) : 0; idx < cnt; idx++) {
            CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(rlm->_timers, idx);
            
            if (__CFIsValid(rlt) && !__CFRunLoopTimerIsFiring(rlt)) {
                if (rlt->_fireTSR <= limitTSR) {
                    // 收集timers
                    if (!timers) timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeArrayCallBacks);
                    CFArrayAppendValue(timers, rlt);
                }
            }
        }
    
        // 遍历所有timers,然后__CFRunLoopDoTimer,DoTimer结束后给标记did,然后release释放。
        for (CFIndex idx = 0, cnt = timers ? CFArrayGetCount(timers) : 0; idx < cnt; idx++) {
            CFRunLoopTimerRef rlt = (CFRunLoopTimerRef)CFArrayGetValueAtIndex(timers, idx);
            Boolean did = __CFRunLoopDoTimer(rl, rlm, rlt);
            timerHandled = timerHandled || did;
        }
        if (timers) CFRelease(timers);
        return timerHandled;
    }
    
    // __CFRunLoopDoTimer中会走__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__回调,timer block回调开始执行。
    static Boolean __CFRunLoopDoTimer(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopTimerRef rlt) {    /* DOES CALLOUT */
        ...省略...    
    
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info);
    
        ...省略...    
    }
    

    根据堆栈信息追踪源码(省略部分源码),主要对关键部分进行分析形成闭环探索runloop和timer的关系:[runloop addTimer] -> ...... -> __CFRunLoopDoTimers -> __CFRunLoopDoTimer -> __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ -> block回调。当然Observer、source也是同理。

    RunLoop Mode:
    OS下Run Loop的主要运行模式mode有:

    • NSDefaultRunLoopMode:默认的运行模式,除了NSConnection对象的事件。
    • NSRunLoopCommonModes:是一组常用的模式集合,将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonModes包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode。
    • UITrackingRunLoopMode:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动), 主线程当触摸事件触发会设置为这个模式,可以用来在控件事件触发过程中设置Timer。
    • GSEventReceiveRunLoopMode:用于接受系统事件,属于内部的RunLoop模式。
    • 自定义Mode:可以设置自定义的运行模式Mode,你也可以用CFRunLoopAddCommonMode添加到NSRUnLoopCommonModes中。

    总结一下:
    Run Loop 运行时只能以一种固定的模式运行,如果我们需要它切换模式,只有停掉它,再重新开其它,运行时它只会监控这个模式下添加的Timer Source和Input Source,如果这个模式下没有相应的事件源,RunLoop的运行也会立刻返回的。注意RunLoop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。

    相关文章

      网友评论

          本文标题:NSRunLoop

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