当你试图解决一个你不理解的问题时,复杂化就产生了。—— AndyBoothe
**RunLoop: **顾名思义也就是循环运行的意思。做iOS 的同学都会接触到这个概念,但是真正用上的却不是很多。在这里,我将结合以往的一些经验及实践来谈谈我对RunLoop的理解。
一、 为什么会存在RunLoop
官方RunLoop模型图我们都知道,oc是一种面向对象的语言,但是代码的执行终究还是面向过程的,也就是说会有始有终。而线程也是一样的,我们的线程从创建到运行再到销毁也是会存在一个生命周期的。在项目开发中,有时候会存在对持续异步任务的需求,那么我们就需要来维护特定线程的生命周期,这时就该轮到RunLoop上场了。说白了,RunLoop就是来保证你的线程以一种环形的结构运行下去,在需要的时候唤醒,不需要的时候让线程进入休眠状态,从而来减少对CPU的开销。
二、RunLoop与线程的关系
在我们的main.m文件里会有这样的一段代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
当我们的程序启动后,上面的代码就会被调用,主线程也就开始执行。大家一定注意到了,我们的主线程是一直存在的,所有的视图、控件的操作以及事件链的监听都是在主线程下进行的,直到APP退出。所以可以推测出,当主线程被创建时,必然存在一个RunLoop来维护它的生命周期,保证后面程序的运行。线程与RunLoop可以说是一种线性的关系(一对一),除主线程的RunLoop会被自动创建,并运行在默认模式外,子线程的RunLoop是需要我们手动来创建的。
三、认识RunLoop
NSRunLoop是Cocoa框架中的类,与之对应,在Core Fundation中是CFRunLoopRef类。这两者的区别是前者不是线程安全的,而后者是线程安全的。
这里我们先从CFRunLoopRef中来剖析一下RunLoop的结构。在CoreFoundation里面有关于RunLoop的5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
刚才也提高过线程与RunLoop是一一对应的关系,而在RunLoop里会存在若干个Mode,每个Mode下又会存在若干个Source、Timer、Observer(观察者)。
Run Loop Mode主要定义有以下几种:
NSDefaultRunLoopMode: 大多数工作中默认的运行方式。
NSConnectionReplyMode: 使用这个Mode去监听NSConnection对象的状态,我们很少需要自己使用这个Mode。
NSModalPanelRunLoopMode: 使用这个Mode在Model Panel情况下去区分事件(OS X开发中会遇到)。
UITrackingRunLoopMode: 使用这个Mode去跟踪来自用户交互的事件(比如UITableView上下滑动)。
GSEventReceiveRunLoopMode: 用来接受系统事件,内部的Run Loop Mode。
NSRunLoopCommonModes: 这是一个伪模式,其为一组run loop mode的集合。
每一次运行自己的Run Loop时,都需要显示或者隐示的指定其运行于哪一种Mode。Run Loop运行时只能以一种固定的Mode运行,并监控这个Mode下添加的Timer source和Input source。如果这个Mode下没有添加事件源,Run Loop会立刻返回。
Run Loop从两个不同的事件源中接收消息:
Input source用来投递异步消息,通常消息来自另外的线程或者程序。在接收到消息并调用程序指定方法时,线程中对应的NSRunLoop对象会通过执行runUntilDate:方法来退出。
Timer source用来投递timer事件(Schedule或者Repeat)中的同步消息。在处理消息时,并不会退出Run Loop。Run Loop还有一个观察者Observer的概念,可以往Run Loop中加入自己的观察者以便监控Run Loop的运行过程。
Input source有两个不同的种类: Port-Based Sources 和 Custom Input Sources:Port-Based Sources由内核自动发送,Custom Input Sources需要从其他线程手动发送。
Cocoa框架为我们定义了一些Custom Input Sources,允许我们在线程中执行一系列selector方法:
1.在主线程的Run Loop下执行指定的 @selector 方法
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
2.在当前线程的Run Loop下执行指定的 @selector 方法
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
3.在当前线程的Run Loop下延迟加载指定的 @selector 方法
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
4.取消当前线程的调用
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
以下是在CFRunLoopRef下添加Sources和Observer的方法:
- (void)runDefaultLoop {
CFRunLoopSourceContext context = {0, (__bridge void *)(URLConnection), NULL, NULL, NULL, NULL, NULL, ScheduleCallBack, CancelCallBack, PerformCallBack};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
while (KRunAlways) {
@autoreleasepool {
CFRunLoopRun();
}
}
}
void ScheduleCallBack(void *info, CFRunLoopRef rl, CFRunLoopMode mode)
{
}
void CancelCallBack(void *info, CFRunLoopRef rl, CFRunLoopMode mode)
{
}
void PerformCallBack(void *info)
{
}
四、RunLoop的使用
1.获取当前线程的RunLoop:有则获取,无则创建
+ (NSRunLoop *)currentRunLoop;
2.获取主线程的RunLoop
+ (NSRunLoop *)mainRunLoop ;
3.获取RunLoop的CFRunLoopRef对象
- (CFRunLoopRef)getCFRunLoop;
4.将定时器添加到runloop中
- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;
5.添加输入源端口到runloop中,NSPort对象可以理解为详细的载体,会传递消息与其代理。
- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;
6.将某个输入源端口移除
- (void)removePort:(NSPort *)aPort forMode:(NSString *)mode;
7.开始运行
- (void)run;
8.在某个期限前运行
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
五、RunLoop的应用
CFRunLoopRef的作用主要还是用在对于消息的监听上面,所以这里主要讲的是关于NSRunLoop的应用场景。
1.创建一个与APP生命周期相同的子线程(不太推荐)
- (id)init{
if (self = [super init]) {
mdapThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
mdapThread_.name = @"MdapThread";
isThreadNeedRun = YES;
conditionLock_ = [[NSConditionLock alloc] init];
[mdapThread_ start];
}
return self;
}
- (void)run{
// 为runloop 加入输入源
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
}
2.维护线程的生命周期,让线程不主动退出
- (id)init{
if (self = [super init]) {
mdapThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
mdapThread_.name = @"MdapThread";
isThreadNeedRun = YES;
conditionLock_ = [[NSConditionLock alloc] init];
[mdapThread_ start];
}
return self;
}
- (void)run{
// 为runloop 加入输入源
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
while (isThreadNeedRun) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}
**注意:在这里如果输入源不存在可能会造成线程的循环空转,造成CPU的浪费**
3.阻塞线程
(void)handleRunLoopThreadButtonTouchUpInside
{
NSLog(@"Enter handleRunLoopThreadButtonTouchUpInside");
self.runLoopThreadDidFinishFlag = NO;
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
[runLoopThread start];
//在这里如果self.runLoopThreadDidFinishFlag不为YES,则 NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside”);代码是不会执行的,我们就可以在handleRunLoopThreadTask方法里执行我们想要的操作了
while (!self.runLoopThreadDidFinishFlag) {
NSLog(@"Begin RunLoop");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"End RunLoop");
}
NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
}
4.在一定时间内监听某种事件,或执行某种任务的线程
NSTimer*udpateTimer=[NSTimer timerWithTimeInterval:30
target:self
selector:@selector(onTimerFired:)userInfo:nil
repeats:YES];
[NSRunLoopcurrentRunLoop] addTimer:udpateTimerforMode:NSRunLoopCommonModes];
注意:NSTimer的初始化有两种scheduledTimerWithTimeInterval和timerWithTimeInterval。在使用scheduledTimerWithTimeInterval进行初始化时,它是会被自动的添加到NSDefaultRunLoopMode这种模式下的。而使用timerWithTimeInterval初始化时则需要我们来手动的添加Mode。那么为什么会有这两种情况呢?不知道大家有没有遇到过这样的情况,就是当NSTimer运行在NSDefaultRunLoopMode模式下,如果我们在滑动页面如UIScrollView或UITableView时,定时器的方法是不执行的。这是因为苹果公司为了增加用户的体验感,在用户进行滑动操作时,会将主线程的RunLoop模式切换到UITrackingRunLoopMode下,UITrackingRunLoopMode的优先级高于NSDefaultRunLoopMode,所以定时器方法会延缓执行。为了避免这种错误的发生,在我们初始化NSTimer时,可以选择将其放入UITrackingRunLoopMode或NSRunLoopCommonModes模式下。
5.避免APP的崩溃
我们可以在自定义的错误捕捉方法里,添加这样一段代码来处理app崩溃事件,可以有效的阻止app奔溃。(关于具体的实现方法,有兴趣的同学可以看看我在简书里的另一篇关于崩溃捕获的博客)
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!_isDismisssed) {
for (NSString *mode in (NSArray *)allModes) {
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
CFRelease(allModes);
另外附送上CFRunLoop的源码地址,有兴趣的同学可以自行下载。
网友评论