美文网首页
深入理解RunLoop

深入理解RunLoop

作者: 迷失之刃 | 来源:发表于2017-02-21 14:48 被阅读0次

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

          本文链接:https://www.haomeiwen.com/subject/nhcuwttx.html