iOS---RunLoop面试问题

2022-03-23  本文已影响0人  BabyNeedCare

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开头的是开源的。

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

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模式下,这时会处理子线程返回给主线程的任务,这时就不会打断用户的滑动操作.

上一篇下一篇

猜你喜欢

热点阅读