RunLoop 详解

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 是若干个 sourcetimerobserver 的集合,它能帮我们过滤掉一些不想要的事件。即一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件 。要保持一个 mode 活着,就必须往里面添加至少一个 source、timer 或 observer 。

苹果公开的 mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode。前者是默认的模式,程序运行的大多时候都处于该 mode 下,后者是滑动 tableView 等时为了界面流畅而用的 mode。还有个 UIInitializationRunLoopMode 是程序启动时进入的 mode,一般用不上。

CFRunLoop 还定义了一个伪 mode 叫 kCFRunLoopCommonModes,它不是一个真正的 mode,而是若干个 mode 的集合,加到 CommonModesource/timer/observer 相当于添加到了它里面所有的 mode 中。我们可以通过 NSLog(@"%@", [NSRunLoop currentRunLoop]) 从打印结果看到 CommonMode 包含了上面的 DefaultModeTrackingRunLoopMode

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 中,这样就可以在 DefaultModeTrackingRunLoopMode 下都能工作了。

CFRunLoopSourceRef

source 是事件产生的地方(输入源),虽然官方文档在概念上把 source 分为三类:Port-Based SourcesCustom Input SourcesCocoa 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,只能通过 CFRunLoopGetMainCFRunLoopGetCurrent 函数,获取过程大致如下:

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 不退出也不占用资源。


参考:

CFRunLoop.c

官方文档

深入理解RunLoop

视频: iOS线下分享《RunLoop》by 孙源@sunnyxx

Run, RunLoop, Run!

Understanding NSRunLoop