美文网首页
Run Loops概念

Run Loops概念

作者: 大鹏鸟 | 来源:发表于2017-12-22 15:51 被阅读15次

    Run Loop,感觉神一般的存在,这么久了,也没搞懂个所以然,希望这次能够了解些皮毛!!!

    这里是官方地址

    Run loop是线程运行的基础,run loop的目的是为了保证让你空闲的线程忙碌起来,或者在没有事情可做的时候,让线程保持睡眠。
    Run loop不是完全的原子的。需要我们在合适的时机去启动线程和处理事件。每一个线程都有一个run loop对象,但是只有主线程的run loop是启动的,其他线程的都需要手动启动。

    一、解剖Run Loop

    run loop和它的字面意思很像。它表示一个循环,让您的线程进入运行事件的处理程序和响应传入的事件。您的代码提供了控制语句去实现真实的run loop,换句话说,您的代码里有类似于while或者for循环之类的去驱动run loop。

    run loop的事件源有两种:

    • 输入源,分发异步事件,事件来源于其他线程或者其他程序
    • 定时器,分发同步事件,事件由预定时间或者循环重复的事件触发

    下图是runloop的结构图:

    runloop.jpg
    在该图中,输入源分发异步事件到相应的(Port对应handlePort、Custom对应customSrc:等等)事件处理程序里并触发runUntilDate:方法去结束runloop;定时器分发事件到相应的事件处理程序里但是不会触发runUntilDate:去结束runloop(???)

    除了处理输入的事件,runloop还会在runloop的不同时间段发送通知,我们可以注册相关的runloop通知来做一些额外的事情。您应该使用Core Foundation去注册runloop的观察者(NSNotification是Foundation下的)。

    在接下来的章节中将会对runloop做进一步的说明。

    1、Run Loop的Modes

    runloop的mode是输入源和定时器源的集合和runloop的观察者接收通知的集合。每次您运行runloop,您都会指定一个mode(无论是明确地还是含蓄地)。在runloop运行的过程中,只有事件源模式和该runloop指定模式匹配的事件源才会被监控和分发它的事件。(相似的,只有观察者的模式和runloop的模式匹配的才能接收到通知)模式不匹配的只能处在等待状态。


    mode.png

    2、输入源

    输入源异步分发事件到你的线程。事件来源取决于输入源的类型。基于端口输入源监控你的程序的Mach端口。自定义输入源监控自定义的事件。对于runloop来说,它不关心你的输入源是哪种类型,系统默认都实现了这两种输入源的事件的处理程序。这两个事件的唯一的不同之处是它们如何发出信号的。基于端口输入源由其内核表明其类型,自定义事件必须手动表明其类型。
    当你创建了一个输入源,你要做好准备,让它在一个或者多个模式的runloop下正常运行。runloop一直在监控着输入源的mode,一旦发生了模式变化,就会发生事件挂起,一直到mode匹配了才会再次触发。

    (1) 基于端口的源

    苹果Cocoa和Core Foundation框架提供了和端口相关的接口去创建端口对象。
    比如,在Cocoa框架里,你不能直接去创建一个输入源的事件,你只能使用NSPort来简单的创建一个端口对象并添加到runloop中。该端口对象负责创建和配置输入源需要的东西。
    在Core Foundation框架下,你必须手动创建端口和runloop。在这两种情况下,你需要使用相应的端口方法(CFMachPortRef, CFMessagePortRef, 和 CFSocketRef)去创建合适的端口对象。

    具体的使用在后面!

    (2) 自定义输入源

    你需要使用类CFRunLoopSourceRef去创建一个自定义输入源,可以使用一些回调函数去处理事件,因为在不同的时间段,会调用这些回调函数去注册事件源、事件和从runloop移除该输入源。
    除了定义接收到自定义事件后的一些行为,你必须定义事件的分发机制。输入源的该部分运行在一个单独的线程上,负责为输入源提供它的数据,并在数据准备好处理时发出信号。事件的交付机制由你决定,但是不要太过于复杂。

    具体的使用在后面!

    (3) Cocoa可执行选择源(Selector Sources)

    除了基于端口的输入源,Cocoa定义了一个自定义的输入源,允许在任何线程上运行。和基于端口的输入源一样,执行 selector请求是在目标线程上序列化的,从而减轻了在一个线程上运行多个方法时可能出现的许多同步问题。和基于端口的输入源不同的是,执行 selector完毕后会自动从runloop移除。
    当在其他线程上执行selector时,目标线程必须有一个可活跃的runloop。对于你创建的线程,在代码里需要明确的去启动runloop。因为在主线程里是自动启动的,所以在程序执行完applicationDidFinishLaunching:方法后,你就可以在主线程上处理事情。
    runloop在每次循环时都会处理所有的在队列中的selector,而不是只处理一次。
    下面是几个自带的selector:

    • performSelectorOnMainThread:withObject:waitUntilDone:和performSelectorOnMainThread:withObject:waitUntilDone:modes:在程序的主线程上执行,如果错过了当前一次runloop循环,会在下次循环中被立即执行;其中waitUntilDone的参数表示添加到runloop中时是否阻塞当前线程(主线程),yes表示阻塞,no表示不阻塞。过程如下:
    - (void)viewDidLoad {
        [super viewDidLoad];
        self.edgesForExtendedLayout = UIRectEdgeNone;
        self.view.backgroundColor = [UIColor whiteColor];
        self.title = @"Run Loop";
        [self executePerfomSelectorOnMainThread];
    }
    
    - (void)executePerfomSelectorOnMainThread {
        [self performSelectorOnMainThreadWithWait];
        [self performSelectorOnMainThreadWithNoWait];
    }
    
    - (void)performSelectorOnMainThreadWithWait {
        [self performSelectorOnMainThread:@selector(logEventForWait:) withObject:@"1" waitUntilDone:YES];
        NSLog(@"I am here for Wait");
    }
    
    - (void)performSelectorOnMainThreadWithNoWait {
        [self performSelectorOnMainThread:@selector(logEventForNoWait:) withObject:@"1" waitUntilDone:NO];
        NSLog(@"I am here for no Wait");
    }
    
    - (void)logEventForWait:(NSString *)infoStr {
        NSLog(@"%@", infoStr);
        sleep(2);
    }
    
    - (void)logEventForNoWait:(NSString *)infoStr {
        NSLog(@"%@", infoStr);
        sleep(2);
    }
    
    结果如下: until.png
    这点和NSNotification很像
    • performSelector:onThread:withObject:waitUntilDone:和performSelector:onThread:withObject:waitUntilDone:modes:在任何自定义的线程上执行

    • performSelector:withObject:afterDelay:和performSelector:withObject:afterDelay:inModes:在当前线程上执行,需要等到下一个runloop的循环,它的执行是阻塞当前线程执行的,但是添加是不阻塞的。另外,它的延迟运行时间也不能做到按时出发,只能尽可能以最靠近设置的延迟触发。这里会启动一个定时器。
      如下:

    - (void)viewDidAppear:(BOOL)animated {
        [super viewDidAppear:animated];
        [self executePerfomSelectorOnCurrentThread];
        [self executePerfomSelectorOnMainThread];
    }
    - (void)executePerfomSelectorOnCurrentThread {
        
        [self performSelector:@selector(logEventForPerfomSelectorOnCurrentThreadForSleep:) withObject:@"1-1" afterDelay:1];
        NSLog(@"after delay 1-1");
        [self performSelector:@selector(logEventForPerfomSelectorOnCurrentThread:) withObject:@"1" afterDelay:1];
        [self performSelector:@selector(logEventForPerfomSelectorOnCurrentThread:) withObject:@"0" afterDelay:0];
    
    }
    - (void)logEventForPerfomSelectorOnCurrentThread:(NSString *)infoStr {
        NSLog(@"%s--%@--info:%@",__func__,[NSThread currentThread],infoStr);
    }
    - (void)logEventForPerfomSelectorOnCurrentThreadForSleep:(NSString *)infoStr {
        NSLog(@"before:%s--%@--info:%@",__func__,[NSThread currentThread],infoStr);
        sleep(2);
        NSLog(@"after:%s--%@--info:%@",__func__,[NSThread currentThread],infoStr);
    }
    
    结果如下: image.png
    1、可以发现,上面在添加的时候,不会阻塞当前线程的执行
    2、它的运行不是准时的,比如上面的两个都是延迟1秒,但是第一个(1-1)没有执行结束,那么另一个就不会进行
    3、这里还调用了方法performSelectorOnMainThread:withObject:waitUntilDone:和performSelectorOnMainThread:withObject:waitUntilDone:modes:,该方法的优先级要高很多,虽然它是后面添加的,但是它最先执行
    4、如果添加一个1秒的睡眠,又会不一样
        
        [self performSelector:@selector(logEventForPerfomSelectorOnCurrentThreadForSleep:) withObject:@"1-1" afterDelay:1];
        NSLog(@"after delay 1-1");
        
        [self performSelector:@selector(logEventForPerfomSelectorOnCurrentThread:) withObject:@"1" afterDelay:1];
        sleep(1);
        [self performSelector:@selector(logEventForPerfomSelectorOnCurrentThread:) withObject:@"0" afterDelay:0];
    }
    
    • cancelPreviousPerformRequestsWithTarget:和cancelPreviousPerformRequestsWithTarget:selector:object:用来暂停上面的带有延迟的方法,但是只移除当前runloop中的,不回移除其他runloop中的。

    3、时间源(定时器)

    时间源会在你预设的未来的某个时间同步的把事件分发到你的线程上。定时器是通知线程自己去做一些事情的很好的例子。

    尽管它是基于定时器的通知,但是它的并不是驱动事件运行的机制。时间源的运行机制和事件源的机制是一样的:需要模式mode匹配;如果当前runloop正在执行任务,它需要等待。

    你可以一次或者多次注册时间源去处理事情。一个重复的计时器会自动根据预定的发射时间自动调整,也就是说真正的触发时间并不是你设置的时间。

    4、Run Loop的观察者

    和在合适的时机触发异步或者同步事件发生的输入源相比,runloop的观察者能在一些特定的时机被触发。你可以利用runloop的这个特性去处理事件或者在线程睡眠前作一些准备工作。可以将以下的事件通过runloop的观察者和实际情况联系起来:

    • runloop的入口
    • runloop将处理计时器(时间源)timer
    • runloop将处理输入源
    • runloop将进入睡眠
    • runloop将被唤醒
    • runloop退出

    你可以使用Core Foundation添加runloop的观察者。你可以通过创建CFRunLoopObserverRef的实例来创建和添加runloop的观察者。它会对你自定义的回调函数或者它感兴趣的活动进行跟踪。

    和时间源(定时器)相似,runloop的观察者可以使用一次或者重复使用多次,只观察一次的观察者会在它运行完后被移除,但是可重复的runloop观察者不会被移除。当然runloop的观察者可被执行几次,是由你决定的。

    5、Run Loop事件执行的顺序

    每次运行线程,runloop执行一些还在等待处理的事件和给观察者发送通知。它所做的事情的执行顺序如下所示:

    1、 告诉观察者,要开始进入runloop了
    2、 告诉观察者,所有的时间源(定时器)都准备好了处理到来的事件
    3、 告诉观察者,所有的输入源除了基于端口的输入源都准备好了去处理到来的事件
    4、 执行任何已经做好准备的输入源(不包括基于端口的输入源)
    5、 如果有基于端口的输入源已经准备好了并在等待触发,立马执行,并进入第9步
    6、告诉观察者线程将要睡眠了
    7、让线程进入睡眠,一直到出现下面的几种情况里的一种,线程才被重新唤醒:
        a、一个基于端口的输入源事件到达了
        b、时间源(定时器)启动
        c、为runloop设置的超时时间过期了
        d、明确的唤醒runloop
    8、告诉观察者,线程被唤醒了,但是runloop没启动
    9、处理等待处理的事件:
        a、如果定时器启动了,则处理时间源事件并重启runloop,进入步骤2
        b、如果有输入源被触发,则分发事件
        c、如果runloop被明确唤醒并且没有超时,进入步骤2
    10、告诉观察者,runloop退出了
    

    因为通知是在定时器和输入源被实际触发前发送的,所以在通知的发送和事件的真实执行之间会有一个时间间隙。如果在这些事件间的时间很重要,可以使用sleep通知和awake通知去让彼此关联起来。

    因为定时器和其他的周期性的事件是在运行runloop的时候分发的,因此如果绕过该runloop就会打乱那些事件的分发。

    一个runloop时可以通过runloop的对象被直接唤醒的,其他事件也可能导致runloop被唤醒。(如上面的第7步)

    二、什么时候该使用Run Loop

    一般在你自己创建了一个线程的时候需要显式的启动runloop,因为程序中的主线程是默认启动的,所以不需要也不希望你去显示的启动。
    对于自己创建的线程,你需要自己决定是否需要runloop,如果需要,那么创建它并启动它。然而并不是所有你创建的线程的runloop都需要启动。例如,如果您使用线程来执行一些长时间运行和预定义的任务,则不需要启动runloop。runloop时为了那些需要和线程有很多交互的场景,比如下面的几种情况:

    • 使用端口或者自定义输入源和其他线程交互
    • 在线程上使用定时器
    • 使用任何类似于performSelector…方法的场景
    • 保持线程执行定期任务

    如果你决定去使用runloop,那么配置和设置很简单。和所有的线程一样,你应该有一个计划在合适的时机去退出线程,退出一个线程要比强制终止一个线程更好。

    针对Run Loop的使用在下一篇中

    相关文章

      网友评论

          本文标题:Run Loops概念

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