大厂常见RunLoop底层原理面试题
写这篇文章我也参考和翻阅了很多相关的文章和面试题,发现有的文章是提供了答案,但是答案缺少前后场景,基本看不懂,看不懂就更别说记得住了。 我觉得我写博客,前提是自己复习和学习,自己要看得懂和明白,不然写出来还有什么用。 大家目标是为了面试,我是尽量舍去了一些面试时不可能口头或笔试写不出来的东西。这样简洁一些,而且能保持逻辑连贯性。
面试造火箭,入职拧螺丝。为了好的offer,也得拼了。
开始面试:
面试露出了慈祥的笑容,你简历上提到不少RunLoop啊。那咱们就聊聊RunLoop,我问题比较多。看你接不接的住。
先说说RunLoop 是什么?
帅气迷人的面试官你好:
Runloop是通过内部维护一个事件循环来对事件、消息进行管理的一个对象。是的,它是一个对象。 大家用C语言过main函数的都知道,main函数运行完成后程序就结束退出了。但是为什么iOS的App的main函数运行完之后APP还能一直运行呢?这就是Runloop的功劳。 这也是Runloop最基本的应用。 参考下面iOS的main函数:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
Runloop是个对象,怎么获取呢?
-
Foundation [NSRunloop currentRunLoop];获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop];获得主线程的Runloop对象
-
Core Foundation CFRunLoopGetCurrent();获得当前线程的RunLoop对象 CFRunLoopGetMain();获得主线程的Runloop对象
结交人脉:
推荐我的高级iOS交流群:1001906160 ,密码‘000’,有一个共同的圈子很重要,结识人脉!里面都是iOS开发,全栈发展,欢迎入驻,共同进步!(群内会免费提供一些群主收藏的免费学习书籍资料以及整理好的几百道面试题和答案文档!)
再说说RunLoop的实现机制是什么?
内心OS: 有深了一层 ,还好我做好了准备。 为了方便Runloop机制的理解,下面写一段伪代码来表示一下RunLoop循环。
function runloop() {
initialize();
do {
var message = get_next_message();//从队列获取消息
process_message(message);//处理消息
} while (message != quit);//当触发quit条件时,Runloop退出
}
复制代码
从代码代码可以看出,Runloop的处理机制是 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息)。 RunLoop的核心是什么? 就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 当然RunLoop它不是简单的while循环,不是用sleep来休眠,毕竟sleep这方法也是会占用cpu资源的。那它是如何实现真正的休眠的呢?那就是:没有消息需要处理时,就会从用户态切换到内核态,用户态进入内核态后,把当前线程控制器交给内核态,这样的休眠线程是被挂起的,不会再占用cpu资源。 [图片上传失败...(image-96f66d-1595059117598)]
这里要注意用户态和内核态 这两个概念,还有mach_msg()方法。 内核态 这个机制是依靠系统内核来完成的(苹果操作系统核心组件 Darwin 中的 Mach )。
下面是RunLoop实现的流程源码:
/// RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里没有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1\. 通知 Observers: RunLoop 即将进入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 内部函数,进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2\. 通知 Observers: RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3\. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4\. RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5\. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
///6\. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7\. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
/// • 一个基于 port 的Source 的事件。
/// • 一个 Timer 到时间了
/// • RunLoop 自身的超时时间到了
/// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8\. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,处理消息。
handle_msg:
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,执行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 如果没超时,mode里没空,loop也没被停止,那继续loop。
} while (retVal == 0);
}
/// 10\. 通知 Observers: RunLoop 即将退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
复制代码
源码我删减了很多,看源码里的注释,可以了解个Runloop运行的流程。 咱们还是围绕RunLoop的核心来理解, 既然上面提到休眠是通过内核来完成的,那唤醒条件呢? 下面几个就是主要的唤醒Runloop的事件:
- 收到基于 port 的 Source1 的事件
- Timer到时间执行
- RunLoop自身的超时时间到了
- 被其他调用者手动唤醒
关于RunLoop的source1和source0
上面介绍了source1包括系统事件捕捉和基于port的线程间通信。什么是系统事件捕捉?又如何理解基于port的线程间通信?其实,我们手指点击屏幕,首先产生的是一个系统事件,通过source1来接受捕捉,然后由Springboard程序包装成source0分发给应用去处理,因此我们在App内部接受到触摸事件,就是source0,这一前一后的关系。source1 通过程序包装是会变成 source0的
image.png
RunLoop的有几种Mode, RunLoop设置Mode作用是什么?
RunLoop的运行模式共有5种,RunLoop只会运行在一个模式下,要切换模式,就要暂停当前模式,重写启动一个运行模式
- kCFRunLoopDefaultMode, App的默认运行模式,通常主线程是在这个运行模式下运行
- UITrackingRunLoopMode, 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)
- kCFRunLoopCommonModes, 伪模式,不是一种真正的运行模式
- UIInitializationRunLoopMode:在刚启动App时第进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
RunLoop设置Mode作用 设置Mode作用是指定事件在运行循环(Loop)中的优先级。 线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作)
为什么只有主线程的Runloop是自动开启的?
看iOS的main函数代码, 代码自动生成了autoreleasepool,这里就是调用了runloop。app启动时main函数就自动开启了主线程的runloop。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
PerformSelector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动TableView的Cell时,label是否刷新?为什么?
不刷新了。 因为NSTimer对象是以NSDefaultRunLoopMode添加到主运行循环中的时候, TableView(ScrollView)滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。如果希望在滚动时,定时器也能运行,那就应该使用common mode。 通过 CFRunloopAddTimer(runloop,timer ,commonMode) 实现。就是同步把事件源timer用同一个mode.
AFNetworking 中如何运用 Runloop?
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
复制代码
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
复制代码
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
解释一下Runloop在 NSTimer中的的作用
NSTimer 其实就是 CFRunLoopTimerRef,这两个类之间,是可以交换使用的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop 为了节省资源,在发生阻塞状态并不会准时回调给Timer。某个时间点被错过了,不会在延期时间后给你执行。比如等公交,如果10:10 有一趟公交,我没赶上,那我只能等 10:20 这一趟。10:10分那趟不会再回来的。
Runloop 和线程的关系?
Runloop 和是一对一的关系,一个线程对应一个 Runloop。主线程的默认就有了 Runloop。 可以通过数据结构看出来,创建线程时,线程默认是没有runloop的,需要手工创建线程的runloop。
有了线程,你觉得为什么还要有runloop?
Runloop最主要的作用 就是它如何在没有消息处理时休眠,在有消息时又能唤醒。这样可以提高CPU资源使用效率 。runloop 另外一个作用是消息处理。只有线程,是做不到这点的。
GCD 在Runloop中的使用?
GCD由子线程返回到主线程,只有在这种情况下才会触发 RunLoop。会触发 RunLoop 的 Source 1 事件。
AFNetworking 中如何运用 Runloop?
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
复制代码
RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
- (void)start {
[self.lock lock];
if ([self isCancelled]) {
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
[self.lock unlock];
}
复制代码
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
PerformSelector:afterDelay:这个方法在子线程中是否起作用?
不起作用,子线程默认没有 Runloop。 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。可以使用 GCD的dispatch_after来实现afterDelay这样的需求。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效,
CADispalyTimer和Timer哪个更精确
当然是CADisplayLink 更精确。
iOS设备的屏幕刷新频率是固定的,CADisplayLink在正常情况下会在每次刷新结束都被调用,精确度相当高。
看上面Runloop在 NSTimer中的使用的问题,就知道NSTimer的触发时间到的时候,runloop如果在阻塞状态,触发时间就会推迟到下一个runloop周期。可见 NSTimer的定时是很不靠谱的。
CADisplayLink使用场合相对专一,适合做UI的不停重绘,比如自定义动画引擎或者视频播放的渲染。NSTimer的使用范围要广泛的多,各种需要单次或者循环定时处理的任务都可以使用。在UI相关的动画或者显示内容使用 CADisplayLink比起用NSTimer的好处就是我们不需要在格外关心屏幕的刷新频率了,因为它本身就是跟屏幕刷新同步的。
面试结束:
小伙子很不错,问了这么多问题,都答上来了,这让我很没面子啊,明天继续来面试,还有很多问题呢。 面试官内心(我明天得找回场子来)。明天还有更精彩的面试其他内容。 我的内心OS:天呐噜,明天还有!! 这是要在走秃(走向秃顶的道路) 越走越远啊。...
推荐
- 直击2020——iOS 面试题大全(补充完整版)
- “新”携程,阿里,腾讯iOS面试常见问题合集(附答案)
- 我是如何同时拿到阿里和腾讯offer的
- 腾讯&阿里&美团&快手&字节等10公司面经
- 腾讯社招iOS面试记录
- 最新阿里腾讯头条美团等iOS面试总结
持续更新,多多关注