iOS RunLoop理解及实践场景分析

前言

线程知识

线程是任务分解成不同的工作单元分配给线程去执行,解决了多任务并发执行的问题,提高了系统性能。在现代交互式系统中,还存在大量的未知不确定的异步事件,这时候线程是一直是出于等待状态的,直到有事件发生才会唤醒线程去执行,执行完成后系统又恢复到以前的等待状态。

如何控制线程在等待和执行任务状态间无缝切换,就引入了RunLoop的概念。

一、RunLoop概念介绍

RunLoop称为事件循环,可以理解为系统中对各种事件源不间断的循环的处理。

应用在运行过程中会产生大量的系统和用户事件,包括定时器事件,用户交互事件(鼠标键盘触控板操作),模态窗口事件,各种系统Source事件,应用自定义的Source事件等等,每种事件都会存储到不同的FIFO先进先去的队列,等待事件循环依次处理。

被RunLoop管理的线程在挂起时,不会占用系统的CPU资源,可以说RunLoop是非常高效的线程管理技术。

RunLoop的作用:

  • a.使程序一直运行并接受用户输入
  • b.决定程序在何时应该处理哪些Event
  • c.调用解耦(Message Queue消息队列)- 比如用户手势操作,被调方队列处理一系列的事件
  • d.节省CPU时间(操作系统按照时间片调度)每个程序按照时间片切分执行,RunLoop使线程没事就休眠

二、RunLoop组成与机制

RunLoop与线程的关系

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)

组成

系统提供了两个对象:NSRunLoop 和 CFRunLoopRef,CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

RunLoop 的状态处理及mode切换

1
2
3
4
5
6
7
8
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

RunLoop注意点:
1.runloop在同一段时间只能且必须在一种特定Mode下run
2.要换Mode时,需要停止当前Loop,然后重启新loop
3.Mode是iOS App滑动顺畅的关键
4.自己也可以定制Mode

  • NSDefaultRunloopMode(默认状态,空闲状态)
  • UITrackingRunloopMode(滑动scrollview)
  • UIInitializationRunloopMode(私有,App启动时)
  • NSRunLoopCommonModes(mode集合,包含前2个)

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

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

    切换mode时需要退出loop重新指定一个mode进入,主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响

RunLoop Source

source是runloop的数据抽象类(比如protocol)

source0:处理App内部事件,App自己管理(触发) 如:UIEvent,CFSocket等

source1:由runloop和内核管理,Mach port驱动
自定义source:自定义Source实现

1、定义3个回调函数,将Source添加到线程的RunLoop,最后就是等待Signal信号触发事件唤醒RunLoop,最终执行回调函数RunLoopSourcePerformRoutine中的处理方法;

2、Source对应的处理线程,线程创建Source事件源并将其绑定到当前的runLoop,while循环中执行运行RunLoop到超时后退出当前RunLoop。由于没有注册到RunLoop的事件发生,线程被挂起休眠等待事件触发。

3、Appdelegate,启动线程后,建立Source,Source建立的回调通知AppDelegate当前正在注册source,AppDelegate将其保存在属性sources中。
UI界面创建按钮绑定事件到fireInputSource方法,用户点击按钮立即触发一次自定义的source事件。

RunLoop Timer

  • NSTimer 的2种初始化方式区别
1
2
3
self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(startTimerAction:) userInfo:nil repeats:YES];
// 将定时器添加到NSRunLoopCommonModes类型的定时器中去,防止用户其它操作时,定时器不执行
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

注:GCD方式创建的timer不受runloop影响;

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
self.queue = dispatch_get_main_queue();
self.timer2 = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue);
// 每隔2秒执行一次
uint64_t interval = (uint64_t)(2.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer2, 0, interval, 0);
dispatch_source_set_event_handler(self.timer2, ^(){
int timeCount = self.timeLabel2.text.intValue;
if(timeCount==0){
dispatch_cancel(self.timer2);
return ;
}
dispatch_async(dispatch_get_main_queue(), ^
int timeCount = self.timeLabel2.text.intValue;
timeCount = timeCount-1;
NSLog(@"timeCount%d",timeCount);
self.timeLabel2.text = [NSString stringWithFormat:@"%d",timeCount];
})
}
);
dispatch_resume(self.timer2);
  • performSelector delay 之类的延迟执行 也是 timer的封装
  • CADisplayLink

RunLoop Observer

cocoa框架中有很多机制都是有RunloopObserver触发,如CAAnimation
分析:比如多个Animation都被设置,在一个runloop结束后(知道所有设置值)开始跑animation
UIButton点击事件中,UITouch事件什么时候被释放 - 应该是observe被触发的时候释放的。

UIKit通过RunloopObserver在runloop两次sleep间对AutoreleasePool进行Pop和Push,将这次loop中产生的Autorelease对象释放

RunLoop内部流程

Alt text
runloop的挂起和唤醒 :

  1. 指定用于唤醒的mach-port端口
  2. 调用mach-msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach-msg-trap状态
  3. 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,runloop继续开始干活

三、RunLoop实践介绍

RunLoop在很多地方都有很好的实践:

NSTimer UIEvent Autorelease(autoreleasepool什么时候释放)
NSObject(NSDelayedPerforming,NSThreadPerformAddition)
CADisplayLink(60帧或30帧) CATransition CAAnimation
dispatch_get_main_queue() NSURLConnection
AFNetworking NSPort

AFNetwork2.x中保持线程

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (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 不至于退出,并没有用于实际的发送消息。当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:] 将这个任务扔到了后台线程的 RunLoop 中。

RunLoop在ASIHTTPRequest中实践

创建单例线程

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
37
38
// In the default implementation, all requests run in a single background thread
// Advanced users only: Override this method in a subclass for a different threading behaviour
// Eg: return [NSThread mainThread] to run all requests in the main thread
// Alternatively, you can create a thread on demand, or manage a pool of threads
// Threads returned by this method will need to run the runloop in default mode (eg CFRunLoopRun())
// Requests will stop the runloop when they complete
// If you have multiple requests sharing the thread or you want to re-use the thread, you'll need to restart the runloop
+ (NSThread *)threadForRequest:(ASIHTTPRequest *)request
{
if (networkThread == nil) {
@synchronized(self) {
if (networkThread == nil) {
networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequests) object:nil];
[networkThread start];
}
}
}
return networkThread;
}
+ (void)runRequests
{
// Should keep the runloop from exiting
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
BOOL runAlways = YES; // Introduced to cheat Static Analyzer
while (runAlways) {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
CFRunLoopRun();
[pool drain];
}
// Should never be called, but anyway
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}

RunLoop在GCDAsyncSocket中实践

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
+ (void)startCFStreamThreadIfNeeded
{
static dispatch_once_t predicate;
dispatch_once(&predicate, ^{
cfstreamThread = [[NSThread alloc] initWithTarget:self
selector:@selector(cfstreamThread)
object:nil];
[cfstreamThread start];
});
}
+ (void)cfstreamThread
{ @autoreleasepool{
[[NSThread currentThread] setName:GCDAsyncSocketThreadName];
LogInfo(@"CFStreamThread: Started");
// We can't run the run loop unless it has an associated input source or a timer.
// So we'll just create a timer that will never fire - unless the server runs for decades.
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
target:self
selector:@selector(doNothingAtAll:)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] run];
LogInfo(@"CFStreamThread: Stopped");
}}
1
2
3
4
5
6
7
8
9
10
11
12
13
+ (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket
{
LogTrace();
NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread");
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
if (asyncSocket->readStream)
CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode);
if (asyncSocket->writeStream)
CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode);
}

读写流都在default mode

tableview cell高度预缓存

高度预缓存是一个优化功能,它要求页面处于空闲状态时才执行计算,当用户正在滑动列表时显然不应该执行计算任务影响滑动体验。
每个任务都被分配到下个“空闲” RunLoop 迭代中执行,其间但凡有滑动事件开始,Mode 切换成 UITrackingRunLoopMode,所有的“预缓存”任务的分发和执行都会自动暂定,最大程度保证滑动流畅。

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
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
// This is a idle mode of RunLoop, when UIScrollView scrolls, it jumps into "UITrackingRunLoopMode"
// and won't perform any cache task to keep a smooth scroll.
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
// Collect all index paths to be precached.
NSMutableArray *mutableIndexPathsToBePrecached = self.fd_allIndexPathsToBePrecached.mutableCopy;
// Setup a observer to get a perfect moment for precaching tasks.
// We use a "kCFRunLoopBeforeWaiting" state to keep RunLoop has done everything and about to sleep
// (mach_msg_trap), when all tasks finish, it will remove itself.
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// Remove observer when all precache tasks are done.
if (mutableIndexPathsToBePrecached.count == 0) {
CFRunLoopRemoveObserver(runLoop, observer, runLoopMode);
CFRelease(observer);
return;
}
// Pop first index path record as this RunLoop iteration's task.
NSIndexPath *indexPath = mutableIndexPathsToBePrecached.firstObject;
[mutableIndexPathsToBePrecached removeObject:indexPath];
// This method creates a "source 0" task in "idle" mode of RunLoop, and will be
// performed in a future RunLoop iteration only when user is not scrolling.
[self performSelector:@selector(fd_precacheIndexPathIfNeeded:)
onThread:[NSThread mainThread]
withObject:indexPath
waitUntilDone:NO
modes:@[NSDefaultRunLoopMode]];
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);

参考文章
深入理解RunLoop