深入理解RunLoop

当你试图解决一个你不理解的问题时,复杂化就产生了。—— 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(观察者)。

RunLoop的相关类关系图

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的源码地址,有兴趣的同学可以自行下载。

CFRunLoop的源码地址

你可能感兴趣的:(深入理解RunLoop)