iOS开发肯定离不开多线程编程,而多线程又跟RunLoop有着密切的关系,这篇文章就来解剖下RunLoop。
每个application运行都会开启一个主线程(UI线程),主线程默认是开启RunLoop的,让application可以随时接收用户的触摸事件实现交互,也可以处理复杂的业务逻辑,还可以休眠。
当我们开启子线程执行任务时,子线程默认是不开启RunLoop的,等子线程的任务执行完,子线程就会被系统销毁回收。但有时候我们会频繁的开启线程去执行任务,开启线程又销毁线程,这也是有一定的性能代价的,所以我们可以让一个子线程成为常驻线程,有任务就执行,没任务就休眠,这样就降低频繁开启和销毁线程的性能浪费。
让一个子线程成为常驻线程就必须开启子线程的RunLoop。开启RunLoop必须要有一个输入源或定时源,不然RunLoop开启就会马上关闭。输入源(input source)传递异步事件,通常事件来自其他的线程或程序。定时源(timer source)则传递同步事件,发生在特定时间或重复的时间间隔的事件。RunLoop的运行要指定其运行模式,无论是隐式或显式。
RunLoop模式
- kCFRunLoopDefaultMode: 默认模式,
- UITrackingRunLoopMode: 界面追踪模式,一般用于scrollView滑动触摸追踪
- UIInitializationRunLoopMode: 启动APP模式,启动完成后就不再使用
- NSRunLoopCommonModes: 占位模式,包含多种模式:default,modal,tracking
除了系统的模式,我们也可以使用自定义模式,NSRunLoopMode的字符串类型可以用于自定义。
RunLoop模式的切换
- 对于非主线程,我们可以退出当前模式,然后再进入另一个模式,也可以直接进入另一个模式,即嵌套
- 对于主线程,我们当然也可以像上面一样操作,但是主线程有其特殊性,有很多系统的事件。系统会做一些切换,我们更关心的是系统是如何切换的?系统切换模式时,并没有使用嵌套
简单开启子线程示例代码如下
- (void)startThread { @autoreleasepool {
NSThread *currentThread = [NSThread currentThread];
BOOL isCancelled = [currentThread isCancelled];
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
/**** 以时钟开启RunLoop ****/
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] repeats:YES block:^(NSTimer * _Nonnull timer) {
// 空任务
}];
/*
[NSTimer scheduledTimerWithTimeInterval:5 repeats:YES block:^(NSTimer * _Nonnull timer) {
// 空任务
}];
*/
// 开启RunLoop
while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
isCancelled = [currentThread isCancelled];
NSLog(@"run ----------- run");
}
}}
RunLoop的开启有几种方法,run, runUntilDate:, runMode:beforeDate:。run方法启动要关闭RunLoop就比较麻烦了,其它两个方法都可以轻易关闭RunLoop。
注意,以** 输入源 唤醒线程做任务,做完任务就会退出RunLoop,如果是以runMode:beforeDate:启动的RunLoop就会直接退出,子线程执行完毕被回收,另外两个方法启动的RunLoop,会退出RunLoop然后又进入RunLoop。以 时钟源 唤醒线程做任务,除了run方法外的启动RunLoop,会受到设置的期限影响,进而退出RunLoop。这里的示例代码为了方便控制RunLoop,使用runMode:beforeDate:启动,还加上线程的取消标志,让RunLoop退出又马上以runMode:beforeDate:**启动,直到当线程取消,使while循环被打破。
结束子线程的代码如下
- (void)stopRunLoop {
[_thread cancelled];
[self performSelector:@selector(stop) onThread:_thread withObject:nil waitUntilDone:NO];
}
/// 空任务唤醒线程
- (void)stop {}
空任务是为了唤醒线程,使子线程走到while循环,然后退出while循环。
RunLoop的观察者
RunLoop除了处理输入源和定时源的事件,也会生成RunLoop行为的通知。可以用Core Foundation框架注册观察者,实现对RunLoop行为的观察。使用观察者可以很清晰地知道RunLoop的行为,方便调试和实现功能。注册观察者使用C语言代码,如下
-(void)addRunloopObserver{
//获取当前的RunLoop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//定义一个centext
CFRunLoopObserverContext context = {
0,
( __bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
//定义一个观察者
static CFRunLoopObserverRef defaultModeObsever;
//创建观察者
defaultModeObsever = CFRunLoopObserverCreate(NULL,
kCFRunLoopAllActivities,
YES,
NSIntegerMax - 999,
&ObserverCallback,
&context
);
//添加当前RunLoop的观察者
CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
//c语言有creat 就需要release
CFRelease(defaultModeObsever);
}
/// 定义一个回调函数 RunLoop行为监听
static void ObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
}
其中的参数都是比较简单的,不在这里一一细说了_。
自定义输入源
自定义输入源就比较复杂了,自己定义两个文件RunLoopSource和RunLoopContext,实现相关的功能。RunLoopSource需要实现的方法
/// 添加输入源到当前RunLoop
- (void)addToCurrentRunLoop;
/// 移除输入源
- (void)invalidate;
/// 当输入源唤醒RunLoop执行的任务
- (void)sourceFired;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
/// 唤醒RunLoop
- (void)fireAllCommands;
在 RunLoopSource 实现文件创建输入源并初始化。
- (instancetype)init {
if (self = [super init]) {
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL,
NULL, &RunLoopSourceScheduleRoutine, RunLoopSourceCancleRoutine, RunLoopSourcePerformRooutine };
// 初始化输入源
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
commands = [[NSMutableArray alloc] init];
}
return self;
}
RunLoopSourceScheduleRoutine是将输入源添加到runloop的回调方法,定义如下
void RunLoopSourceScheduleRoutine(void *info, CFRunLoopRef rl, CFStringRef mode) {
}
RunLoopSourcePerformRooutine是输入源被告知时用来处理自定义数据的回调方法,定义如下
void RunLoopSourcePerformRooutine(void *info) {
}
RunLoopSourceCancleRoutine是将输入源从runloop移除的回调方法,定义如下
void RunLoopSourceCancleRoutine(void *info, CFRunLoopRef rl, CFStringRef mode) {
}
实现后相关的方法后,可以在子线程把RunLoopSource添加进去,这里使用run方法启动RunLoop
/// 添加输入源到runloop
- (void)addToCurrentRunLoop {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
_runLoop = runLoop;
CFRunLoopRun();
}
显式唤醒runloop,当客户端准备好处理加入缓冲区的命令后会调用此方法
- (void)fireAllCommands {
CFRunLoopSourceSignal(runLoopSource);
CFRunLoopWakeUp(_runLoop);
}
子线程被唤醒执行的任务
- (void)sourceFired {
NSLog(@"sourceFired -- %@", [NSThread currentThread]);
}
结束RunLoop,以退出子线程,注意,这个方法一定要在子线程里面调用
- (void)stopRunLoop {
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopStop(runLoop);
}
迷之总结
使用这种自定义输入源,可以在任何时候唤醒子线程执行任务,而且RunLoop不会在执行完任务后就退出然后又进入(要用run方法启动)。当然,这个执行任务是固定的,跟时钟源以重复间隔开启RunLoop的效果很像,不过这种自定义输入源可以随便在任何时刻唤醒线程执行任务,而时钟要以一定的时间间隔。
用run方法启动RunLoop,就要用CFRunLoopStop结束RunLoop,不过苹果官方文档不推荐使用CFRunLoopStop来结束RunLoop。在我的示例代码,虽然可以结束到RunLoop,但不是马上结束的,有一定的延时,由系统来决定结束的时间,通过观察者就可以很好地观察到其行为。
经过测试,先移除RunLoop的输入源,在唤醒线程,然后线程不执行任务就直接退出RunLoop,退出线程。
示例代码已经上传到 GitHub