美文网首页
iOS---RunLoop面试问题

iOS---RunLoop面试问题

作者: BabyNeedCare | 来源:发表于2022-03-23 22:23 被阅读0次

    Q: 如何实现常驻线程?


    image.png

    Q:什么是 RunLoop?
    A:RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象
    1). 没有消息需要处理时,休眠以避免资源占用


    image.png

    2). 有消息需要处理时,立即被唤醒


    image.png
    应用程序一般都是运行在用户态上的,用户进程包括平时开发所使用的绝大多数的API都是针对于用户层面的。而当我们发生系统调用,需要用到一些关于操作系统,底层相关的指令或者说API,就会触发系统调用。而有些系统调用就会发生状态空间的切换,这种切换空间,或者说区分用户态和内核态,实际上是对计算机的一些资源调度,包括资源管理的统一或者说一致性的操作,这样就可以合理的安排资源调度,也可以避免特殊异常。
    内核态里的一些内容可以对用户态当中的一些线程进行调度。

    Q:main函数为什么能保持不退出?


    image.png

    A:在main函数中调用UIApplicationMain, 函数内部会启用运行循环RunLoop,RunLoop是对事件循环的维护机制,做到有事做的时候做事,没事做的时候会通过用户态到内核态的切换,从而避免资源浪费,例如滑动列表,处理网络请求返回,不断接收消息,接收消息后,就会对消息进行处理,处理完后就会继续等待。这里的循环,是发生了用户态到内核态的切换,以及内核态到用户态的切换。

    RunLoop相关问题的关键,在于状态的切换。

    等待不等于死循环!!!

    数据结构

    NSRunLoop是CFRunLoop的封装,提供了面向对象的API。
    NSRunLoop位于Foundation框架中,CFRunLoop位于Core Foundation当中,苹果对于CF开头的是开源的。

    • NSRunLoop
    • NSRunLoopMode
    • Source/Timer/Observer


      image.png

    pthread:对应RunLoop与线程之间的关系
    currentMode: CFRunLoopMode的数据结构
    modes: NSMutableSet<CFRunLoopMode *>
    commonModes: NSMutableSet<NSString *> //特殊,多注意
    commonModeItems: 集合,里面包含多个Observer,Timer, Source。

    image.png

    name: NSDefaultRunLoopMode
    Sources0-Sources1: NSMutableSet
    Observers: NSMutableArray
    Timers: NSMutableArray

    CFRunLoopSource

    *source0: 需要手动唤醒线程.
    添加source0到对应RunLoop中,不会主动唤醒当前线程。
    *source1: 具备唤醒线程的能力。

    CFRunLoopTimer

    基于事件的定时器,和NSTimer是toll-free bridged的。

    CFRunLoopObserver

    观测时间点
    Q:可以监听RunLoop哪些时间点
    A:主要有6个。
    1). KCFRunLoopEntry,入口时机,当RunLoop准备启动的时候,系统会给出回调通知
    2). KCFRunLoopBeforeTimes:通知观察者,Timer将要对一些相关事件进行处理了
    3). KCFRunLoopBeforeSources:将要处理一些source事件
    4). KCFRunLoopBeforeWaiting:通知当前RunLoop即将进入休眠状态, 非常重要的观测点,即将发生用户态到内核态的切换
    5). KCFRunLoopAfterWaiting:内核态切到用户态不久的时间
    6). KCFRunLoopExit:RunLoop退出的通知

    线程和RunLoop之间是一一对应的

    Q:RunLoop和mode, 以及mode和其对应的source, timer, observer是什么关系?
    A:


    image.png

    RunLoop的mode

    image.png

    运行在哪个mode上,就只能接收来自哪个mode的消息。例如当前运行在Mode1上,此时Mode2当中,source, timer, observer发消息,RunLoop是不会对事件进行处理的。

    Q:滑动tableview的时候,如果tableview中有广告栏,广告栏已经不会自动滚动了,是什么原因?
    A:

    image.png

    Q:比如timer既想在mode1上正常运行,在mode2也需要相应的处理和事件回调接收,那么timer怎样同时加入2个mode呢?
    A:系统是有提供添加到2个mode当中的机制的。

    commonMode

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

    Q:有使用过commonMode吗? 怎样理解commonMode?
    A:是同步Source/Timer/Observer到多个Mode的一种技术方案

    事件循环实现机制

    void CFRunLoopRun()


    image.png

    描述:在RunLoop启动后,会首先发送一个通知来告知观察者,当前RunLoop即将启动,RunLoop把将要处理的Timer/Source0事件发送,如果有Source1要处理,使用goto语句,代码逻辑跳转,处理唤醒时收到的消息。如果没有Source1要处理,线程将要休眠,同时发送通知给observer。就要发生从用户态到内核态的切换。唤醒RunLoop有几种方式,比如timer事件回调,外部手动唤醒。

    Q:处于休眠方式的RunLoop, 可以通过哪些方式唤醒?
    A:timer事件回调,外部手动唤醒, Source1

    RunLoop处于休眠状态,点击屏幕,产生machPort, 基于machPort,最终会转成Source1, 把主线程唤醒,运行,处理,把程序杀死时,就RunLoop退出,发送通知,即将退出RunLoop,RunLoop退出后,线程就销毁掉了

    RunLoop核心

    image.png

    Q:滑动TableView的时候,定时器还会生效吗?


    image.png

    A:TableView正常情况下是运行在kCFRunLoopDefaultMode下,当对TableView进行滑动时,会发生mode的切换,会切换到UITrackingRunLoopMode, 把timer, source, observer添加到某个mode, 如果当前RunLoop运行在另一个mode上,对应的timer, source, observer是无法进行后续的处理和回调。

    ***Mode和Mode之前是隔离开的。

    Q:如果想要滑动TableView的时候,定时器继续生效,要怎么做?
    A:

    commonMode不是实际的mode, 它只是为其他的mode打上common的标记,可以把某个事情源同步到多个mode.


    企业微信截图_bce4fdca-7c7d-4411-b576-b70e10e3a605.png
    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) {
           //首先会提取RunLoop的commonModes
        CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
    //同时判断runloop对应的commonItems是否为空,若为空,会重新创建集合
        if (NULL == rl->_commonModeItems) {
            rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
        }
            // commonModelItems 增加该 timer
        CFSetAddValue(rl->_commonModeItems, rlt);
        if (NULL != set) {
    //再把timer和Runloop封装成一个context,
            CFTypeRef context[2] = {rl, rlt};
            /* add new item to all common-modes */
            // 遍历每一个 commonMode,添加该 timer.
     //之后对集合中的每一个元素,都调用__CFRunLoopAddItemToCommonModes函数(对象元素,context)
            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();
            // 删除 timers 中的该 timer,并新增该 timer
                __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) {
        //取当前mode的名字,然后吧Runloop和Item取出来
        CFStringRef modeName = (CFStringRef)value;
        CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
        CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
        //根据当前item类型判断,来决定调用CFRunLoopAddSource还是CFRunLoopAddObserver还是CFRunLoopAddTimer,并不是循环调用,因为传进来的modeName 参数已经从commonMode变成被打上了标记的具体实际的mode
        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与多线程之间的关系

    Q:线程与RunLoop是什么关系?
    A:线程和RunLoop是一一对应的。自己创建的线程默认是没RunLoop的,需要手动创建RunLoop

    Q:怎样实现常驻线程?
    A:
    1). 为当前线程开启RunLoop
    2). 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环
    3). 启动该RunLoop

    image.png image.png

    Q:怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?
    A:用户滑动操作, RunLoop是运行在UITrackingRunLoopMode下,而一般网络请求是放在子线程下,而子线程返回给主线程的数据,用来更新UI,可以把子线程返回给主线程的数据包装起来, 然后提交到主线程的default模式下, 那么抛回来的任务, 就不会执行,当我们手停止滑动操作后,当前线程切换到default模式下,这时会处理子线程返回给主线程的任务,这时就不会打断用户的滑动操作.

    相关文章

      网友评论

          本文标题:iOS---RunLoop面试问题

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