简介
Runloop可以保证程序会一直运行并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候运行,在没有操作的时候就停下来休息,节省CPU资源,提高程序性能,RunLoop就是控制线程生命周期并接收事件进行处理的机制。
CFRunLoopRef的代码是开源的,可以直接查看源码 源码
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
RunLoop能干什么
- 线程保活
- 处理App中的各种事件
基本概念
- Runloop,顾名思义就是跑圈,他的本质就是一个do,while循环,当有事做时就做事,没事做时就休眠。
- 每个线程都由一个Run Loop,主线程的Run Loop会在App运行的时自动运行,子线程需要手动获取运行,第一次获取时,才会去创建
- 每个Run Loop都会以一个模式mode来运行,runMode:beforeDate:
- Run Loop的处理两大类事件源:TimerSource和InputSource(包括performSelector *方法簇、Port或者自定义的InputSource),每个事件源都会绑定在RunLoop的某个特定模式mode上,而且只有RunLoop在这个模式下运行的时候,才会触发Timer和InputSource
- 最后,如果没有任何事件源添加到RunLoop上,RunLoop就会立刻exit
![](https://img.haomeiwen.com/i1430170/43be4ac05b4d70fd.png)
![](https://img.haomeiwen.com/i1430170/9f03057eb7f1da08.png)
RunLoop机制
主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:
-
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation -
CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
消息通知、非延迟的perform、dispatch调用、block回调、KVO -
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
CFRunloop is servicing the main desipatch queue -
CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延迟的perform, 延迟dispatch调用 -
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用 -
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
RunLoop的几种状态
- kCFRunLoopEntry -- 进入runloop循环
- kCFRunLoopBeforeTimers -- 处理定时调用前回调
- kCFRunLoopBeforeSources -- 处理input sources的事件
- kCFRunLoopBeforeWaiting -- runloop睡眠前调用
- kCFRunLoopAfterWaiting -- runloop唤醒后调用
- kCFRunLoopExit -- 退出runloop
如果没有添加任何输入源事件或Timer事件,线程会一直在无限循环空转中,会一直占用CPU时间片,没有实现资源的合理分配。
没有while循环且没有添加任何输入源或Timer的线程,线程会直接完成,被系统回收。
CFRunLoopMode
- NSDefaultRunLoopMode:默认的运行模式,除了NSConnection对象的事件。
- UITrackingRunLoopMode:用于跟踪触摸事件触发的模式(UIScrollView上下滚动), 主线程当触摸事件触发会设置为这个模式,可以用来在控件事件触发过程中设置Timer
- NSRunLoopCommonModes:是一组常用的模式集合,将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode
- NSConnectionReplyMode,用来监听处理网络请求NSConnection的事件
- NSModalPanelRunLoopMode,OS X的Modal面板事件
假如我有个timer要关联到这些模式上,一个个注册很麻烦,我可以用:
CFRunLoopAddCommonMode([[NSRunLoop currentRunLoop] getCFRunLoop],(__bridge CFStringRef) UITrackingRunLoopMode);
注意:Run Loop 运行时只能以一种固定的模式运行,如果我们需要它切换模式,只有停掉它,再重新开其它,Run Loop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。
最早接触到的RunLoop用法如下:
AFN2.x
+ (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;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
[runloop addPort:[NSMachPort port] forMode: NSDefaultRunLoopMode];
[runloop run];
runloop开始跑起来,但是要注意,这种runloop,只有一种方式能停止。
[NSRunloop currentRunloop] removePort:forMode:]
在AFN3.x中,没有了常驻线程,都是用的run,结束的时候stop。
需要开启的时候:
CFRunLoopRun();
终止的时候:
CFRunloopStop(CFRunLoopGetCurrent());
操作RunLoop
Foundation层和Core Foundation层都有相应的接口可以操作Run Loop:Foundation层对应的是NSRunLoop,Core Foundation层对应的是CFRunLoopRef;
NSRunLoop相关方法:
// 运行NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制,属于无条件运行,不建议使用会导致Run Loop永久性的在NSDefaultRunLoopMode模式,即使用CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,除非能移除这个runloop上的所有事件源,包括定时器和source时间,不然这个子线程就无法停止,只能永久运行下去
- (void)run;
// 运行NSRunLoop:参数为时间期限,运行模式为默认的NSDefaultRunLoopMode模式,CFRunLoopStop(runloopRef), 也无法停止Run Loop的运行
- (void)runUntilDate:(NSDate *)limitDate;
// 运行NSRunLoop:参数为运行模式、时间期限,返回值为YES表示处理事件后返回的,NO表示是超时或者停止运行导致返回的,这种运行方式是可以被CFRunLoopStop(runloopRef)所停止的
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;
runUntilDate:方法可以如下方式使用,Run Loop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行Run Loop
while(!hasDone) {//hasDone使我们自己定义的
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 10]];
NSLog(@"exiting runloop, ......");
}
注意:消息是一个 非timer的事件,所有runloop处理完就退出
CFRunLoopRef相关方法:**
// 运行CFRunLoopRef,CFRunLoopStop接口调用停止这个RunLoop,或者RunLoop的所有事件源被删除
void CFRunLoopRun();
// 运行CFRunLoopRef:参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
// 停止运行CFRunLoop
void CFRunLoopStop(CFRunLoopRef rl);
// 唤醒CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);
停止RunLoop的方法
- 移除掉runloop种的所有事件源(timer和source)。
- 设置一个超时时间。
- 只要CFRunLoop运行起来就可以用:void CFRunLoopStop(CFRunLoopRef rl); 去停止, NSRunLoop的runMode:beforeDate方法也能使用void CFRunLoopStop(CFRunLoopRef rl); 停止,因为它是基于SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled)的封装。
NSRunLoop是线程不安全的,而CFRunLoop是线程安全的
用第二个参数控制,runloop的开始于停止
while(!cancel) {
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1, YES);//每次runloop只运行1秒就停止YES标识当有非timer事件进来也会立即开始下一次runloop,当然每次进来我们都可以去修改Mode的值,这样我们呢可以让runloop每次都运行在不同的模式下
}
输入源
线程间的通信(不仅限于通信,几乎所有iOS事件都是如此),实际上是各种输入源,触发runloop去处理对应的事件
RunLoopObserver与AutoreleasePool的关系
UIKit 通过 RunLoopObserver 在 RunLoop 两次 Sleep 间对 Autorelease Pool 进行 Pop 和 Push 将这次 Loop 中产生的 Autorelease 对象释放。
RunLoop的挂起与唤醒
指定用于唤醒的 mach_port 端口 调用 mach_msg 监听唤醒端口,被唤醒前系统内核将这个线程挂起,停留在mach_msg_trap状态。 由另一个线程向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续工作。
基于端口的输入源
实践
线程保活
/**
保活
*/
- (void)keepAlive {
__block NSThread *thread;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"当前线程开始");
thread = [NSThread currentThread];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"当前线程结束");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self performSelector:@selector(test) onThread:thread withObject:self waitUntilDone:YES];
});
}
- (void)test {
NSLog(@"我还活着呢");
}
给Timer指定RunLoop
/**
timer
*/
- (void)startTimer {
count = 10;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(exeTimer:) userInfo:@"timerTest" repeats:YES];
//[timer fire];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
CFRunLoopRunResult result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, MAXFLOAT, NO);
//kCFRunLoopRunFinished = 1, //Run Loop结束,没有Timer或者其他Input Source
//kCFRunLoopRunStopped = 2, //Run Loop被停止,使用CFRunLoopStop停止Run Loop
//kCFRunLoopRunTimedOut = 3, //Run Loop超时
//kCFRunLoopRunHandledSource = 4 ////Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行
switch (result) {
case kCFRunLoopRunFinished:
NSLog(@"kCFRunLoopRunFinished");
break;
case kCFRunLoopRunStopped:
NSLog(@"kCFRunLoopRunStopped");
case kCFRunLoopRunTimedOut:
NSLog(@"kCFRunLoopRunTimedOut");
case kCFRunLoopRunHandledSource:
NSLog(@"kCFRunLoopRunHandledSource");
default:
break;
}
});
}
- (void)exeTimer:(NSTimer *)timer{
NSLog(@"第%d次调用",count);
count--;
// if (count == 8) {//验证停止
// CFRunLoopRef runloop = CFRunLoopGetCurrent();
// CFRunLoopStop(runloop);
// }
if (count == 0) {//验证正常结束,timer停止,也就意味着runloop没有事件源
[timer invalidate];
timer = nil;
NSLog(@"定时器结束");
}
}
基于NSPort的线程通讯
/**
基于NSPort的线程通讯
*/
- (void)sendMessage {
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
threadPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程开始");
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"线程结束");
});
NSString *s1 = @"message";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithObjects:mainPort,data,nil];
NSLog(@"发送消息");
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
#pragma mark - NSPortDelegate
- (void)handlePortMessage:(id)message {//此处需要改写为id类型
//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
}
基于Source线程通讯
@interface ViewController ()<NSMachPortDelegate>
{
CFRunLoopRef _runLoopRef;
CFRunLoopSourceRef _source;
CFRunLoopSourceContext _source_context;
}
@end
- (void)source {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"线程开始");
_runLoopRef = CFRunLoopGetCurrent();
//初始化_source_context。
//bzero(&_source_context, sizeof(_source_context));
//这里创建了一个基于事件的源,绑定了一个函数
_source_context.perform = fire;
//参数
_source_context.info = "你好";
//创建一个source
_source = CFRunLoopSourceCreate(NULL, 0, &_source_context);
//将source添加到当前RunLoop中去
CFRunLoopAddSource(_runLoopRef, _source, kCFRunLoopDefaultMode);
//开启runloop 第三个参数设置为YES,执行完一次事件后返回
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 111111, YES);
NSLog(@"线程结束");
});
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (CFRunLoopIsWaiting(_runLoopRef)) {
NSLog(@"RunLoop 正在等待事件输入");
//添加输入事件
CFRunLoopSourceSignal(_source);
//唤醒线程,线程唤醒后发现由事件需要处理,于是立即处理事件
CFRunLoopWakeUp(_runLoopRef);
}else {
NSLog(@"RunLoop 正在处理事件");
//添加输入事件,当前正在处理一个事件,当前事件处理完成后,立即处理当前新输入的事件
CFRunLoopSourceSignal(_source);
}
});
}
应用场景:
- 维护线程的生命周期,让线程不自动退出,isFinished为Yes时退出。
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (!self.isCancelled && !self.isFinished) {
@autoreleasepool {
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
}
}
- 指定时长执行一次
@autoreleasepool {
NSRunLoop * runLoop = [NSRunLoop currentRunLoop];
NSTimer * udpateTimer = [NSTimer timerWithTimeInterval:60
target:self
selector:@selector(onTimerFired:)
userInfo:nil
repeats:YES];
[runLoop addTimer:udpateTimer forMode:NSRunLoopCommonModes];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:60*60]];
}
未完待续:只把自己理解和实践过的写出来了,还只是冰山一角啊。
参考
CFRunLoopRef源码
大神的博客
源码解析
源码剖析
https://www.cnblogs.com/zy1987/p/4582466.html
网友评论