ios 多线程开发(三)Run Loops

Run loops是线程相关的一些基本东西。一个run loop是一个处理消息的循环。用来处理计划任务或者收到的事件。run loop的作用是在有事做的时候保持线程繁忙,没事的时候让线程挂起。

Run loop的管理并不是完全自动的。你仍然需要设计代码来在合适的时候启动run loop来相应事件。Cocoa和Core Foundation都提供了run loop对象来配置和管理run loop。程序并不需要创建这些对象,每个线程,包括主线程都有一个对应的run loop对象。只有非主线程需要明确的启动它的run loop。自动启动主线程的run loop是app框架启动流程的一部分。

下面会介绍一下run loop以及如何配置它。

 

Run Loop 详解

Run loop正如它名字所说的一样。是线程进入的一个环,用来处理接收和处理事件。你需要写代码来控制run loop实际的循环,也就是说,你需要提供驱动run loop的while或者for循环。在循环中,使用run loop对象来处理事件,接收事件以及调用对应的处理程序。

run loop接收两种源。输入源传递异步的消息,通常是其他线程或其他程序发送过来的。定时器源传递同步事件,在一个计划的时间或重复的时间间隔产生。两种类型都使用程序指定的处理程序来处理事件。

下面的图展示了run loop和它的消息源的概念。输入源传递异步事件给对应的处理程序并且导致runUntilDate:方法退出。Timer发送事件给对应的处理程序但是不会导致run loop退出

 ios 多线程开发(三)Run Loops

另外,run loop有时候会发出广播。注册run-loop observers可以接收到这些广播然后来在线程上做你想做的事。可以使用Core Foundation来在线程上设置run-loop observers

 

Run Loop Modes

一个run loop mode是一个需要监控和处理的输入源和定时器的集合。每次运行run loop,都可以设置一个类型来运行。在这种情况下,只有和这种类型相关的事件才会被接收到。(也就是说,只有和这种类型相关的事件才会通知run loop的执行程序。)其他类型相关的源会挂起直到有对应的类型来接收它。

代码中使用名字来标识类型。Cocoa和Core Foundation都定义了一个默认类型以及其他几个通常用到的类型,也是通过字符串来标识他们的。你可以给类型名字指定一个字符串来自定义类型。虽然你可以自定义任何名字,但是类型的内容并不是任意的。你必须要添加一个或多个输入源,定时器或run-loop observer来让他们有意义。

使用类型来过滤run loop的事件。大多数时候,都会运行系统默认的类型。modal panel可能会使用"modal"类型。在这种类型下,只有和modal pannel相关的事件会发送。对于其他线程,可以使用自定义的类型来过滤低优先级的源。

提示:类型是根据事件源的类型,而不是事件的类型。比如,你不仅仅需要只需要鼠标按下或键盘事件。可能还需要监听端口,定时器挂起,或源的改变等。

下面是Cocoa和Core Foundation预定义的一些类型。

Mode 名字 描述
Defaule

NSDefaultRunLoopMode(Cocoa)

kCFRunLoopDefaultMode(Core Foundation)

默认类型是大部分操作用到的。大多数时候都使用这种类型来启动run loop以及配置输入源。

Connection NSConnectionReplyMode(Cocoa)

Cocoa使用这种类型来监测NSCOnnection对象返回。很少用到这种类型。

Modal NSModalPannelRunLoopMode(Cocoa)

Cocoa使用这种类型来标识modal pannels相关的事件。

Event tracking NSEventTrackingRunLoopMode(Cocoa)

Cocoa使用这种类型来监测鼠标拖动以及其他类型的用户界面操作追踪。

Common modes

NSRunLoopCommonModes(Cocoa)

kCFRunLoopCommonModes(Core Foundation)

这是常用类型的集合。指定一个输入源和这个类型相关也就是指定它和这个集合的类型相关。对于Cocoa程序,这个集合包括default,modal,以及event tracking类型。Core Foundation只包括default类型。可以使用CFRunLoopAddCommonMode方法来添加自定义类型到这个集合

 

输入源

输入源异步的发送消息到线程。事件的源取决于输入源,总体上有两种类型。基于Port的源模拟程序的port源,自定义源模拟自定义事件。不过run loop关心的并不是基于port或自定义事件。系统基本上两种都会实现。唯一的不同是他们如何发出的。基于port的是自动由内核发出的,自定义的源是由其他线程发出的。

创建输入源时,可以指定一个或多个run loop类型。类型决定了输入源在什么时候被监测到。大多数时候,run loop在默认类型下运行,不过也可以指定类型。如果输入源不是当前监测的类型,任何产生的事件都会挂起,直到有对应的类型能接收它。

下面介绍一些输入源

 

Port-Based Sources

Cocoa和Core Foundation提供了创建port-based源的相应对象和方法。例如,在Cocoa,基本上不用直接创建输入源。只需要创建一个port对象然后使用NSPort的方法把它加到run loop中。port对象会处理创建和配置输入源的事情。

 

自定义输入源

创建自定义输入源,需要使用Core Foundation中CFRunLoopSourceRef相关的方法。可以给输入源配置几个回调方法。Core Foundation会在不同点回调这些方法来配置输入源,处理事件,以及在从run loop移除时销毁它。

另外如果要定义收到事件的行为的话,需要定义事件的分发机制。这部分在另一个线程上运行,它负责给输入源提供数据并且在数据准备好后通知它。事件的分发之际又你决定,但是不要弄的太复杂。

 

Cocoa Perform Selector Sources

对于port-based sources,Cocoa定义了一个自定义的源来在任何线程上执行方法。和port-based源相似的是,执行方法的请求在目标线程上被序列化,这样可以减轻多个方法在同一个线程上被调用的同步问题。和port-based源不同的是,它执行后会从run loop把自己移除掉。

在另一个线程上执行方法时,目标线程必须要有一个活着的run loop。对于你创建的线程,这意味着它会等到启动run loop时才执行。因为主线程会自动启动它的run loop,因此在程序调用applicationDidFinishLaunching:后就可以开始调用这个方法了。run loop会一次调用所有计划的方法,而不是一个循环调用一个。

下面列出了NSObject在另一个线程上执行方法的方法。由于是在NSObject中定义的,所以可以在任何能访问Objective-C对象的线程中调用,包括POSIX线程。这些方法不会创建新线程来执行他们。

方法 描述

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

在主线程的下一个run loop中执行指定的方法。这个方法可以阻塞当前线程直到指定的方法执行完

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

在指定的线程上执行指定的方法。可以阻塞房前线程直到指定的方法执行完

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

在当前线程的下一个run loop中执行指定方法并且可以设置延时时间。由于等到下个run loop才会执行,所以这些方法默认的有一个最小的延时时间。队列中的多个方法会根据队列中的位置一个一个的执行。

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

可以取消使用performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes:发送给当前线程的消息。

 

定时器源

定时器源会在预设的时间给线程同步的发送事件。定时器是线程通知自己做一些事情的一种方法。例如,搜索框可以用定时器来实现用户输入内容后自动搜索。延时可以让用户在开始搜索前输入想输入的内容。

虽然它产生基于时间的消息,定时器并不是实时的。和输入源一样,定时器也是和run loop指定的类型相关。如果定时器不在当前run loop监测的类型中,它会一直等到支持定时器的run loop执行时才会触发。同样的,如果定时器在run loop执行的过程中触发了,定时器会等到下一个run loop才能实行相应的方法。如果run loop没有运行,定时器就根本不会触发。

 

Run Loop Observers

和源不同,源是在同步或异步事件产生是触发,run loop observers在run loop指定的特殊点触发。可以使用run loop observsers来让线程准备处理事件或让线程准备挂起。可以把run loop observers和下面的事件关联起来:

  • run loop的入口
  • run loop将要开始执行定时器
  • run loop将要执行一个输入源
  • run loop将要挂起
  • run loop被唤醒
  • run loop退出

可以使用Core Foundation来添加run loop监听者。要创建一个run loop监听者,需要创建一个CFRunLoopObserverRef实例。这个类型会追踪指定的回调方法以及感兴趣的事件。

和定时器相似,run loop监听者可以被使用一次或重复使用。一次性的监听者会在触发后从run loop移除,重复使用的会保留。使用一次还是重复使用是在创建时指定的。

 

Run Loop事件顺序

每次运行时,线程的run loop会执行预定义的事件并且给每个监听者发广播。调用的顺序时固定的:

  1. 通知监听者run loop进入了
  2. 通知监听者任何准备好的定时器将要触发
  3. 通知监听者任何非基于port的输入源将要触发
  4. 触发任何非基于port的事件
  5. 如果有任何基于port事件将要触发,处理事件,然后到第9步
  6. 通知监听者线程将要挂起
  7. 把线程挂起直到下面的事件之一触发
    • 一个基于port的消息触发
    • 定义定时器触发
    • run loop设置的超时时间到了
    • run loop被明确的唤醒
  8. 通知监听者线程被唤醒
  9. 处理需要处理的事件
    • 如果一个用户定义的定时器触发,执行定时器然后重启消息循环。跳转到第2步
    • 如果一个事件源触发,分发事件
    • 如果run loop被明确的唤醒但是还没有超时,重启消息循环。跳转到第2步
  10. 通知所有的监听者run loop退出

由于定时器和输入源的监听者广播是在事件执行前发出的,广播的事件和真实执行的事件可能会有时间差。如果这个时间差很重要,可以使用sleep和awake-from-sleep广播来修正真实时间。

由于定时器和其他周期性的事件是在run loop运行时分发的,破坏run loop会破会消息分发。

 

什么时候应该使用Run Loop

程序中只有明确的需要使用另外一个线程时才会需要run loop。主线程的run loop是程序的基础部分之一。所以,app的框架提供了运行主线程以及自动启动run loop的代码。UIApplication的run方法启动了主循环。如果使用Xcode工程模版创建程序,根本步需要调用这些方法。

对于辅助线程,你需要决定是否需要run loop, 如果需要的话需要自己配置并启动它。有些时候完全步需要run loop。比如,创建一个线程来执行一个长时间或指定好的任务,这时候根本不需要启动run loop。Run loop在需要很多线程间交互的时候使用。比如,需要做下面的事情时:

  • 使用基于port或自定义的源来进行线程间通讯
  • 在线程上使用定时器
  • 在Cocoa程序中使用performSelector...方法
  • 执行周期性的任务

如果选择使用run loop,配置和使用是相对简单的。在整个多线程编程过程中,最好是计划好需要哪些辅助线程。线程最好是让它正常结束而不是强行中止。

 

使用Run Loop对象

run loop对象提供了添加输入源,定时器,run-loop obervers以及运行它的接口。每个线程都有一个对应的run loop对象。在Cocoa中,这个对象是一个NSRunLoop类实例。在底层程序中,它是指向CFRunLoopRef的指针。

 

获取Run Loop对象

要获取当前线程的run loop对象,可以使用下面的方法:

  • 在Cocoa程序中,使用NSRunLoop的currentRunLoop方法来获得NSRunLoop对象。
  • 使用CFRunLoopGetCurrent方法。

虽然他们不是完全相同的,但是也可以通过NSRunLoop对象获得CFRunLoopRef。NSRunLoop类定义了一个getCGRunLoop方法来返回一个CGRunLoopRef类型。由于两种对象指向同一个run loop,因此NSRunLoop对象和CFRunLoopRef可以混合使用。

 

配置Run Loop

在辅助线程中使用run loop之前,需要至少加入一个输入源或定时器。如果run loop没有东西要监控,它运行时会立刻退出。

除了添加源以外,也可以添加run loop监听者来监测run loop运行的状态。创建run loop监听者,需要创建一个CFRunLoopObserverRef类型,然后使用CGRunLoopAddObserver方法添加到run loop。就算是Cocoa程序,run loop监听者也需要使用Core Foundation来创建。

下面展示了创建run loop监听者的主要程序。

- (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);
}

如果是配置长时间存活的线程,最好是添加一个输入源来接收消息。虽然可以只添加一个定时器,一旦定时器触发后,如果定时器无效了,就会导致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指定一种类型。类型和超时并不是互斥的,他们可以同时被使用。类型用来限制输入源的类型。

下面展示一个线程的入口框架。主要是添加输入源和定时器后,重复的调用run loop来接收消息。每次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.
}

run loop是可以递归的使用的。也就是说,可以在输入源或定时器的处理程序中调用CFRunLoopRun,CFRunLoopRunInMode等方法。

 

退出Run Loop

有两种方式可以让run loop收到事件前退出

  • 配置run loop的超时时间
  • 主动让run loop停止

如果可以的话,最好是使用超时时间。指定超时时间可以让run loop在退出前完成它所有应该做的事,包括给监听者发送广播。

明确的使用CFRunLoopStop停止run loop和超时相似。run loop也会把需要发送的广播发送给监听者。不同的事这种方法主要用在使用无条件启动的run loop。

虽然删除Run Loop的输入源和定时器也会导致run loop退出,但是不是很靠谱。系统也许会自动添加一些输入源。可能代码没有意识到这些输入源,他们可能是无法移除的,这会导致run loop无法退出。

 

线程安全以及Run Loop对象

线程安全取决于你使用什么API来维护run loop。Core Foundation中的方法整体上都是线程安全的并且可以在任何线程调用。如果是做改变run loop的配置的操作,最好还是在run loop所属的线程做比较好。

Cocoa的NSRunLoop类并没有继承Core Foundation的线程安全部分。如果是使用NSRunLoop类来修改run loop,应该在run loop所属的线程上做。在另一个线程上给run loop添加输入源或定时器可能会导致crash或其他异常。

 

配置Run Loop源

自定义输入源

创建自定义源包括下面一些内容:

  • 输入源需要执行的信息
  • 感兴趣的客户如何与输入源交互
  • 处理客户请求的处理程序
  • 取消输入源的方法
  • 由于是自定义的输入源来处理自定义的信息,所有实际的配置就很灵活了。调度,处理,取消流程是自定义源的主要流程。大多数其他行为都在这几个方法之外。比如,什么时候传递数据什么时候和其他线程交互由你决定。

下面的图展示了一个简单的自定义源。程序的主线程维护了一个输入源的引用,输入源的命令缓冲区,以及输入源所在的run loop。当主线程有工作交给工作线程时,它把命令和所需的数据一起发到命令缓冲区来让工作线程开始工作。(由于主线程和工作线程都能访问命令缓冲区,所以访问必须是同步的。)命令发送之后,主线程会给输入源发送一个信号来唤醒工作线程的run loop。当收到唤醒命令后,run loop调用输入源的处理程序来处理命令缓冲区中的命令。

ios 多线程开发(三)Run Loops

 

定义输入源

定义输入源需要使用Core Foundation来配置以及和run loop交互。虽然基本的处理程序是基于C的方法,也可以使用Objective-C或C++来封装。

下面展示了一个输入源的定义。RunLoopSource对象管理一个命令缓冲区并且用它来接收其他线程的消息。也展示了一个RunLoopContext对象的定义,它只是一个把RunLoopSource对象和run loop对象的引用传递给主线程的容器。

@interface RunLoopSource : NSObject
{
    CFRunLoopSourceRef runLoopSource;
    NSMutableArray* commands;
}
 
- (id)init;
- (void)addToCurrentRunLoop;
- (void)invalidate;
 
// Handler method
- (void)sourceFired;
 
// Client interface for registering commands to process
- (void)addCommand:(NSInteger)command withData:(id)data;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
 
@end
 
// These are the CFRunLoopSourceRef callback functions.
void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
void RunLoopSourcePerformRoutine (void *info);
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode);
 
// RunLoopContext is a container object used during registration of the input source.
@interface RunLoopContext : NSObject
{
    CFRunLoopRef        runLoop;
    RunLoopSource*        source;
}
@property (readonly) CFRunLoopRef runLoop;
@property (readonly) RunLoopSource* source;
 
- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop;
@end

虽然使用Objectice-C代码管理输入源的自定义数据,把输入源和run loop关联仍然需要c回调方法。第一个被调用的方法是把它和run loop关联起来。由于这个输入源只有一个客户端(主线程),它调用计划方来发送消息来注册到线程上。当代理需要和输入源通讯时,使用RunLoopContext对象中的信息就可以了。

void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
    RunLoopSource* obj = (RunLoopSource*)info;
    AppDelegate*   del = [AppDelegate sharedAppDelegate];
    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
 
    [del performSelectorOnMainThread:@selector(registerSource:)
                                withObject:theContext waitUntilDone:NO];
}

最重要的回调过程之一是,当输入源触发时处理自定义的数据。下面展示了处理RunLoopSource对象相关的回调。这个方法只是简单的把请求转发给sourceFired方法,它会执行命令缓冲区中的命令。

void RunLoopSourcePerformRoutine (void *info)
{
    RunLoopSource*  obj = (RunLoopSource*)info;
    [obj sourceFired];
}

如果你有调用CFRunLoopSourceInvalidate方法来移除输入源,系统会调用输入源的取消操作。可以在这个时候通知客户端将要无效,客户端需要移除对它的引用。下面展示了RunLoopSource对象注册的取消回调方法。这个方法发送RunLoopContext对象给程序的代理,这次时通知他们移除引用

void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
    RunLoopSource* obj = (RunLoopSource*)info;
    AppDelegate* del = [AppDelegate sharedAppDelegate];
    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
 
    [del performSelectorOnMainThread:@selector(removeSource:)
                                withObject:theContext waitUntilDone:YES];
}

 

在Run Loop上配置输入源

下面是RunLoopSource类的init和addToCurrentRunLoop方法。init方法创建一个CGRunLoopSourceRef类型是实际被关联到run loop的。它返回它自己也就是RunLoopSource对象,这样外面可以有一个指向对象的指针。添加到线程的工作直到工作线程调用addToCurrentRUnLoop方法后才会生效,那时候RunLoopSourceScheduleRoutine回调方法会被调用。只要这个源加到run loop上之后,线程就可以运行run loop来等待消息了

- (id)init
{
    CFRunLoopSourceContext    context = {0, self, NULL, NULL, NULL, NULL, NULL,
                                        &RunLoopSourceScheduleRoutine,
                                        RunLoopSourceCancelRoutine,
                                        RunLoopSourcePerformRoutine};
 
    runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
    commands = [[NSMutableArray alloc] init];
 
    return self;
}
 
- (void)addToCurrentRunLoop
{
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}

 

客户端和输入源对应

想要输入源真的有用的话,需要维护它并且用它向其他线程发送信号。输入源的主要功能是让相关的线程挂起,直到他们有事要做的时候在唤醒。所以就需要让其他线程直到这个输入源并且有一个和它通讯的方式。

一种让客户端知道输入源的方法是在第一个装载在run loop上时发送注册请求。可以注册任意多的你想要的客户端,或者这册一个核心的,然后由它把消息转给其他的。下面展示一个定义在程序回调中的注册方法(它在RunLoopSource对象的计划方法中被调用了)。这个方法接收到一个RunLoopContext对象然后加入到列表中。这里也展示了如何注销它

- (void)registerSource:(RunLoopContext*)sourceInfo;
{
    [sourcesToPing addObject:sourceInfo];
}
 
- (void)removeSource:(RunLoopContext*)sourceInfo
{
    id    objToRemove = nil;
 
    for (RunLoopContext* context in sourcesToPing)
    {
        if ([context isEqual:sourceInfo])
        {
            objToRemove = context;
            break;
        }
    }
 
    if (objToRemove)
        [sourcesToPing removeObject:objToRemove];
}

 

输入源发送信号

在处理好输入源的数据后,客户端就可以发送信号来唤醒run loop了。输入源发信号可以让run loop知道可以准备好执行了。因为有可能在收到信号时线程是挂起状态,所以每次都需要明确的唤醒run loop。否则可能会导致输入源延迟执行。

下面展示了RunLoopSource的fireCommandsOnRunLoop方法。客户端把命令加入到命令缓冲区并且准备好执行后会调用它。

- (void)fireCommandsOnRunLoop:(CFRunLoopRef)runloop
{
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

注意:不要尝试去处理SIGHUP或其他线程级别的信号。Core Foundation唤醒run loop的信号不是信号安全的,不应该在你的程序中处理。

 

配置定时器源

要创建一个定时器源,只需要创建一个定时器对象然后加到run loop上。在Cocoa中,可以使用NSTimer类来创建定时器对象,在Core Foundation中可以使用CFRunLoopTimerRef。实际上NSTimer类是Core Foundation的扩展,提供了一下更方便的方法,比如创建以及添加到线程。

在Cocoa中,可以通过下面的方法创建定时器

  • scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:
  • scheduledTimerWithTimeInterval:invocation:repeats:

这些方法会创建一个定时器然后以默认类型(NSDefaultRunLoopMode)添加到当前线程的run loop。也可以手动创建一个NSTImer对象,然后调用NSRunLoop的addTimer:forMode:方法添加到run loop上。两种方法本质上做了相同的事,但是可以让你在不同层面上控制定时器的配置。比如,如果手动的创建定时器然后添加到run loop,这样可以使用除了默认类型以外的类型。下面展示了两种方法。第一个定时器延时1秒后每0.1秒触发一次。第二个定时器0.2秒后每0.2秒触发一次。

NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
// Create and schedule the first timer.
NSDate* futureDate = [NSDate dateWithTimeIntervalSinceNow:1.0];
NSTimer* myTimer = [[NSTimer alloc] initWithFireDate:futureDate
                        interval:0.1
                        target:self
                        selector:@selector(myDoFireTimer1:)
                        userInfo:nil
                        repeats:YES];
[myRunLoop addTimer:myTimer forMode:NSDefaultRunLoopMode];
 
// Create and schedule the second timer.
[NSTimer scheduledTimerWithTimeInterval:0.2
                        target:self
                        selector:@selector(myDoFireTimer2:)
                        userInfo:nil
                        repeats:YES];

下面展示了使用Core Foundation方法来配置定时器。虽然下面没有传递任何用户定义的信息,但是你可以使用这个数据结构传递你想传递的任何数据

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0,
                                        &myCFTimerCallback, &context);
 
CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

 

配置Port-Based输入源

Cocoa和Core Foundation都提供了port-based对象来进行线程间通讯。

 

配置NSMachPort对象

要建立和NSMachPort对象的联系,需要创建一个port对象然后把它加到线程的run loop中。当启动辅助线程时,把同样的对象传递给线程的入口方法。辅助线程就可以使用相同的对象来把消息发送回来。

 

主线程代码实现

下面展示了启动辅助线程的代码。Cocoa框架处理了port和run loop的一些中间步骤,所有启动线程的方法比Core Fouundation要短。但是两种效果是一样的。不同的是这个方法直接发送NSPort对象给工作线程

- (void)launchThread
{
    NSPort* myPort = [NSMachPort port];
    if (myPort)
    {
        // This class handles incoming port messages.
        [myPort setDelegate:self];
 
        // Install the port as an input source on the current run loop.
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
 
        // Detach the thread. Let the worker release the port.
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)
               toTarget:[MyWorkerClass class] withObject:myPort];
    }
}

如果要设置线程的双向通讯,也需要工作线程把它的port发送给主线程。

#define kCheckinMessage 100
 
// Handle responses from the worker thread.
- (void)handlePortMessage:(NSPortMessage *)portMessage
{
    unsigned int message = [portMessage msgid];
    NSPort* distantPort = nil;
 
    if (message == kCheckinMessage)
    {
        // Get the worker thread’s communications port.
        distantPort = [portMessage sendPort];
 
        // Retain and save the worker port for later use.
        [self storeDistantPort:distantPort];
    }
    else
    {
        // Handle other messages.
    }
}

辅助线程代码实现

对于辅助线程,需要配置线程来使用port和主线程通讯。

下面讲述了设置辅助线程。在创建autorelease pool之后,它创建了一个工作对象来控制线程执行。工作对象的sendCheckinMessage:方法给工作线程创建一个本地port然后发送消息给主线程。

+(void)LaunchThreadWithPort:(id)inData
{
    NSAutoreleasePool*  pool = [[NSAutoreleasePool alloc] init];
 
    // Set up the connection between this thread and the main thread.
    NSPort* distantPort = (NSPort*)inData;
 
    MyWorkerClass*  workerObj = [[self alloc] init];
    [workerObj sendCheckinMessage:distantPort];
    [distantPort release];
 
    // Let the run loop process things.
    do
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                            beforeDate:[NSDate distantFuture]];
    }
    while (![workerObj shouldExit]);
 
    [workerObj release];
    [pool release];
}

在使用NSMachPort时,线程间单向通讯可以使用同一个对象。也就是说,当前线程创建的port对象是其他线程接收到的port对象。

下面是辅助线程的check-in流程。这个方法设置了一个port用来在以后进行通讯,然后把它发送给主线程。这个方法使用LaunchThreadWithPort:方法传过来的port对象

// Worker thread check-in method
- (void)sendCheckinMessage:(NSPort*)outPort
{
    // Retain and save the remote port for future use.
    [self setRemotePort:outPort];
 
    // Create and configure the worker thread port.
    NSPort* myPort = [NSMachPort port];
    [myPort setDelegate:self];
    [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];
 
    // Create the check-in message.
    NSPortMessage* messageObj = [[NSPortMessage alloc] initWithSendPort:outPort
                                         receivePort:myPort components:nil];
 
    if (messageObj)
    {
        // Finish configuring the message and send it immediately.
        [messageObj setMsgId:setMsgid:kCheckinMessage];
        [messageObj sendBeforeDate:[NSDate date]];
    }
}

配置NSMessagePort对象

与NSMessagePort对象建立连接并不是见到的在线程间传递port对象。远程port纤细必须要有一个名字。Cocoa用一个特定的名字注册port然后把它传递给远程线程来进行通讯。下面展示了创建和注册消息port的代码

NSPort* localPort = [[NSMessagePort alloc] init];
 
// Configure the object and add it to the current run loop.
[localPort setDelegate:self];
[[NSRunLoop currentRunLoop] addPort:localPort forMode:NSDefaultRunLoopMode];
 
// Register the port using a specific name. The name must be unique.
NSString* localPortName = [NSString stringWithFormat:@"MyPortName"];
[[NSMessagePortNameServer sharedInstance] registerPort:localPort
                     name:localPortName];

使用Core Foundation配置port-based输入源

这里展示如何使用Core Foundation在主线程和工作线程之间设置一个双向通讯通道。

主线程调用下面的方法来启动工作线程。里面做的第一件事是设置了一个CFMessagePortRef类型来监听工作线程的消息。工作线程需要port的名字来建立连接, 所以名字会在工作线程的入口传过去。名字必须是唯一的。

#define kThreadStackSize        (8 *4096)
 
OSStatus MySpawnThread()
{
    // Create a local port for receiving responses.
    CFStringRef myPortName;
    CFMessagePortRef myPort;
    CFRunLoopSourceRef rlSource;
    CFMessagePortContext context = {0, NULL, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
 
    // Create a string with the port name.
    myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
 
    // Create the port.
    myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &MainThreadResponseHandler,
                &context,
                &shouldFreeInfo);
 
    if (myPort != NULL)
    {
        // The port was successfully created.
        // Now create a run loop source for it.
        rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
 
        if (rlSource)
        {
            // Add the source to the current run loop.
            CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
            // Once installed, these can be freed.
            CFRelease(myPort);
            CFRelease(rlSource);
        }
    }
 
    // Create the thread and continue processing.
    MPTaskID        taskID;
    return(MPCreateTask(&ServerThreadEntryPoint,
                    (void*)myPortName,
                    kThreadStackSize,
                    NULL,
                    NULL,
                    NULL,
                    0,
                    &taskID));
}

线程启动之后,主线程在等待反馈时会继续执行其他的任务。当反馈消息回来时,会分发到下面的MainThreadResponseHandler方法。

#define kCheckinMessage 100
 
// Main thread port message handler
CFDataRef MainThreadResponseHandler(CFMessagePortRef local,
                    SInt32 msgid,
                    CFDataRef data,
                    void* info)
{
    if (msgid == kCheckinMessage)
    {
        CFMessagePortRef messagePort;
        CFStringRef threadPortName;
        CFIndex bufferLength = CFDataGetLength(data);
        UInt8* buffer = CFAllocatorAllocate(NULL, bufferLength, 0);
 
        CFDataGetBytes(data, CFRangeMake(0, bufferLength), buffer);
        threadPortName = CFStringCreateWithBytes (NULL, buffer, bufferLength, kCFStringEncodingASCII, FALSE);
 
        // You must obtain a remote message port by name.
        messagePort = CFMessagePortCreateRemote(NULL, (CFStringRef)threadPortName);
 
        if (messagePort)
        {
            // Retain and save the thread’s comm port for future reference.
            AddPortToListOfActiveThreads(messagePort);
 
            // Since the port is retained by the previous function, release
            // it here.
            CFRelease(messagePort);
        }
 
        // Clean up.
        CFRelease(threadPortName);
        CFAllocatorDeallocate(NULL, buffer);
    }
    else
    {
        // Process other messages.
    }
 
    return NULL;
}

主线程配置好之后,剩下的工作是给工作线程创建port。下面展示了工作线程的入口。

OSStatus ServerThreadEntryPoint(void* param)
{
    // Create the remote port to the main thread.
    CFMessagePortRef mainThreadPort;
    CFStringRef portName = (CFStringRef)param;
 
    mainThreadPort = CFMessagePortCreateRemote(NULL, portName);
 
    // Free the string that was passed in param.
    CFRelease(portName);
 
    // Create a port for the worker thread.
    CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"), MPCurrentTaskID());
 
    // Store the port in this thread’s context info for later reference.
    CFMessagePortContext context = {0, mainThreadPort, NULL, NULL, NULL};
    Boolean shouldFreeInfo;
    Boolean shouldAbort = TRUE;
 
    CFMessagePortRef myPort = CFMessagePortCreateLocal(NULL,
                myPortName,
                &ProcessClientRequest,
                &context,
                &shouldFreeInfo);
 
    if (shouldFreeInfo)
    {
        // Couldn‘t create a local port, so kill the thread.
        MPExit(0);
    }
 
    CFRunLoopSourceRef rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);
    if (!rlSource)
    {
        // Couldn‘t create a local port, so kill the thread.
        MPExit(0);
    }
 
    // Add the source to the current run loop.
    CFRunLoopAddSource(CFRunLoopGetCurrent(), rlSource, kCFRunLoopDefaultMode);
 
    // Once installed, these can be freed.
    CFRelease(myPort);
    CFRelease(rlSource);
 
    // Package up the port name and send the check-in message.
    CFDataRef returnData = nil;
    CFDataRef outData;
    CFIndex stringLength = CFStringGetLength(myPortName);
    UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);
 
    CFStringGetBytes(myPortName,
                CFRangeMake(0,stringLength),
                kCFStringEncodingASCII,
                0,
                FALSE,
                buffer,
                stringLength,
                NULL);
 
    outData = CFDataCreate(NULL, buffer, stringLength);
 
    CFMessagePortSendRequest(mainThreadPort, kCheckinMessage, outData, 0.1, 0.0, NULL, NULL);
 
    // Clean up thread data structures.
    CFRelease(outData);
    CFAllocatorDeallocate(NULL, buffer);
 
    // Enter the run loop.
    CFRunLoopRun();
}

进入run loop后,其他发送过来的时间由ProcessClientRequest方法处理。这个方法怎么实现取决于这个线程想做什么。

ios 多线程开发(三)Run Loops,布布扣,bubuko.com

ios 多线程开发(三)Run Loops

上一篇:Android Exception 9(requestFeature() must be called before adding content)


下一篇:PHP 数组函数