美文网首页
iOS--RunLoop

iOS--RunLoop

作者: mayuee | 来源:发表于2020-08-05 17:02 被阅读0次

    run loop官方文档
    RunLoop是一个事件处理循环,用于管理事件和消息。RunLoop的目的是让线程在有事件和消息需要处理时立刻被唤醒来处理事件和消息,此时线程由内核态转换为用户态;在没有事件和消息需要处理时让线程休眠来避免资源占用,线程由用户态切换为内核态。

    image.png image.png

    应用程序不需要显式地创建RunLoop对象;线程和RunLoop是一一对应的关系,每个线程(包括应用程序的主线程)都有一个关联的RunLoop对象。子线程需要显式地运行其RunLoop。作为应用程序启动过程的一部分,应用程序框架在主线程上自动设置并运行RunLoop。具体到工程里则是在main函数里调用的UIApplicationMain()里面启动了一个RunLoop。

    image.png
    image.png

    Apple提供了NSRunLoop和CFRunLoop,NSRunLoop是CFRunLoop的封装,提供了面向对象的API。有关run loop objects的其他信息,请参见NSRunLoop Class ReferenceCFRunLoop Reference以及CFRunLoop源码

    Run Loop 剖析

    image.png

    RunLoop是线程进入并用于run事件处理程序以响应传入事件的loop。程序代码提供了用于实现RunLoop的实际loop部分的控制语句,换句话说,你的代码提供了驱动RunLoop的while或for循环。在你的loop里,使用RunLoop object来“run”接收事件并调用已安装的处理程序的事件处理代码。

    RunLoop从两种不同类型的源接收事件。Input源传递异步事件,通常是来自另一个线程或来自不同应用程序的消息。Timer源提供同步事件,在预定时间或重复间隔发生。两种类型的源在事件到达时都使用特定的处理程序例程来处理事件。

    图3-1显示了RunLoop和各种源的概念结构。input源将异步事件传递给相应的处理程序,并导致runUntilDate:方法(在线程的关联NSRunLoop对象上调用)退出。Timer源向其处理程序传递事件,但不会导致运行循环退出。

    图3-1 run loop的结构及其来源

    除了处理input源, run loops还生成有关 run loops行为的通知。注册的 run-loop observers可以接收这些通知对线程执行额外的处理。使用Core Foundation在线程上安装运行run-loop observers。

    下面各节提供有关run loop组件及其运行模式的详细信息。它们还描述了在处理事件期间的不同时间生成的通知。

    Run Loop模式

    image.png
    image.png

    Run Loop模式是要监视的input源和Timer源的集合,以及要通知的run-loop observers的集合。每次运行run loop时,都要指定(显式或隐式)要运行的特定“模式”。在这个run loop的传递过程中,只监视与该模式关联的源,并允许它们传递事件。(类似地,只有与该模式相关联的observers才会收到run loop进度的通知。)与其他模式关联的源将保留任何新事件,直到后续事件以适当的模式通过循环。

    在代码中,通过名称标识模式。Cocoa和Core Foundation都定义了一个默认模式和几种常用模式,以及在代码中指定这些模式的字符串。只需为模式名指定一个自定义字符串,就可以定义自定义模式。尽管为自定义模式指定的名称是随意的,但这些模式的内容不是随意的。必须确保将一个或多个input源、Timer源或run-loop observers添加到你创建的任何模式中是有用的。

    在run loop的特定传递过程中,可以使用模式从不需要的源中筛选出事件。大多数情况下,你希望以系统定义的“默认”模式运行run loop。然而,模态面板可能以“模态”模式运行。在这种模式下,只有与模式面板相关的源才会向线程传递事件。对于子线程,可以使用自定义模式来阻止低优先级的源在时间关键型操作期间传递事件。

    注:模式的区别取决于事件的来源,而不是事件的类型。例如,你不会使用模式来只匹配鼠标按下事件或键盘事件。可以使用模式来监听不同的端口集,暂时挂起计时器,或者更改当前正在监视的源和run loop observers。

    表3-1列出了Cocoa和Core Foundation定义的标准模式,以及何时使用该模式的说明。name列列出了用于在代码中指定模式的实际常量。
    |
    表 3-1 预定义的运行循环模式

    1. Default
    NSDefaultRunLoopMode(Cocoa)
    kCFRunLoopDefaultMode (Core Foundation)
    默认模式,大多数操作使用的模式。大多数时候,应该使用这种模式来启动 run loop 并配置输入源。

    2. Connection
    NSConnectionReplyMode(Cocoa)
    Cocoa将此模式与 NSConnection 对象结合使用来监视响应。很少需要自己使用这种模式。

    3. Modal
    NSModalPanelRunLoopMode(Cocoa)
    Cocoa使用此模式来识别用于模式面板的事件预期。

    4. Event tracking
    NSEventTrackingRunLoopMode (Cocoa)
    Cocoa使用这种模式来限制在鼠标拖动循环和其他类型的用户界面跟踪循环期间传入事件。

    5. Common modes
    NSRunLoopCommonModes(Cocoa)
    kCFRunLoopCommonModes (Core Foundation)
    这是一组可配置的常用模式,不是一种真正的模式,只是一种标记。将 input源与此模式关联也会将其与组中的每个模式相关联。对于Cocoa应用程序,默认情况下,此集合包括default, modal和 event tracking 模式。Core Foundation最初只包括default模式。可以使用CFRunLoopAddCommonMode 函数向集合添加自定义模式。

    Input Sources 输入源

    Input源以异步方式向线程传递事件。event源取决于input源的类型,通常是两种类型之一。基于端口的input源监视应用程序的Mach端口。自定义input源监视event的自定义源。就run loop而言,input源是基于端口还是自定义的并不重要。系统通常实现两种类型的input源,可以按原样使用。这两个信号源之间的唯一区别是它们是如何发出信号的。基于端口的源由内核自动发出信号,自定义源必须从另一个线程手动发出信号。

    创建输入源时,将其指定给run loop的一个或多个模式。模式影响在任何给定时刻给定的受监控的输入源。大多数情况下,可以在默认模式下运行run loop,但也可以指定自定义模式。如果输入源不在当前监视的模式下,它生成的任何事件都将被保留,直到run loop以正确的模式运行。

    基于端口的源

    Cocoa和Core Foundation为使用端口相关的对象和函数创建基于端口的输入源提供了内置支持。例如,在Cocoa中,根本不必直接创建输入源。你只需创建一个port对象并使用NSPort的方法将这个port添加到run loop中。port对象处理所需输入源的创建和配置。

    在Core Foundation中,必须手动创建端口及其run loop源。在这两种情况下,都可以使用与端口不透明类型 (CFMachPortRef, CFMessagePortRef,或 CFSocketRef)关联的函数来创建适当的对象。

    有关如何设置和配置自定义基于端口的源的示例,请参阅 Configuring a Port-Based Input Source

    自定义输入源

    要创建自定义输入源,必须使用与Core Foundation中的 CFRunLoopSourceRef不透明类型关联的函数。你可以使用几个回调函数配置自定义输入源。Core Foundation在不同的点调用这些函数来配置输入源,处理任何传入事件,并在源从run loop中移除时将其销毁。

    除了定义事件到达时自定义源的行为外,还必须定义事件传递机制。源的这一部分运行在一个单独的线程上,负责向输入源提供其数据,并在数据准备好进行处理时发出信号。事件传递机制由你决定,但不必过于复杂。

    有关如何创建自定义输入源的示例,请参见 Defining a Custom Input Source。 有关自定义输入源的参考信息,请参见 CFRunLoopSource Reference

    执行Selector源

    除了基于端口的源之外,Cocoa还定义了一个自定义输入源,允许你在任何线程上执行selector。与基于端口的源一样,perform selector请求在目标线程上序列化,从而减轻了在一个线程上运行多个方法时可能出现的许多同步问题。与基于端口的源不同,perform selector source在执行其selector后将自身从运行循环中移除。

    在另一个线程上执行selector时,目标线程必须具有活动的run loop。对于你创建的线程,这意味着等待,直到你的代码显式启动run loop。但是,由于主线程启动了自己的run loop,因此只要应用程序调用应用application delegate的applicationdiffinishlaunching:方法,就可以开始在该线程上发起调用。run loop每次通过循环处理所有排队的perform selector调用,而不是在每次循环迭代期间处理一个。

    表3-2列出了在NSObject上定义的可用于在其他线程上执行 selectors的方法。因为这些方法是在NSObject上声明的,所以你可以从任何可以访问Objective-C对象的线程中使用它们,包括POSIX线程。这些方法实际上并不创建新线程来执行selector。

    表3-2在其他线程上执行selectors
    |
    performSelectorOnMainThread:withObject:waitUntilDone:
    performSelectorOnMainThread:withObject:waitUntilDone:modes:

    在应用程序的主线程的下一个 run loop cycle中对该线程执行指定的selector。这些方法提供了阻塞当前线程直到selector被执行的选项。
    |
    performSelector:onThread:withObject:waitUntilDone:
    performSelector:onThread:withObject:waitUntilDone:modes:

    在任何有 NSThread对象的线程上执行指定的selector。这些方法提供了阻塞当前线程直到selector被执行的选项。
    |
    performSelector:withObject:afterDelay:
    performSelector:withObject:afterDelay:inModes:

    在下一个run loop cycle以及在可选延迟期之后,在当前线程执行指定的selector。因为等到下一个run loop cycle才执行selector,所以这些方法为当前执行的代码提供一个自动的小延迟。多个排队的selectors按其排队顺序依次执行。
    |
    cancelPreviousPerformRequestsWithTarget:
    cancelPreviousPerformRequestsWithTarget:selector:object:

    允许使用performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes:方法取消发送到当前线程的消息。
    |
    有关这些方法的详细信息,请参见NSObject Class Reference

    Timer Sources 定时器源

    定时器源在将来的预设时间将事件同步传递到线程。定时器是线程通知自身执行某项操作的一种方式。例如,搜索框可以使用定时器在用户连续输入经过一定时间后启动自动搜索。使用这个延迟时间,让用户有机会在开始搜索之前尽可能多的键入所期望的搜索字符串。

    尽管定时器生成基于时间的通知,但它不是一种实时机制。与输入源一样,定时器与run loop的特定模式相关联。如果定时器未处于run loop当前监视的模式,则在以定时器支持的某一模式来运行run loop之后,定时器才会触发。类似地,如果定时器在run loop正在执行处理程序例程程的过程中触发,则定时器将等到下一次通过run loop调用其(定时器)处理程序例程。如果run loop根本没有运行,定时器就不会触发。

    你可以将定时器配置为仅生成一次事件或重复生成事件。重复定时器根据预定的触发时间自动重新调度自己,而不是实际触发时间。例如,如果一个定时器被安排在某个特定时间触发,并且此后每隔5秒触发一次,那么即使实际触发时间被延迟,计划的触发时间也将始终落在原来的5秒时间间隔上。如果触发时间延迟太久,以致错过了一个或多个预定的触发时间,则对于错过的时间段,定时器只触发一次。在为错过的时间段触发后,定时器将重新安排为下一个预定的触发时间。

    有关配置计时器源的详细信息,请参阅 Configuring Timer Sources。有关引用信息,请参见NSTimer Class ReferenceCFRunLoopTimer Reference

    Run Loop Observers - Run Loop观察者

    image.png

    与在发生适当的异步或同步事件时触发的源不同,Run Loop Observers在 run loop本身执行期间在特殊位置激发。你可以使用run loop observers 准备线程来处理给定的事件,或者在线程进入休眠状态之前对其进行准备。可以将运行run loop observers与run loop中的以下事件关联:

    • run loop的入口。
    • 当run loop即将处理定时器时。
    • 当run loop即将处理输入源时。
    • 当run loop即将进入休眠状态时。
    • 当run loop已唤醒时,但在它处理唤醒它的事件之前。
    • 从run loop中退出。

    可以使用Core Foundation向应用程序添加run loop observers。要创建run loop observers,需要创建 CFRunLoopObserverRef不透明类型的新实例。此类型跟踪自定义回调函数及其感兴趣的活动。

    与定时器类似,运行run loop observers可以使用一次或重复使用。一次性观察者在激发后从run loop中移除自己,而重复观察者保持连接。在创建观察者时可以指定是运行一次还是重复运行。

    有关如何创建run loop observers的示例,请参阅 Configuring the Run Loop。有关参考信息,请参阅CFRunLoopObserver Reference

    The Run Loop Sequence of Events -事件的运行循环序列

    每次运行它时,线程的run loop都会处理挂起的事件,并为任何附加的观察者生成通知。其执行顺序非常具体,如下所示:


    image.png
    1. 通知观察者 run loop已进入。
    2. 通知观察者任何准备就绪的定时器即将启动。
    3. 通知观察者任何非基于端口的输入源都将被触发。
    4. 启动任何准备好触发的非基于端口的输入源。
    5. 如果基于端口的输入源已准备就绪并等待触发,则立即处理该事件。转至步骤9。
    6. 通知观察者线程即将休眠。
    7. 将线程置于休眠状态,直到发生以下事件之一:
    • 基于端口的输入源的事件到达。
    • 计时器启动。
    • 为 run loop设置的超时值过期。
    • run loop被显式唤醒。
    1. 通知观察者线程唤醒。
    2. 处理挂起的事件。
    • 如果触发了用户定义的定时器,请处理定时器事件并重新启动循环。转到步骤2。
    • 如果输入源激发,则传递事件。
    • 如果 run loop已显式唤醒但尚未超时,请重新 run loop。转到步骤2。
    1. 通知观察者 run loop已退出。

    因为计时器和输入源的观察者通知是在这些事件实际发生之前传递的,所以通知的时间和实际事件的时间之间可能会有间隔。如果这些事件之间的时间安排很关键,那么可以使用sleep和awake-from-sleep通知来帮助你将实际事件之间的时间序列关联起来。

    因为计时器和其他周期性事件是在运行 run loop时传递的,因此绕过该循环会中断这些事件的传递。这种行为的典型示例是,每当你通过输入循环并从应用程序反复请求事件来实现鼠标跟踪例程时,都会发生这种行为。因为你的代码直接捕获事件,而不是让应用程序正常发送地这些事件,所以在鼠标追踪例程退出并将控制权传回应用程式之前,活动计时器将无法启动。

    可以使用run loop对象显式地唤醒run loop。其他事件也可能导致run loop被唤醒。例如,添加另一个非基于端口的输入源会唤醒运行循环,以便可以立即处理该输入源,而不是等待其他事件发生。

    什么时候会是用Run Loop?

    只有在为应用程序创建子线程时才需要显式运行Run Loop。应用程序主线程的Run Loop是基础设施的关键部分。因此,应用程序框架提供运行主应用程序循环并自动启动该循环的代码。iOS中UIApplication (或osx中的NSApplication)的run方法启动应用程序的主循环作为正常启动序列的一部分,不必显式调用这些例程。

    对于子线程,你需要确定是否需要 run loop,如果需要,请自行配置并启动它。你不需要在所有情况下都启动线程的run loop。例如,如果使用线程来执行一些长期运行的预定任务,你可能避免启动run loop。run loop用于希望与线程进行更多交互的情况。例如,如果计划执行以下任一操作,则需要启动run loop:

    • 使用端口或自定义输入源与其他线程通信。
    • 在线程上使用Timer。
    • 在Cocoa应用程序中使用任何performSelector…方法。
    • 保持线程循环执行周期性任务。

    如果选择使用 run loop,它的配置和设置很简单。不过,与所有线程编程一样,你应该有一个在适当情况下退出子线程的计划。通过让线程退出而干净地结束它总是比强制它终止要好。有关如何配置和退出run loop的信息,请参见 Using Run Loop Objects

    Using Run Loop Objects

    运行循环对象提供主接口,用于向运行循环添加输入源、计时器和 run-loop observers ,然后运行它。每个线程都有一个与之关联的 run loop object。在Cocoa中,此对象是NSRunLoop类的实例。在低级应用程序中,它是指向CFRunLoopRef不透明类型的指针。

    Getting a Run Loop Object -获取Run Loop对象

    要获取当前线程的运行循环,请使用以下方法之一:

    尽管它们不是免费的桥接类型,但可以在需要时从NSRunLoop对象获取CFRunLoopRef opaque类型。NSRunLoop类定义了一个getCFRunLoop方法,该方法返回一个可以传递给 Core Foundation例程的CFRunLoopRef类型。因为这两个对象引用同一个run loop,因此可以根据需要混合调用NSRunLoop对象和CFRunLoopRef opaque类型。

    Configuring the Run Loop配置运行循环

    在子线程上运行 Run Loop之前,必须至少向其添加一个输入源或定时器。如果 Run Loop没有任何要监视的源,则当你尝试运行它时,它会立即退出。有关如何向run loop添加源的示例,请参阅Configuring Run Loop Sources

    除了安装源代码之外,还可以安装run loop observers,并使用它们来检测run loop的不同执行阶段。要安装run loop observers,需要创建一个CFRunLoopObserverRef opaque类型,并使用 CFRunLoopAddObserver函数将其添加到run loop中。run loop observers必须使用Core Foundation创建,即使对于Cocoa应用程序也是如此。

    清单3-1显示了一个线程将run loop observer连接到其run loop的主线程。该示例的目的是展示如何创建run loop observer,因此代码只需设置一个run loop observer来监视所有run loop活动。基本处理程序例程(未显示)只是在处理定时器请求时记录run loop活动。

    清单3-1创建run loop observer

    - (void)threadMain
    {
        // The application uses garbage collection, so no autorelease pool is needed.
        NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
     
        // Create a run loop observer and attach it to the run loop.
        CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
        CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
     
        if (observer)
        {
            CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
            CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
        }
     
        // Create and schedule the timer.
        [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                    selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
     
        NSInteger    loopCount = 10;
        do
        {
            // Run the run loop 10 times to let the timer fire.
            [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
            loopCount--;
        }
        while (loopCount);
    }
    

    在为long-lived线程配置运行循环时,最好添加至少一个输入源来接收消息。虽然可以在只附加一个定时器的情况下进入run loop,但一旦定时器触发,它通常会失效,这将导致run loop退出。附加一个重复定时器可以使run loop在在长的时间内运行,但需要定期启动定时器以唤醒线程,这实际上是另一种轮询形式。相比之下,输入源在等待事件发生时,让线程一直处于休眠状态。

    Starting the Run Loop -启动运行循环

    只有应用程序中的子线程才需要启动run loop。run loop必须至少有一个要监视的输入源或定时器。如果没有连接,则run loop立即退出。

    有几种启动run loop的方法,包括:

    • 无条件地
    • 固定的时间限制
    • 在特定模式下

    无条件地进入run loop是最简单的选择,但也是最不可取的。无条件地运行run loop会将线程放入一个永久循环中,这使几乎无法控制run loop本身。你可以添加和删除输入源和定时器,但停止run loop的唯一方法是终止它。也无法在自定义模式下运行run loop。

    与其无条件运行run loop,不如使用超时值运行run loop。使用超时值时,run loop将一直运行,直到事件到达或分配的时间过期。如果事件到达,则将该事件分配给处理程序进行处理,然后run loop退出。然后,你的代码可以重新启动run loop来处理下一个事件。如果分配的时间过期,你可以简单地重新启动run loop或使用该时间来执行任何需要的housekeeping。

    除了超时值,还可以使用特定模式运行run loop 。模式和超时值不是互斥的,都可以在启动run loop时使用。模式限制向run loop 传递事件的源的类型,在Run Loop Modes中有更详细的描述。

    清单3-2显示了一个线程主入口例程的框架版本。本例的关键部分显示了run loop的基本结构。本质上,将输入源和定时器添加到run loop中,然后反复调用其中一个例程来启动run loop。每次run loop例程返回时,都要检查是否出现了任何可能需要退出线程的条件。该示例使用Core Foundation run loop例程,以便检查返回结果并确定run loop退出的原因。如果使用的是Cocoa,并且不需要检查返回值,也可以使用NSRunLoop类的方法以类似的方式运行运行run loop。(有关调用NSRunLoop类方法的run loop示例,请参见清单 Listing 3-14)。

    清单3-2运行一个run loop

    - (void)skeletonThreadMain
    {
        // Set up an autorelease pool here if not using garbage collection.
        BOOL done = NO;
     
        // Add your sources or timers to the run loop and do any other setup.
     
        do
        {
            // Start the run loop but return after each source is handled.
            SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
     
            // If a source explicitly stopped the run loop, or if there are no
            // sources or timers, go ahead and exit.
            if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
                done = YES;
     
            // Check for any other exit conditions here and set the
            // done variable as needed.
        }
        while (!done);
     
        // Clean up code here. Be sure to release any allocated autorelease pools.
    }
    

    可以递归地运行runloop。换句话说,可以调用CFRunLoopRunCFRunLoopRunInMode或任何NSRunLoop方法,用于从输入源或定时器的处理程序中启动run loop。执行此操作时,可以使用任何要运行嵌套run loop的模式,包括外部run loop使用的模式。

    Exiting the Run Loop -退出Run Loop

    有两种方法可以让Run Loop在处理事件之前退出:

    • 配置Run Loop以使用超时值运行。
    • 告诉Run Loop停止。

    如果可以管理的话,使用超时值当然是首选。指定一个超时值可以让Run Loop在退出之前完成其所有的正常处理,包括向run loop observers发送通知。

    使用CFRunLoopStop函数显式停止Run Loop会产生类似超时的结果。Run Loop发送任何剩余的run-loop通知,然后退出。不同之处在于,你可以在无条件启动的 run loops 中使用此技术。

    虽然移除run loop的输入源和定时器也可能导致运行循环退出,但这不是停止run loop的可靠方法。一些系统例程将输入源添加到run loop以处理所需的事件。因为你的代码可能不知道这些输入来源,所以无法移除它们,这会阻止run loop退出。

    RunLoop休眠的实现原理

    需要知道 用户态 与 内核态 的转换


    image.png
    Runloop与用户交互

    source1捕获事件交给source0处理

    RunLoop在实际中应用

    1. 控制线程生命周期(线程保活,AFNetworking),线程频繁创建去做串行的事,执行完一件事就销毁,此时可以线程保活,把这些事串行交给一个保活的线程去做

    2. 解决NSTimer 在华东是停止工作的问题 (Commons model)

    3. 监控应用卡顿

    4. 性能优化

    5. [[NSRunLoop currentRunLoop] run],run 方法是无法停止的,它专门开启一个永不销毁的线程(RunLoop),stop只是停止当前一次循环。
      自定义一个循环和条件标志去调用
      while (/自定义条件/) {
      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
      }
      停止自定义条件置否,停止RunLoop:CFRunLoopStop(CFRunLoopGetCurrent())

    一个 autorelease 对象什么时候释放

    NSObject *obj = [[[NSObject alloc] init] autorelease];
    对象释放是由 RunLoop 来控制,本次RunLoop的循环在睡眠前会对 autorelease对象调用 release


    image.png

    相关文章

      网友评论

          本文标题:iOS--RunLoop

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