视图-①Runloop

视图-①Runloop

[toc]

一、RunLoop的理解

让线程永不休眠。

背景:负责持续性的处理各种任务(比如Source,Timer,Observer),让线程能一直运行,且在没有任务的时候能够进入休眠,减少 CPU 的使用率,从而节省电量和资源。

1、正常一个线程一次只能执行一个任务,执行完成后线程就退出了。为了让线程能随时处理事件但并不退出,使用do-while循环实现。

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

2、do-while的检查是一直主动检查?线程使用RunLoop时,不需要一直主动检查输入源是否有事件到来。而是靠事件驱动。即

事件驱动(Event-Driven)

在事件驱动的编程模型中,应用程序的执行流程是由事件(如用户操作、消息、定时器超时等)来驱动的。应用程序不需要不断地检查某个条件,而是注册事件处理函数,并让操作系统在相应的事件发生时通知应用程序。这样,应用程序就可以在没有事件发生时执行其他任务或者进入休眠状态,从而节省资源。

3、do-while性能消耗?

当 RunLoop 检测到没有待处理的事件时,它会将线程置于”休眠等待”状态,而不是忙等。这时,线程不会消耗 CPU 资源。当事件到来时,如用户输入、定时器超时或 I/O 完成等,RunLoop 会被唤醒,处理相应的事件,然后再次进入休眠状态。

Runloop是绑定到线程上的(每个线程可以有一个对应的 RunLoop 对象。这些 RunLoop 对象被保存在一个全局的 Dictionary 中,其中线程作为 Key,RunLoop 作为 Value。)。每个线程可以有自己的 RunLoop,这样每个线程可以独立地处理与自己任务相关的事件,提高了事件处理的效率和局部性。

每个Runloop有多种Model。不同的 Mode 可以包含不同的事件源(Sources)和定时器(Timers)。通过切换 Mode,RunLoop 可以过滤掉一些不想要的事件,只处理当前 Mode 下相关的事件。这样可以避免在处理特定任务时被不相关的事件打扰,提高程序的响应性和效率。例如当用户滚动列表时,iOS 应用程序的 RunLoop 通常会切换到 UITrackingRunLoopMode 模式。这个模式会降低非滚动相关的事件(如未将定时器添加到 NSRunLoopCommonModes 模式则timer会暂停)处理优先级,从而确保滚动操作的流畅性 。

主线程是如何切换runloop?

当系统检测到有scrollerview滑动时,系统就会将当前进程的主线程切换到UITrackingRunLoopMode,直到滑动结束,又会切换到NSDefaultRunLoopMode。

模拟主线程runloop的mode切换。在touchbegan的时候切换到UITrackingRunLoopMode,touchend的时候又切换回NSDefaultRunLoopMode。从模拟中可以看出如果所切到的mode是timer未添加的,则timer会暂停。这也就是为什么NSTimer需要设置在NSRunLoopCommonModes模式下运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  dispatch_async(dispatch_get_global_queue(0, 0), ^{
self.rl = CFRunLoopGetCurrent();

//timer1 运行在 default mode
NSTimer *timer1 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer1 forMode:NSDefaultRunLoopMode]; // NSDefaultRunLoopMode

//timer2 运行在 track Mode
NSTimer *timer2 = [NSTimer timerWithTimeInterval:1.f repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer2 fired");
}];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode]; // UITrackingRunLoopMode

//指定当前运行mode
self.currentMode = NSDefaultRunLoopMode;
while (1) {
[[NSRunLoop currentRunLoop] runMode:self.currentMode beforeDate:[NSDate distantFuture]];
}
});


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch began")
//touchbegan 切换成track mode
self.currentMode = UITrackingRunLoopMode;
CFRunLoopStop(self.rl);
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"touch end");
//touchend 切换成kCFRunLoopDefaultMode
self.currentMode = kCFRunLoopDefaultMode;
CFRunLoopStop(self.rl);
}

runloop与卡顿的关系

在iOS开发中,卡顿通常是由于主线程被长时间占用导致的。CFRunLoop的状态变化可以反映主线程的运行情况,因此通过监听CFRunLoop的状态,我们可以检测到应用的卡顿现象。

正常情况下,CFRunLoop会经历以下几个状态:

  1. kCFRunLoopEntry:即将进入RunLoop

  2. kCFRunLoopBeforeTimers:即将处理定时器。

  3. kCFRunLoopBeforeSources:即将处理输入源。

  4. kCFRunLoopBeforeWaiting:即将进入休眠。

  5. kCFRunLoopAfterWaiting:刚从休眠中唤醒。

  6. kCFRunLoopExit:即将退出RunLoop

为什么通常选择kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态来作为卡顿的判断依据,而不使用kCFRunLoopBeforeWaitingkCFRunLoopBeforeTimers呢?

因为①卡顿通常发生在事件处理阶段,而kCFRunLoopBeforeWaiting状态标志着RunLoop即将进入休眠,而不是正在处理事件。当应用处于静止状态,即用户没有进行任何操作时,RunLoop通常处于kCFRunLoopBeforeWaiting状态,等待新的事件到来。在这种状态下,线程会进入休眠模式,以节省CPU资源。

②至于kCFRunLoopBeforeTimers状态,虽然它也是RunLoop状态之一,但它主要表示RunLoop即将处理定时器事件。如果定时器回调执行时间过长,确实可能导致卡顿,但是在实际应用中,定时器回调通常执行时间较短,且定时器回调的执行时间可以通过调整定时器的触发频率来控制,因此kCFRunLoopBeforeTimers状态不是卡顿判断的主要依据。

在没有卡顿的情况下,CFRunLoopkCFRunLoopBeforeSourceskCFRunLoopAfterWaiting这两个状态的停留时间通常是非常短的。kCFRunLoopBeforeSources状态表示RunLoop即将处理输入源,而kCFRunLoopAfterWaiting状态表示RunLoop从休眠中被唤醒。如果主线程在这两个状态之间花费的时间过长,说明线程可能被阻塞,导致应用无法响应用户操作,从而出现卡顿。

这两个状态是RunLoop循环中的关键点,它们分别代表了事件处理前后的状态。如果主线程在这两个状态之间花费的时间过长,说明线程可能被阻塞,导致应用无法响应用户操作,从而出现卡顿。因此,通过监控这两个状态,我们可以有效地检测和优化应用的性能,提高用户体验。

runloop与崩溃的关系

在 iOS 开发中,由于 RunLoop 导致的崩溃通常不是直接由 RunLoop 本身引起的,而是由于 RunLoop 中的事件处理代码存在问题。

《起死回生/回光返照》见《异常与崩溃.md

runtime

运行时(Runtime)是 Objective-C 语言的核心特性之一,它提供了一组丰富的 API,允许程序在运行时查询和修改程序的行为。这种动态性使得 Objective-C 语言具有很高的灵活性。

1、RunLoop概念

Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其内部就是do-while循环,这个循环内部不断地处理各种任务(比如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。

2、RunLoop和线程的关系?

run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程相关的基础框架的一部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(以下都已Cocoa为例)。

每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。

①、主线程的run loop默认是启动的。

1
2
3
4
5
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。

②、对其它线程来说,run loop默认是没有启动的。

③、在任何一个Cocoa程序的线程中,都可以通过:NSRunLoop *runloop = [NSRunLoop currentRunLoop];来获取到当前线程的run loop。

3、RunLoop相关各类关系

在 CoreFoundation 里面关于 RunLoop 有5个类:

1
2
3
4
5
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系/RunLoop相关各类关系,如下图所示:

RunLoop相关各类关系
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

RunLoop的内部逻辑
RunLoop的内部逻辑

3.1 CFRunLoopSourceRef

Source有两个版本:Source0 和 Source1。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

在iOS 中,除了source1可以自己唤醒run loop之外,其他的事件都需要用户手动唤醒run loop才可以。

3.1.附上题中button点击后,关于RunLoop的过程(此处略过对响应链的描述)

大概为:当一个硬件事件(触摸/锁屏/摇晃等)发生后,
①、首先由 IOKit.framework 生成一个 IOHIDEvent 事件,Source1 接收到系统事件,RunLoop被唤醒
②、RunLoop通知Observer,处理Timer和Source 0
③、RunLoop处理Source 1,Source1 触发回调,并调用_UIApplicationHandleEventQueue() 进行应用内部的分发
_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
④、Springboard接受touch event,并用source1 的 之后mach port 转发给App进程。
⑤、RunLoop处理完毕进入睡眠,此前会释放旧的autorelease pool并新建一个autorelease pool。

3.2 CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调(NSTimer 其实就是 CFRunLoopTimerRef)。

3.3 CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

4、RunLoop的应用

最常见的为定时器 NSTimer

NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

讲到RunLoop,我们需明确一点。runloop会对timer有强引用,timer会对目标对象进行强引用

其他详细参考以下文章:

4.1、autoreleasepool 自动释放池

既然说到runloop,简单说下autoreleasepool自动释放池。runloop会默认创建autoreleasepool,在runloop睡眠前或者退出前会执行pop操作。线程池详情查看下面的内存管理中的介绍。

@autoreleasepool是自动释放池,让我们更自由的管理内存;所以我们下面说说内存管理。

4.2、runloop、autorelease pool以及线程之间的关系

END

其他参考文档:runloop