底层初窥——Runloop


Runloop 介绍

关于 Runloop 的文章在网上一搜一大片,这里也就不费心去写一篇了,这里推荐一篇写的比较全面的,有兴趣的可以去看。

iOS RunLoop详解

这篇文章,主要就像记录一些关于 Runloop 的面试题目,在解题的过程来加深对 Runloop 的了解。

Runloop 相关问题

Runloop 对于一个标准的 iOS 开发来说都不陌生,下面就随便列几个典型问题吧.

app如何接收到触摸事件的?

app如何接收到触摸事件

  • 系统响应阶段
  1. 手指触碰屏幕,屏幕感应到触碰后,将事件交由 IOKit 处理。

  2. IOKit 将触摸事件封装成一个 IOHIDEvent 对象,并通过 mach port 传递给 SpringBoad 进程。

mach port 进程端口,各进程之间通过它进行通信。
SpringBoad.app 是一个系统进程,可以理解为桌面系统,可以统一管理和分发系> 统接收到的触摸事件。
SpringBoard 进程因接收到触摸事件,触发了主线程 runloop 的 source1 事件源的回调。

  1. 此时 SpringBoard 会根据当前桌面的状态,判断应该由谁处理此次触摸事件。因为事件发生时,你可能正在桌面上翻页,也可能正在刷微博。若是前者(即前台无 APP 运行),则触发 SpringBoard 本身主线程 runloop 的 source0 事件源的回调,将事件交由桌面系统去消耗;若是后者(即有 App 正在前台运行),则将触摸事件通过 IPC 传递给前台 APP 进程,接下来的事情便是 APP 内部对于触摸事件的响应了。
  • App 响应阶段
  1. APP 进程的 mach port 接受到 SpringBoard 进程传递来的触摸事件,主线程的 runloop 被唤醒,触发了 source1 回调。
  2. source1 回调又触发了一个 source0 回调,将接收到的 IOHIDEvent 对象封装成 UIEvent 对象,此时 APP 将正式开始对于触摸事件的响应。
  3. source0 回调内部将触摸事件添加到 UIApplication 对象的事件队列中。事件出队后,UIApplication 开始一个寻找最佳响应者的过程,这个过程又称 hit-testing。另外,此处开始便是与我们平时开发相关的工作了。
  4. 寻找到最佳响应者后,接下来的事情便是事件在响应链中的传递及响应了。事实上,事件除了被响应者消耗,还能被手势识别器或是 target-action 模式捕捉并消耗掉。其中涉及对触摸事件的响应优先级。
  5. 触摸事件历经坎坷后要么被某个响应对象捕获后释放,要么最后也没能找到能够响应的对象,最终释放。至此,这个触摸事件的使命就算终结了。runloop 若没有其他事件需要处理,也将重归于眠,等待新的事件到来后唤醒。

步骤3,就是寻找最佳响应者。
步骤4,就是寻找事件响应者。


为什么只有主线程的runloop是开启的?

  • 目的:

保持程序持续运行,如果没有 Runloop。 在运行,当主线程执行完当前任务后,线程自动销毁了。

  • 原因:

我们的 iOS 程序能保持持续运行的原因就是在 main() 函数中调用了 UIApplicationMain 函数,这个函数内部会启动主线程的 RunLoop。

CFRunLoopRun

1
2
3
4
5
6
7
8
void CFRunLoopRun(void) {   /* DOES CALLOUT */
int32_t result;
do {
//默认在kCFRunLoopDefaultMode下运行runloop
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

在 CFRunLoopGetCurrent() 方法中:

  • RunLoop和线程的一一对应的,对应的方式是以 key-value 的方式保存在一个全局字典中。
  • 主线程的 RunLoop 会在初始化全局字典时创建。

PerformSelector和 Runloop 的关系?

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。


Runloop 和 AutoreleasePool?

AutoreleasePool 是另一个与 RunLoop 相关讨论较多的话题。其实 从RunLoop 源代码分析,AutoreleasePool 与 RunLoop 并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在 iOS 应用启动后会注册两个 Observer 管理和维护 AutoreleasePool。可以应用程序刚刚启动时打印 currentRunLoop 可以看到系统默认注册了很多个 Observer,其中有两个 Observer 的 callout 都是 ** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。

  • 第一个 Observer 会监听 RunLoop 的进入:
    • 会回调 objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。
  • 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态
    • 在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用 objc_autoreleasePoolPop() 释放自动自动释放池内对象。

实现一个常驻线程?

线程保活

  1. 在子线程中启动 Runloop,注意添加的那个空 port。并使用 runMode:beforeDate: 启动。
1
2
3
4
5
6
7
8
9
10
11
- (void)run {
@autoreleasepool {
NSLog(@"current thread = %@", [NSThread currentThread]);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
if (!self.emptyPort) {
self.emptyPort = [NSMachPort port];
}
[runLoop addPort:self.emptyPort forMode:NSDefaultRunLoopMode];
[runLoop runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];
}
}
  1. 退出时,使用 CFRunLoopStop()
1
2
3
4
5
- (void)stopThread {
CFRunLoopStop(CFRunLoopGetCurrent());
NSThread *thread = [NSThread currentThread];
[thread cancel];
}

使用 performSelector:withObject:afterDelay 验证 Runloop 是否停止。

我们知道 performSelector:withObject:afterDelay 依赖于线程的 runloop,因为它本质上是由一个定时器负责定期加入到 runloop 中执行。所以如果这个方法可以成功执行,说明当前线程的 runloop 已经开启,否则则说明没有启动。