RunLoop
是 iOS 和 OSX 开发中非常重要的一个概念,本文主要从以下几部分介绍 RunLoop
:
本文用到的一些例子可见 我的Demo
一、基本概念
1、RunLoop
从字面上来看,RunLoop 是个运行循环。而官方文档对 RunLoop 的介绍是:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
即 RunLoop 是与线程相关的事件处理循环,我们可以在这里安排操作和协调对从外面传进来的事件的接收。RunLoop 的目的是让我们的线程在有活干的时候工作,在没活干的时候进入睡眠。所以 RunLoop 能让程序保持活着,在没有消息可处理时休眠来节省 CPU 资源。这种机制也不是 iOS / OSX 特有。
如果没有 RunLoop,我们的程序是这样在一条线程中从上之下执行一次就退出的:
1 | int someFunc() { |
2 | // do something |
3 | return 0; |
4 | } |
而在 RunLoop 中,要保持线程总是活着,能不断的处于“接受消息 –> 等待 –> 处理消息”的循环中,则大致逻辑如下:
1 | int runloop() { |
2 | do { |
3 | receive_message(); |
4 | wait(); |
5 | process_message(); |
6 | } while (!quit); |
7 | return 0; |
8 | } |
在 Xcode 的项目中,main
函数中调用的 UIApplicationMain
函数内部就启动了一个 RunLoop,保持程序的持续运行,而且这个默认启动的 RunLoop 的与主线程相关联的:
1 | int main(int argc, char * argv[]) { |
2 | @autoreleasepool { |
3 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); |
4 | } |
5 | } |
也因为 UIApplicationMain
一直在运行,没有返回,所以如果把 main
函数改为下面这样,则 UIApplicationMain
函数之后的代码在程序运行阶段都是不会执行的:
1 | int main(int argc, char * argv[]) { |
2 | @autoreleasepool { |
3 | int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); |
4 | // 只要程序在运行,不会运行到下面这句 |
5 | NSLog(@"after UIApplicationMain"); |
6 | return result; |
7 | } |
8 | } |
在 iOS 中,RunLoop 就是个对象,在 CoreFoundation
框架为 CFRunLoopRef
对象,它提供了纯 C 函数的 API,并且这些 API 是线程安全的;而在 Foundation
框架中用 NSRunLoop
对象来表示,它是基于 CFRunLoopRef
的封装,提供的是面向对象的 API,但这些 API 不是线程安全的。CFRunLoopRef
的代码是开源的,我们可以在 这里 或 这里 找到 CFRunLoop.c
来查看 RunLoop 的源码。
2、构成元素
在 CoreFoundation 中关于 RunLoop 有 5 个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
这 5 个类都在 CFRunLoop.c
中都有定义,除 CFRunLoopModeRef
外都在 CFRunLoop.h
公开:
1 | // CFRunLoop.h |
2 | typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef; |
3 | typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef; |
4 | typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef; |
5 | typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef; |
6 | |
7 | // CFRunLoop.c |
8 | struct __CFRunLoop { |
9 | pthread_t _pthread; |
10 | CFMutableSetRef _commonModes; |
11 | CFRunLoopModeRef _currentMode; |
12 | ... |
13 | }; |
14 | |
15 | struct __CFRunLoopMode { |
16 | CFMutableSetRef _sources0; |
17 | CFMutableSetRef _sources1; |
18 | CFMutableArrayRef _observers; |
19 | CFMutableArrayRef _timers; |
20 | ... |
21 | }; |
所以一个 RunLoop 对应一条线程,可以包含若干个 mode,但一个时刻只能在一个 mode 上运行(即 currentMode
),要切换 mode 只能退出 loop 再指定一个 mode 后重新进入。每个 mode 可以包含若干个 source/timer/observer
,不同组之间的互不影响。这 5 个类的关系大致如下:
CFRunLoopModeRef
官方文档介绍如下:
A run loop mode is a collection of input sources and timers to be monitored and a collection of run loop observers to be notified.
You must be sure to add one or more input sources, timers, or run-loop observers to any modes you create for them to be useful.
You use modes to filter out events from unwanted sources during a particular pass through your run loop.
即一个 run loop mode
是若干个 source
、timer
和 observer
的集合,它能帮我们过滤掉一些不想要的事件。即一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件 。要保持一个 mode 活着,就必须往里面添加至少一个 source、timer 或 observer 。
苹果公开的 mode
有两个:kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
) 和 UITrackingRunLoopMode
。前者是默认的模式,程序运行的大多时候都处于该 mode 下,后者是滑动 tableView
等时为了界面流畅而用的 mode。还有个 UIInitializationRunLoopMode
是程序启动时进入的 mode,一般用不上。
CFRunLoop 还定义了一个伪 mode 叫 kCFRunLoopCommonModes
,它不是一个真正的 mode,而是若干个 mode 的集合,加到 CommonMode
的 source/timer/observer
相当于添加到了它里面所有的 mode 中。我们可以通过 NSLog(@"%@", [NSRunLoop currentRunLoop])
从打印结果看到 CommonMode
包含了上面的 DefaultMode
和 TrackingRunLoopMode
:
1 | common modes = <CFBasicHash 0x7fdaa0d00ae0 [0x1084b57b0]>{type = mutable set, count = 2, |
2 | entries => |
3 | 0 : <CFString 0x10939f950 [0x1084b57b0]>{contents = "UITrackingRunLoopMode"} |
4 | 2 : <CFString 0x1084d5b40 [0x1084b57b0]>{contents = "kCFRunLoopDefaultMode"} |
5 | } |
因为 mode 类没有公开,所以是通过 CFStringRef
字符串来操作。
如要查看程序什么时候处于哪个 mode,我们可以新建一个 project
,在界面上添加一个 textField
,然后 ViewController.m
代码如下:
1 | @interface ViewController () <UITextViewDelegate> |
2 | @property (weak, nonatomic) IBOutlet UITextView *textFeild; |
3 | @end |
4 | |
5 | @implementation ViewController |
6 | - (void)viewDidLoad { |
7 | [super viewDidLoad]; |
8 | self.textFeild.delegate = self; |
9 | NSLog(@"viewDidLoad: %@", [NSRunLoop currentRunLoop].currentMode); |
10 | } |
11 | |
12 | - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { |
13 | NSLog(@"touch: %@", [NSRunLoop currentRunLoop].currentMode); |
14 | } |
15 | |
16 | #pragma mark - UITextViewDelegate |
17 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView { |
18 | NSLog(@"scrollViewDidScroll: %@", [NSRunLoop currentRunLoop].currentMode); |
19 | } |
20 | @end |
运行后,先后点击屏幕、拖拽 textField
,打印如下:
1 | 2016-05-20 23:51:33.869 RunLoopTest[3317:206736] viewDidLoad: UIInitializationRunLoopMode |
2 | 2016-05-20 23:51:38.636 RunLoopTest[3317:206736] touch: kCFRunLoopDefaultMode |
3 | 2016-05-20 23:51:40.709 RunLoopTest[3317:206736] scrollViewDidScroll: kCFRunLoopDefaultMode |
4 | 2016-05-20 23:51:40.732 RunLoopTest[3317:206736] scrollViewDidScroll: UITrackingRunLoopMode |
5 | 2016-05-20 23:51:40.754 RunLoopTest[3317:206736] scrollViewDidScroll: UITrackingRunLoopMode |
6 | ... |
所以在 viewDidLoad
时是处于 UIInitializationRunLoopMode
,之后程序大部分时间处于 kCFRunLoopDefaultMode
,当滑动 scrollView
时会将切换到 UITrackingRunLoopMode
。
一个经典的问题是:当我们用 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES]
添加一个 timer
到主线程时,默认是添加到 DefaultMode
上,刚开始时 timer 可以正常工作并调用 [self run]
方法,但若此时滑动了 textView ,RunLoop 就切换到了 UITrackingRunLoopMode
,这样处于另外一个 mode(DefaultMode)
的这个 timer 就会失效,使之不影响到滑动效果。如果我们希望 timer 在滑动 textView 的时候仍能正常工作,则需要用下面的做法把 timer 加进 CommonMode
中,这样就可以在 DefaultMode
或 TrackingRunLoopMode
下都能工作了。
CFRunLoopSourceRef
source 是事件产生的地方(输入源),虽然官方文档在概念上把 source 分为三类:Port-Based Sources
,Custom Input Sources
,Cocoa Perform Selector Sources
。
但在源码中 source 只有两个版本:source0 和 source1,它们的区别在于它们是怎么被标记 (signal) 的。
source0
是 app 内部的消息机制,使用时需要调用 CFRunLoopSourceSignal()
来把这个 source 标记为待处理,然后掉用 CFRunLoopWakeUp()
来唤醒 RunLoop,让其处理这个事件。
1 | void CFRunLoopSourceSignal(CFRunLoopSourceRef rls) { |
2 | if (__CFIsValid(rls)) { |
3 | __CFRunLoopSourceSetSignaled(rls); |
4 | } |
5 | } |
6 | void CFRunLoopWakeUp(CFRunLoopRef rl) { |
7 | if (__CFRunLoopIsIgnoringWakeUps(rl)) { |
8 | return; |
9 | } |
10 | SetEvent(rl->_wakeUpPort); |
11 | } |
用上面的 project,在 touchesBegan...
方法中打断点,点击屏幕可以看到调用栈是这样的:
注意到 RunLoop 是在 CFRunLoopRun
函数(下面再介绍)中调用了 __CFRunLoopDoSources0()
来处理 source0,它的过程简化如下:
1 | static Boolean __CFRunLoopDoSources0(CFRunLoopRef rl, CFRunLoopModeRef rlm, Boolean stopAfterHandle) { |
2 | Boolean sourceHandled = false; |
3 | // 判断 source 是否为空 |
4 | if (NULL != sources) { |
5 | // 判断 source 是否被标记 |
6 | if (__CFRunLoopSourceIsSignaled(rls)) { |
7 | // 取消标记 |
8 | __CFRunLoopSourceUnsetSignaled(rls); |
9 | // 判断 source 是否有效 |
10 | if (__CFIsValid(rls)) { |
11 | // 处理 source |
12 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(...); |
13 | sourceHandled = true; |
14 | } |
15 | return sourceHandled; |
16 | } |
17 | |
18 | __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ 函数看起来很长,但其实也没干什么事,只是调用函数: |
19 | |
20 | static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) { |
21 | if (perform) { |
22 | perform(info); |
23 | } |
24 | } |
其他类似的还有下面几个,它们都只是帮助我们在调用栈上调试,确保所有的代码调用都从这几种函数中的某一个开始的:
static void CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK();
static void CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION();
static void CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION();
source1
是基于 mach_ports
的,用于通过内核和其他线程互相发送消息。iOS / OSX 都是基于 Mach
内核,Mach 的对象间的通信是通过消息在两个端口(port)之间传递来完成。很多时候我们的 app 都是处于什么事都不干的状态,在空闲前指定用于唤醒的 mach port 端口,然后在空闲时被 mach_msg()
函数阻塞着并监听唤醒端口, mach_msg() 又会调用 mach_msg_trap()
函数从用户态切换到内核态,这样系统内核就将这个线程挂起,一直停留在 mac_msg_trap 状态。直到另一个线程向内核发送这个端口的 msg 后, trap 状态被唤醒, RunLoop 继续开始干活
当程序在运行但又空闲的时候,我们可以暂停它,可以看到此时的调用栈是这样的:
CFRunLoopTimerRef
CFRunLoopTimerRef
基于时间的触发器,在 iOS 用到的 NSTimer 或者 performSelector:afterDelay:
都是通过它来实现的。使用时先设置一个时间长度和一个回调,然后将其加入 RunLoop,这样 RunLoop 就会注册对应的时间点,当到了该时间点时就会唤醒 RunLoop 来执行那个回调。iOS7 之后,timer 还可有一个 tolerance
,因为 timer 不太准确,如上面提到的,某个 mode 下的 timer 在 RunLoop 切换 mode 时可能就失效了,而 tolerance
则用来计算最后能执行那个回调的时间点。
CFRunLoopObserverRef
CFRunLoopObserverRef
是观察者,可以用来观测 RunLoop 的状态的变化。可以观测的情况有:
1 | /* Run Loop Observer Activities */ |
2 | typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { |
3 | // 即将进入 loop |
4 | kCFRunLoopEntry = (1UL << 0), |
5 | // 即将处理 timer |
6 | kCFRunLoopBeforeTimers = (1UL << 1), |
7 | // 即将处理 source |
8 | kCFRunLoopBeforeSources = (1UL << 2), |
9 | // 即将 sleep |
10 | kCFRunLoopBeforeWaiting = (1UL << 5), |
11 | // 刚被唤醒,退出 sleep |
12 | kCFRunLoopAfterWaiting = (1UL << 6), |
13 | // 即将退出 |
14 | kCFRunLoopExit = (1UL << 7), |
15 | // 全部的活动 |
16 | kCFRunLoopAllActivities = 0x0FFFFFFFU |
17 | }; |
我们可以使用 CFRunLoopObserverCreateWithHandler()
来创建 observer,创建时设置要监听的状态变化和回调,再用 CFRunLoopAddObserver()
来给 RunLoop 添加 observer,当该 RunLoop 状态发生在监听类型内的变化时,observer 就会执行回调 :
1 | /* |
2 | 创建 observer: |
3 | 传入的参数:observer, 要监听 RunLoop 的哪些状态变化,是否重复,顺序,监听到状态变化的回调 |
4 | */ |
5 | CFRunLoopObserverRef observer = |
6 | CFRunLoopObserverCreateWithHandler( |
7 | CFAllocatorGetDefault(), |
8 | kCFRunLoopAllActivities, |
9 | YES, |
10 | 0, |
11 | ^(CFRunLoopObserverRef observer, |
12 | CFRunLoopActivity activity) { |
13 | NSLog(@"RunLoop 的状态变化:%zd", activity); |
14 | }); |
15 | CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); |
16 | CFRelease(observer); |
二、运行逻辑
1、获取 RunLoop
苹果不允许我们创建 RunLoop,要获取主线程或当前线程对应的 RunLoop,只能通过 CFRunLoopGetMain
或 CFRunLoopGetCurrent
函数,获取过程大致如下:
1 | // 全局的 dictionary, key 是 pthread_t, value 是 CFRunLoopRef |
2 | static CFMutableDictionaryRef __CFRunLoops = NULL; |
3 | |
4 | CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) { |
5 | // 第一次进入时,创建全局 dictionary |
6 | if (!__CFRunLoops) { |
7 | // 创建可变字典 |
8 | CFMutableDictionaryRef dict = CFDictionaryCreateMutable(); |
9 | // 先创建主线程的 RunLoop |
10 | CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); |
11 | // 主线程的 RunLoop 存进字典中 |
12 | CFDictionarySetValue(dict, pthread_main_thread_np(), mainLoop); |
13 | } |
14 | |
15 | // 用 传进来的线程 作 key,获取对应的 RunLoop |
16 | CFRunLoopRef loop = CFDictionaryGetValue(__CFRunLoops, t); |
17 | |
18 | // 如果获取不到,则新建一个,并存入字典 |
19 | if (!loop) { |
20 | CFRunLoopRef newLoop = __CFRunLoopCreate(t); |
21 | CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); |
22 | } |
23 | return loop; |
24 | } |
25 | |
26 | // 获取主线程的 RunLoop |
27 | CFRunLoopRef CFRunLoopGetMain(void) { |
28 | if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); |
29 | return __main; |
30 | } |
31 | |
32 | // 获取当前线程的 RunLoop |
33 | CFRunLoopRef CFRunLoopGetCurrent(void) { |
34 | return _CFRunLoopGet0(pthread_self()); |
35 | } |
可见,线程和 RunLoop 是一一对应的,对应关系保存在一个全局的 dictionary 中。RunLoop 类似懒加载,只有在第一次获取的时候才会创建。当线程销毁时,也销毁对应的 RunLoop。
2、RunLoop 的运行:
1 | // 用 DefaultMode 启动 |
2 | void CFRunLoopRun(void) { /* DOES CALLOUT */ |
3 | CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); |
4 | } |
5 | |
6 | // 用指定的 mode 启动 |
7 | SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { |
8 | return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled); |
9 | } |
10 | |
11 | // RunLoop 的实现 |
12 | SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { |
13 | // 根据 modeName 找到对应的 mode |
14 | CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); |
15 | // 判断 mode 是否为空 (即 source/timer 皆空),是的话则返回 |
16 | if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) { |
17 | return ; |
18 | } |
19 | // 通知 observers: 即将进入 loop |
20 | __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); |
21 | // 进入 loop |
22 | result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); |
23 | // 通知 observers: 即将退出 |
24 | __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); |
25 | |
26 | return; |
27 | } |
1 | // 进入 RunLoop 后 |
2 | static int32_t __CFRunLoopRun() { |
3 | // 设置 timer |
4 | dispatch_source_t timeout_timer = NULL; |
5 | // 设置过期时间 |
6 | seconds = 9999999999.0; |
7 | |
8 | int32_t retVal = 0; |
9 | |
10 | // 开始 loop |
11 | do { |
12 | // 告诉 observer:要处理 timer |
13 | __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); |
14 | // 告诉 observer:要处理 sources |
15 | __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); |
16 | |
17 | // 执行被加入的 block |
18 | __CFRunLoopDoBlocks(rl, rlm); |
19 | |
20 | // 处理 Sources0(非 port) |
21 | Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); |
22 | if (sourceHandledThisLoop) { |
23 | __CFRunLoopDoBlocks(rl, rlm); |
24 | } |
25 | |
26 | if (!didDispatchPortLastTime) { |
27 | // 如果有 GCD 分发到 main queue 的 block |
28 | if (__CFRunLoopServiceMachPort(dispatchPort, &msg)) { |
29 | // 跳过睡眠阶段,直接去处理消息 |
30 | goto handle_msg; |
31 | } |
32 | } |
33 | |
34 | // 通知 observers:即将进入睡眠 |
35 | if () __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); |
36 | |
37 | // 调用 mach_msg 等待接受 mach_port 的消息,线程将进入睡眠 |
38 | __CFRunLoopServiceMachPort(waitSet, &msg, ...); |
39 | |
40 | // 通知 observers:刚被唤醒 |
41 | __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); |
42 | |
43 | // 处理消息的标记 |
44 | handle_msg:; |
45 | |
46 | // 通过判断端口,找出要处理的事件 |
47 | if (MACH_PORT_NULL == livePort) { |
48 | // 纯粹是被手动唤醒的,无消息,则不做任何处理 |
49 | } else if (livePort == rlm->_timerPort) { |
50 | // 被 timer 唤醒,则触发这个 timer 的回调 |
51 | __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); |
52 | } else if (livePort == dispatchPort) { |
53 | // 被 GCD 唤醒,则执行所有调用 dispatch_async 等方法放入main queue 的 block |
54 | __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); |
55 | } else { |
56 | // 如果被 source1(基于 port) 唤醒的,则处理这个事件 |
57 | __CFRunLoopDoSource1(rl, rlm, &reply) || sourceHandledThisLoop; |
58 | if (NULL != reply) { |
59 | mach_msg(reply, MACH_SEND_MSG); |
60 | } |
61 | } |
62 | |
63 | // 执行加入到 loop 的 block |
64 | __CFRunLoopDoBlocks(rl, rlm); |
65 | |
66 | // 判断是否应该退出 loop |
67 | if (sourceHandledThisLoop && stopAfterHandle) { |
68 | // 传入的参数是否说明应该在处理完事件就返回 |
69 | retVal = kCFRunLoopRunHandledSource; |
70 | } else if (timeout_context->termTSR < mach_absolute_time()) { |
71 | // 是否过期 |
72 | retVal = kCFRunLoopRunTimedOut; |
73 | } else if (__CFRunLoopIsStopped(rl)) { |
74 | // 是否被强制停止 |
75 | retVal = kCFRunLoopRunStopped; |
76 | } else if (rlm->_stopped) { |
77 | retVal = kCFRunLoopRunStopped; |
78 | } else if (__CFRunLoopModeIsEmpty(rl, rlm)) { |
79 | // mode 是否为空,即 source、timer 为空 |
80 | retVal = kCFRunLoopRunFinished; |
81 | } |
82 | |
83 | // 都不是,则继续 loop |
84 | } while (0 == retVal); |
85 | |
86 | return retVal; |
87 | } |
而判断 mode 的逻辑大致如下:
1 | __CFRunLoopModeIsEmpty() { |
2 | if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false; |
3 | if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false; |
4 | if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false; |
5 | return true; |
6 | } |
将上面代码逻辑画成图如下:
三、与 RunLoop 相关的功能
1、自动释放池
一般我们比较关心的是自动释放池什么时候会释放?
在打印 [NSRunLoop currentRunLoop]
的结果中我们可以看到与自动释放池相关的:
1 | <CFRunLoopObserver>{activities = 0x1, callout = _wrapRunLoopWithAutoreleasePoolHandler} |
2 | <CFRunLoopObserver>{activities = 0xa0, callout = _wrapRunLoopWithAutoreleasePoolHandler} |
即 app 启动后,苹果会给 RunLoop 注册很多个 observers,其中有两个是跟自动释放池相关的,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
第一个 observer 监听的是 activities = 0x1
(kCFRunLoopEntry),也就是在即将进入 loop 时,其回调会调用 _objc_autoreleasePoolPush()
创建自动释放池;
第二个 observer 监听的是 activities = 0xa0
(kCFRunLoopBeforeWaiting | kCFRunLoopExit),即监听的是准备进入睡眠和即将退出 loop 两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用 _objc_autoreleasePoolPop()
释放旧的释放池,并调用 _objc_autoreleasePoolPush()
创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出 loop 时则会 _objc_autoreleasePoolPop()
释放池子。
2、界面更新
在当前 RunLoop 的打印结果我们还可以看到
1 | <CFRunLoopObserver >{activities = 0xa0,callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv} |
即准备进入睡眠和即将退出 loop 两个时间点,会调用函数更新 UI 界面.当在操作 UI 时,某个需要变化的 UIView/CALayer 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。所以如果在一次运行循环中想用如下方法设置一个 view 的两条移动路径是行不通的,因为它会把视图的属性变化汇总起来,直接让 myView 从起点移动到终点了:
1 | CGRect frame = self.myView.frame; |
2 | |
3 | // 先向下移动 |
4 | frame.origin.y += 200; |
5 | [UIView animateWithDuration:1 animations:^{ |
6 | self.myView.frame = frame; |
7 | [self.myView setNeedsDisplay]; |
8 | }]; |
9 | |
10 | // 再向右移动 |
11 | frame.origin.x += 200; |
12 | [UIView animateWithDuration:1 animations:^{ |
13 | self.myView.frame = frame; |
14 | [self.myView setNeedsDisplay]; |
15 | }]; |
3、RunLoop 与 GCD
RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。如当调用了 dispatch_async(dispatch_get_main_queue(), block)
时,主队列会把该 block 放到对应的线程(恰好是主线程)中,主线程的 RunLoop 会被唤醒,从消息中取得这个 block,回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
来执行这个 block:
四、应用
1、UIImageView 延迟加载图片
给 UIImageView 设置图片可能耗时不少,如果此时要滑动 tableView 等则可能影响到界面的流畅。解决是:使用 performSelector:withObject:afterDelay:inModes:
方法,将设置图片的方法放到 DefaultMode 中执行。
2、常驻线程
子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop,保持 RunLoop 运行,则使子线程保持不死。AFNetworking
基于 NSURLConnection
时正是这样做的,希望在后台线程能保持活着,从而能接收到 delegate 的回调。具体做法是:
1 | /* 返回一个线程 */ |
2 | + (NSThread *)networkRequestThread { |
3 | static NSThread *_networkRequestThread = nil; |
4 | static dispatch_once_t oncePredicate; |
5 | dispatch_once(&oncePredicate, ^{ |
6 | // 创建一个线程,并在该线程上执行下一个方法 |
7 | _networkRequestThread = [[NSThread alloc] initWithTarget:self |
8 | selector:@selector(networkRequestThreadEntryPoint:) |
9 | object:nil]; |
10 | // 开启线程 |
11 | [_networkRequestThread start]; |
12 | }); |
13 | return _networkRequestThread; |
14 | } |
15 | /* 在新开的线程中执行的第一个方法 */ |
16 | + (void)networkRequestThreadEntryPoint:(id)__unused object { |
17 | @autoreleasepool { |
18 | [[NSThread currentThread] setName:@"AFNetworking"]; |
19 | // 获取当前线程对应的 RunLoop |
20 | NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; |
21 | // 为 RunLoop 添加 source,模式为 DefaultMode |
22 | [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; |
23 | // 开始运行 RunLoop |
24 | [runLoop run]; |
25 | } |
26 | } |
因为 RunLoop 启动前必须设置一个 mode,而 mode 要存在则至少需要一个 source / timer。所以上面的做法是为 RunLoop 的 DefaultMode 添加一个 NSMachPort 对象,虽然消息是可以通过 NSMachPort
对象发送到 loop 内,但这里添加的 port 只是为了 RunLoop 一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer 来既保持 RunLoop 不退出也不占用资源。
参考: