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