RunLoop解析

作者: Jimmy_L_Wang | 来源:发表于2019-06-26 22:20 被阅读0次

    什么是RunLoop?

    RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象

    事件循环 Event Loop

    • 没有消息需要处理时,休眠以避免资源占用。
    event_loop01.png
    • 有消息需要处理时,立刻被唤醒。
    event_loop02.png

    应用程序一般运行在用户态上面,当发生系统调用,需要一些底层API的话,是运行在内核态的。同时用户态的有些内容可以对内核态进行一些线程的调度和管理,包括进程间的通信。

    main()函数为何会保持一致运行的状态而不退出?

    因为main()函数在内部会调用UIApplicationMain()函数,UIApplicationMain()内部会启动一个主线程的runloop,进行消息的接收,消息的处理,已经处理后的等待这么一个循环,而这个循环不是一个简单的for循环或while循环,他是发生了一个用户态到内核态的切换,以及内核态到用户态的切换。

    RunLoop的数据结构

    NSRunLoopCFRunLoop的封装,提供了面向对象的API。

    • CFRunLoop
    • CFRunLoopMode
    • Source/Timer/Oberver

    CFRunLoop源码

    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;//--对应(runloop和线程关系)
        uint32_t _winthread;
        CFMutableSetRef _commonModes;
        CFMutableSetRef _commonModeItems;
        CFRunLoopModeRef _currentMode;
        CFMutableSetRef _modes;
        struct _block_item *_blocks_head;
        struct _block_item *_blocks_tail;
        CFTypeRef _counterpart;
    };
    
    cfrunloop结构.png

    CFRunLoopMode

    runloopmode.png

    CFRunLoopSource

    • source0

      需要手动唤醒线程。

    • source1

      具备唤醒线程的能力。

    CFRunLoopTimer

    基于事件的定时器

    ​ 和NSTimertoll-free bridged的。

    CFRunLoopOberver

    观测时间点:

    • kCFRunLoopEntry

      RunLoop入口时机,当runloop准备启动的时候,系统会给一个回调通知。

    • kCFRunLoopBeforeTimers

      另一个通知:通知观察者runloop将要对timer的一些相关事件进行处理

    • kCFRunLoopBeforeSources

      另一个通知:通知观察者runloop将要对source的一些相关事件进行处理。

    • kCFRunLoopBeforeWaiting

      通知对应观察者,当前runloop将要进入休眠状态,这个通知标志着用户即将开始用户态到内核态的切换。

    • kCFRunLoopAfterWaiting

      这个通知标志着内核态切换到用户态后的不久时间。

    • kCFRunLoopExit

      代表runloop退出的通知

    各个数据结构之间的关系

    关系.png

    线程与runloop是一对一的关系,runloop与Mode之间是1对多的关系,Mode与source,timer,observer是一对多的关系

    Runloop的Mode

    runloop_mode.png

    当runloop运行在mode1当中时,我们只能处理mode1当中的事件,此时如果mode2,mode3对应的一些回调事件需要处理,如果不是运行在对应mode下面是没办法处理的,这就是mode有多个的原因。

    CommonMode的特殊性

    NSRunLoopCommonModes

    • CommonMode不是实际存在的一种Mode。
    • 是同步Source/Timer/Observer到多个mode中的一种技术方案

    如何将timer添加到两个mode当中?

    RunLoop事件循环机制

    void CFRunLoopRun()

    事件循环机制.png

    RunLoop的核心

    runloop核心.png

    调用mach_msg()函数系统会从核心态到用户态的切换,需要出现source1或Timer事件,或手动唤醒。

    RunLoop与NSTimer

    为什么在滑动tableView时,banner不会再发生滚动?

    当我们滑动tableview的时候mode会由:kCFRunLoopDefaultMode切换为UITrackingRunLoopMode。所以出于之前mode上的timer事件就不会生效。如何解决这个问题,让其生效?可以通过void CFRunLoopAddTimer(runloop,timer, commenMode)函数,把NSTimert添加到commenMode中。

    void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {    
        CHECK_FOR_FORK();
        if (__CFRunLoopIsDeallocating(rl)) return;
        if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
        __CFRunLoopLock(rl);
        if (modeName == kCFRunLoopCommonModes) {
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
        if (NULL == rl->_commonModeItems) {
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
        CFSetAddValue(rl->_commonModeItems, rlt);
        if (NULL != set) {
            CFTypeRef context[2] = {rl, rlt};
            /* add new item to all common-modes */
            CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
            CFRelease(set);
        }
        } else {
        CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
        if (NULL != rlm) {
                if (NULL == rlm->_timers) {
                    CFArrayCallBacks cb = kCFTypeArrayCallBacks;
                    cb.equal = NULL;
                    rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
                }
        }
        if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
                __CFRunLoopTimerLock(rlt);
                if (NULL == rlt->_runLoop) {
            rlt->_runLoop = rl;
            } else if (rl != rlt->_runLoop) {
                    __CFRunLoopTimerUnlock(rlt);
                __CFRunLoopModeUnlock(rlm);
                    __CFRunLoopUnlock(rl);
            return;
            }
            CFSetAddValue(rlt->_rlModes, rlm->_name);
                __CFRunLoopTimerUnlock(rlt);
                __CFRunLoopTimerFireTSRLock();
                __CFRepositionTimerInMode(rlm, rlt, false);
                __CFRunLoopTimerFireTSRUnlock();
                if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) {
                    // Normally we don't do this on behalf of clients, but for
                    // backwards compatibility due to the change in timer handling...
                    if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl);
                }
        }
            if (NULL != rlm) {
            __CFRunLoopModeUnlock(rlm);
        }
        }
        __CFRunLoopUnlock(rl);
    }
    
    static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
        CFStringRef modeName = (CFStringRef)value;
        CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
        CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
        if (CFGetTypeID(item) == __kCFRunLoopSourceTypeID) {
        CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
        } else if (CFGetTypeID(item) == __kCFRunLoopObserverTypeID) {
        CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
        } else if (CFGetTypeID(item) == __kCFRunLoopTimerTypeID) {
        CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
        }
    }
    

    RunLoop与多线程

    关系:

    • 线程与RunLoop是一一对应的。
    • 自己创建的线程默认是没有RunLoop的。

    怎样实现一个常驻线程

    1. 为当前线程开启/创建一个RunLoop。

      CFRunloopGetCurrent[NSRunLoop currentRunLoop]都可以为当前线程创建一个RunLoop,因为获取方法本身的过程中,没有它会自己创建。

    2. 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。

    3. 启动该RunLoop。

    #import "MCObject.h"
    
    @implementation MCObject
    
    static NSThread *thread = nil;
    // 标记是否要继续事件循环
    static BOOL runAlways = YES;
    
    + (NSThread *)threadForDispatch
    {
        if (thread == nil) {
            @synchronized(self) {
                if (thread == nil) {
                    // 线程的创建
                    thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                    [thread setName:@"com.imooc.thread"];
                    //启动
                    [thread start];
                }
            }
        }
        return thread;
    }
    
    + (void)runRequest
    {
        // 创建一个Source
        CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
        CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
        
        // 创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
        
        // 如果可以运行
        while (runAlways) {
            @autoreleasepool {//每次运行循环可以对资源进行一个释放
                // 令当前RunLoop运行在DefaultMode下面
                //运行的mode和上面添加source的mode要一致,不一致就会死循环
                //这个循环在RunLoop运行过程中会由用户态向核心态转换,当前线程会休眠,while就会暂停,所以不会死循环
                //第二个参数是运行时间,给个很大的数,代表遥远的未来
                //资源被处理是否立即返回,这里是立即返回true
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
            }
        }
        
        // 某一时机 静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出
        CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
        CFRelease(source);
    }
    

    总结

    • 什么是RunLoop,它是怎样做到有事做事,没事休息的?

    • RunLoop与线程是怎样的关系?

    • 如何实现一个常驻线程

    • 怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

    相关文章

      网友评论

        本文标题:RunLoop解析

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