iOS 多线程编程之RunLoop

2,501 阅读19分钟

这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战

iOS多线程编程系列

  1. iOS 多线程编程之概述
  2. iOS 多线程编程之RunLoop
  3. iOS 多线程编程之同步

概述

RunLoop是与线程相关的基础架构的一部分。运行循环是一个事件处理循环,用于安排工作和协调接收传入事件。RunLoop的目的是在有工作要做时让你的线程保持忙碌,并在没有工作时让线程进入睡眠状态。

RunLoop不是完全自动的。需要设计线程代码以在适当的时间启动RunLoop并响应传入事件。 Cocoa 和 Core Foundation 都提供了运行循环对象来配置和管理线程的RunLoop。不需要显式创建这些对象;每个线程,包括应用程序的主线程,都有一个关联的RunLoop对象。然而,只有辅助线程需要显式运行它们的运行循环。作为应用程序启动过程的一部分,应用程序框架会在主线程上自动设置和运行运行循环。

RunLoop Object

RunLoop是响应传入事件的循环。

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

官方提的示例图,显示了运行循环和各种源的概念结构。输入源将异步事件传递给相应的处理程序并导致 runUntilDate: 方法(在线程关联的 NSRunLoop 对象上调用)退出。计时器源将事件传递给它们的处理程序,但不会导致运行循环退出。

image.png

以下部分提供了有关RunLoop的组件及其运行模式的更多信息。 它们还描述了在事件处理过程中在不同时间生成的通知。

Run Loop Modes

RunLoop Modes 是要监视的输入源和计时器的集合以及要通知的RunLoop观察者的集合。RunLoop运行需要指定(显式或隐式)一个特定的“模式”运行。在运行循环的该阶段,只有与该模式关联的源被监控并允许传递它们的事件。(类似地,只有与该模式关联的观察者会被通知运行循环的进度).与其他模式关联的源会保留,直到后续以适当的模式处理。

Cocoa 和 Core Foundation 都定义了默认模式和几种常用模式,以及用于在代码中指定这些模式的字符串。可以通过简单地为模式名称指定模式。这些模式必须确保将一个或多个sourcetimerobserver添加到您创建的任何模式中,以使它们有用。

OS X 和iOS模式有差异,具体模式如下

ModeNameDescription
DefaultNSDefaultRunLoopMode (Cocoa)kCFRunLoopDefaultMode (Core Foundation)默认模式是用于大多数操作的模式。 大多数时候,应该使用这个模式来启动RunLoop并配置Source
ConnectionNSConnectionReplyMode (Cocoa)OS X 上使用,解释略
ModalNSModalPanelRunLoopMode (Cocoa)OS X 上使用,解释略
Event trackingNSEventTrackingRunLoopMode (Cocoa)Cocoa 使用此mode来限制滑动期间传入的事件u
Common modesNSRunLoopCommonModes (Cocoa)kCFRunLoopCommonModes (Core Foundation)iOS中此模式不是特指某种模式,而是表示default和tracking mode

Sources

source将事件异步传递到您的线程。source源分两种:1.基于Port的输入源监控应用程序的 Mach 端口;2.自定义输入源监视自定义事件源.两个来源之间的唯一区别是它们是如何发出信号的。基于Port的源由内核自动发出信号,而自定义源必须从另一个线程手动发出信号。

创建输入源需要分配给RunLoop的一种或多种模式。模式会影响在任何给定时刻监控哪些输入源。大多数时候,在默认模式下运行运行循环,但也可以指定自定义模式。如果输入源不在当前监控的模式下,它生成的任何事件都会被保留,直到运行循环以正确的模式运行。 以下部分描述了一些sources.

基于Port的源

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

自定义source

要创建自定义source,必须使用与 Core Foundation 中的 CFRunLoopSourceRef 关联的函数。 可以使用多个回调函数配置自定义输入源。 Core Foundation 在不同的点调用这些函数来配置源,处理任何传入事件,并在源从RunLoop中删除时将其拆除。

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

NSObject的performSelect方法

MethodsDescription
performSelectorOnMainThread:withObject:waitUntilDone:``performSelectorOnMainThread:withObject:waitUntilDone:modes:在应用程序的主线程的下一个运行循环周期中执行指定的选择器。 可以选择阻塞当前线程,直到执行选择器。
performSelector:onThread:withObject:waitUntilDone:``performSelector:onThread:withObject:waitUntilDone:modes:在您拥有NSThread对象的任何线程上执行指定的选择器。 可以选择阻塞当前线程,直到执行选择器。
performSelector:withObject:afterDelay:``performSelector:withObject:afterDelay:inModes:在下一个运行循环周期和可选的延迟期之后,在当前线程上执行指定的选择器。 因为它会等到下一个运行循环周期来执行选择器,所以这些方法提供了当前执行代码的自动最小延迟。 多个排队选择器按照它们排队的顺序一个接一个地执行。
cancelPreviousPerformRequestsWithTarget:``cancelPreviousPerformRequestsWithTarget:selector:object:允许使用 performSelector:withObject:afterDelay:performSelector:withObject:afterDelay:inModes: 方法取消发送到当前线程的消息.

Timer

Timer源在预设时间将事件同步传递到线程。定时器是线程通知自己做某事的一种方式。

虽然它会生成基于时间的通知,但计时器并不是一种实时机制。与输入源一样,计时器与运行循环的特定模式相关联。如果计时器不在运行循环当前正在监视的模式下,它不会触发,直到您以计时器支持的模式之一运行运行循环。类似地,如果在 run loop 正在执行处理程序例程的中间触发计时器,则计时器将等待直到下一次通过 run loop 调用其处理程序例程。如果运行循环根本没有运行,则计时器永远不会触发。

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

Observers

Observer在RunLoop本身执行期间在特殊位置触发。您可以使用RunLoop Observer来处理给定事件或在线程进入睡眠之前准备线程。RunLoop Observer关联的的时间如下:

  • The entrance to the run loop.(进入runloop)
  • When the run loop is about to process a timer.(处理定时器)
  • When the run loop is about to process an input source.(处理输入源)
  • When the run loop is about to go to sleep.(睡眠)
  • When the run loop has woken up, but before it has processed the event that woke it up.(唤醒)
  • The exit from the run loop.(退出) 可以使用CFRunLoopObserverRef创建Observe,与计时器类似,observer可以使用一次或重复使用。单次observe在触发后将自己从RunLoop中移除,而重复Observer保持连接状态。可以在创建观察者时指定Observe是运行一次还是重复运行。

RunLoop的运行步骤

线程的RunLoop都会处理挂起的事件并为任何附加的Observe生成通知。它执行此操作的顺序非常具体:

  1. Notify observers that the run loop has been entered.(通知观察者已经进入RunLoop)

  2. Notify observers that any ready timers are about to fire.(通知观察者即将处理定时器)

  3. Notify observers that any input sources that are not port based are about to fire.(通知观察者任何不基于port的输入源即将触发)

  4. Fire any non-port-based input sources that are ready to fire.(触发任何准备触发的非基于port的输入源)

  5. If a port-based input source is ready and waiting to fire, process the event immediately. Go to step 9.(如果基于port的输入源已准备好并等待触发,则立即处理该事件。转到步骤 9。)

  6. Notify observers that the thread is about to sleep.(通知观察者线程即将休眠)

  7. Put the thread to sleep until one of the following events occurs:(线程休眠,如果有以下事件则唤醒)

    • An event arrives for a port-based input source.(基于端口的输入源的事件到达)
    • A timer fires.(计时器触发)
    • The timeout value set for the run loop expires.(为运行循环设置的超时值到期)
    • The run loop is explicitly woken up.(运行循环被显式唤醒)
  8. Notify observers that the thread just woke up.(通知观察者线程刚刚唤醒)

  9. Process the pending event.(处理挂起的事件)

    • If a user-defined timer fired, process the timer event and restart the loop. Go to step 2.(如果触发了用户定义的计时器,则处理计时器事件并重新启动循环。转到第 2 步)
    • If an input source fired, deliver the event.(如果触发了输入源,则传递事件)
    • If the run loop was explicitly woken up but has not yet timed out, restart the loop. Go to step 2.(如果运行循环被显式唤醒但尚未超时,则重新启动循环。转到第 2 步)
  10. Notify observers that the run loop has exited.(通知观察者运行循环已退出)

RunLoop的使用

cocoa中,提供了使用NSRunLoop创建实例的方式,更低层的C 语言提供了CFRunLoopRef的指针. RunLoop的应用层面也是通过这两种方式展开的.

获取线程

//方式一
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
//方式二
CFRunLoopRef cfrunloop = CFRunLoopGetCurrent();

添加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);

}

启动RunLoop

只有应用程序中的辅助线程才需要启动运行循环。一个运行循环必须至少有一个sourcetimer来监控。如果没有附加,则运行循环立即退出。 有几种方法可以启动运行循环,包括以下几种:

  • 无条件的 此方式将RunLoop将线程置于一个永久循环中,所以几乎无法控制RunLoop本身。可以添加和删除输入源和计时器,但停止运行循环的唯一方法是终止它。也没有办法在自定义模式下运行运行循环。
  • 设置时间限制 使用此方式时,RunLoop将一直运行,直到事件到达或分配的时间到期。如果一个事件到达,该事件被分派给一个处理程序进行处理,然后RunLoop退出。然后,可以重新启动RunLoop以处理下一个事件。如果分配的时间到期,可以简单地重新RunLoop
  • 在特定模式下 模式和设置时间限制不是互斥的,在启动RunLoop时都可以使用。模式限制了将事件传递到运行循环的源类型.

退出RunLoop

在处理事件之前,有两种方法可以让运行循环退出:

  • 设置RunLoop的超时时间 此方式可管理RunLoop,此方式使的RunLoop在退出之前会完成所有正常处理,包括向运行循环观察者发送通知.
  • 显示调用停止 使用 CFRunLoopStop 函数显式停止运行循环会产生类似于超时的结果。运行循环发送所有剩余的运行循环通知,然后退出。可对应启动RunLoop无条件方式使用.

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

配置 RunLoop Sources

自定义input source(Source0)

可以在主线程直接通过激活子线程的souce0,在子线程做一些事情(自定义的source事件)。官方给出的示例图

image.png

代码实现如下,

头文件

//创建自定义的source类
@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

实现文件

#import "RunLoopSource.h"
#import "AppDelegate.h"

void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{

    NSLog(@"RunLoopSourceScheduleRoutine 对象地址 %p",info);

    RunLoopSource* obj = ( __bridge RunLoopSource*)(info);

    AppDelegate*   del = (AppDelegate*)[UIApplication sharedApplication].delegate;;

    //这里将source0封装成context

    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

    NSLog(@"RunLoopSourceScheduleRoutine 对象引用计数为 %@",[obj valueForKey:@"retainCount"]);

    //这里将context加入到HCRunLoopDelegate中

    [del performSelectorOnMainThread: @selector(registerSource:)

                          withObject:theContext waitUntilDone:NO];

}

void RunLoopSourcePerformRoutine (void *info)
{

    NSLog(@"RunLoopSourcePerformRoutine 对象地址 %p",info);

    RunLoopSource*  obj = ( __bridge RunLoopSource*)(info);

    NSLog(@"RunLoopSourcePerformRoutine 对象引用计数 %@",[obj valueForKey:@"retainCount"]);

    [obj sourceFired];

    obj = nil;

}

void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)

{

    NSLog(@"RunLoopSourceCancelRoutine 对象地址 %p",info);

    RunLoopSource* obj = ( __bridge RunLoopSource*)(info);

    AppDelegate*   del = (AppDelegate*)[UIApplication sharedApplication].delegate;;

    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];

    //移除引用

    [del performSelectorOnMainThread: @selector(removeSource:)

                          withObject:theContext waitUntilDone:YES];

}

@implementation HCCommand

- (id)initWithPriorty:(NSInteger)prior withData:(id)data {

    if (self = [super init]) {

        _priority = prior;

        _data = data;

    }

    return self;

}

@end

@implementation RunLoopSource

- (id)init

{

    //创建source0

    CFRunLoopSourceContext    context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,

        &RunLoopSourceScheduleRoutine,

        RunLoopSourceCancelRoutine,

        RunLoopSourcePerformRoutine};


    runLoopSource = CFRunLoopSourceCreate(**NULL**, 0, &context);

    commands = [[NSMutableArray alloc] init];

    

    return self;

}

- (void)addToCurrentRunLoop
{

    //将source0加入到当前runloop中

    CFRunLoopRef runLoop = CFRunLoopGetCurrent();

    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);

}

// Client interface for registering commands to process

- (void)addCommand:(NSInteger)command withData:(**id**)data {

    //在source0中加入命令操作

    HCCommand *cmd = [[HCCommand alloc] initWithPriorty:command withData:data];

    [commands addObject:cmd];

}

// Handler method

- (void)sourceFired
{

    NSLog(@"source fired"); //在这里断点观察source0的堆栈

    for (HCCommand *cmd in commands) {

        NSLog(@"[线程:%@] 执行command : %@",[NSThread currentThread],cmd.data);

    }

}

- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop
{

    //触发执行source0,会触发RunLoopSourcePerformRoutine

    CFRunLoopSourceSignal(runLoopSource);

    CFRunLoopWakeUp(runloop);

}

- (**void**)invalidate:(CFRunLoopRef)runloop {

    //CFRunLoopRemoveSource会触发RunLoopSourceCancelRoutine

    CFRunLoopRemoveSource(runloop, runLoopSource, kCFRunLoopDefaultMode);

    CFRunLoopStop(runloop);

    CFRelease(runloop);

}
@end

@implementation**  RunLoopContext

- (id)initWithSource:(RunLoopSource*)src andLoop:(CFRunLoopRef)loop {

    if (self = [super init]) {

        _source = src;

        _runLoop = loop;

    }

    return self;

}
@end

在Appdelegate中注册,移除 input source

-(NSMutableArray *)sourcesToPing{
    if (!_sourcesToPing) {
        _sourcesToPing = @[].mutableCopy;
    }
    return _sourcesToPing;
}

- (void)registerSource:(RunLoopContext*)sourceInfo{

    [self.sourcesToPing addObject:sourceInfo];

}

- (void)removeSource:(RunLoopContext*)sourceInfo{
    id  objToRemove = nil;
    for (RunLoopContext* context in self.sourcesToPing){
        if ([context isEqual:sourceInfo]){
            objToRemove = context;

            break;

        }

    }
    if(objToRemove)

        [self.sourcesToPing removeObject:objToRemove];
}

准备工作做好后,调用添加和移除source,代码如下

- (void)viewDidLoad {

    [super viewDidLoad];

    NSThread *thread = [[NSThread alloc] initWithTarget:self selector: @selector(timerThreadAction) object:**nil**];

    thread.name = @"haocai";

    [thread start];

    NSLog(@"[开启了一个新线程 : %@]",thread);
}

#pragma mark **- 触摸事件**

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    NSLog(@"来了,老弟!!!");//这里断点观察触摸事件是由source0触发,并观察堆栈
}

- (void)timerThreadAction {

    NSRunLoop *runloop = [NSRunLoop currentRunLoop];

    RunLoopSource *source =[[RunLoopSource alloc] init];

    //**MARK:这里会将一个source0加入到子线程中,同时子线程会触发RunLoopSourceScheduleRoutine回调,回调中会将source0赋值给一个上下文,并存入HCRunLoopDelegate中**

    [source addToCurrentRunLoop];

    do {

        [runloop runMode:NSDefaultRunLoopMode

              beforeDate:[NSDate distantFuture]];

    } while (YES);

}

- (IBAction)doSomething:(id)sender {

    //主线程通知子线程做事 - 这个步骤是可以重复发送的,例如需要在子线程中写入数据

    AppDelegate *del = (AppDelegate *)[UIApplication sharedApplication].delegate;

    NSMutableArray *rlsources = del.sourcesToPing;

    //这里取出我们需要操作的context,因为只有一个,所以lastobject

    id obj = rlsources.lastObject;

    if ([obj isKindOfClass:[RunLoopContext class]]) {

        RunLoopContext *context = (RunLoopContext*)obj;

        //取出source0

        RunLoopSource *source = context.source;

        //给source0添加一个动作command

        [source addCommand:0 withData:@"customCommand"];

        //source0执行所有命令

        [source fireAllCommandsOnRunLoop:context.runLoop];

    }else {

        NSLog(@"obj error");

    }

}

- (IBAction)endSource0:(id)sender {

    AppDelegate *del = (AppDelegate *)[UIApplication sharedApplication].delegate;

    NSMutableArray *rlsources = del.sourcesToPing;

    id obj = rlsources.lastObject;

    if ([obj isKindOfClass:[RunLoopContext class]]) {

        RunLoopContext *context = (RunLoopContext*)obj;

        RunLoopSource *source = context.source;

        //这里source0从子线程中删除,并停止了runloop

        [source invalidate:context.runLoop];

    }else {

        NSLog(@"obj error");

    }

}

配置定时器

方式一 NSTimer

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];

方式二 CFRunLoopTimerRef

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的sources(source1)

Cocoa 和 Core Foundation 都为线程之间或进程之间的通信提供了基于Port的对象,依次如下,

NSMachPort

以下代码展示了用于启动辅助工作线程的主线程代码,通知与线程中有使用,可混合食用效果更佳.

- (void)launchThread{

    //1. 创建主线程的port
    // 子线程通过此端口发送消息给主线程
    NSPort* myPort = [NSMachPort port];

    if (myPort)
    {
        // This class handles incoming port messages.
//2. 设置port的代理回调对象
        [myPort setDelegate:self];
        
        // Install the port as an input source on the current run loop.
//3. 把port加入runloop,接收port消息
        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

        // Detach the thread. Let the worker release the port.
//4. 启动次线程,并传入主线程的port
        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:)

               toTarget:[MyWorkerClass class] withObject:myPort];

    }
}

  #define kMsg1 100
  #define kMsg2 101
 - (void)handlePortMessage:(NSMessagePort*)message{

 NSLog(@"接到子线程传递的消息!%@",message);

 //1. 消息id
 NSUInteger msgId = [[message valueForKeyPath:@"msgid"] integerValue];

 //2. 当前主线程的port
 NSPort *localPort = [message valueForKeyPath:@"localPort"];

  //3. 接收到消息的port(来自其他线程)
  NSPort *remotePort = [message valueForKeyPath:@"remotePort"];

  if (msgId == kMsg1)
  {
  //向子线的port发送消息
  [remotePort sendBeforeDate:[NSDate date]
  msgid:kMsg2
  components:nil
  from:localPort
  reserved:0];

 } else if (msgId == kMsg2){
 NSLog(@"操作2....\n");
 }
 }
  • MyWorkerClass
#import "MyWorkerClass.h"
@interface MyWorkerClass() <NSMachPortDelegate> {

    NSPort *remotePort;

    NSPort *myPort;

}

@end

#define kMsg1 100

#define kMsg2 101

@implementation** MyWorkerClass

- (void)launchThreadWithPort:(NSPort *)port {
         
         @autoreleasepool {

        //1. 保存主线程传入的port

        remotePort = port;

        //2. 设置子线程名字

        [[NSThread currentThread] setName:@"MyWorkerClassThread"];

        //3. 开启runloop

        [[NSRunLoop currentRunLoop] run];

        //4. 创建自己port

        myPort = [NSPort port];
        //5.
        myPort.delegate = **self**;

        //6. 将自己的port添加到runloop

        //作用1、防止runloop执行完毕之后推出

        //作用2、接收主线程发送过来的port消息

        [[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

        //7. 完成向主线程port发送消息

        [self sendPortMessage];
    }
} 

/**
 *   完成向主线程发送port消息
 */

- (**void**)sendPortMessage {

    NSMutableArray *array  =[[NSMutableArray alloc]initWithArray:@[@"1",@"2"]];

    //发送消息到主线程,操作1

    [remotePort sendBeforeDate:[NSDate date]
                         msgid:kMsg1
                   components:array
                          from:myPort
                     reserved:0];

    //发送消息到主线程,操作2
    //    [remotePort sendBeforeDate:[NSDate date]
    //                         msgid:kMsg2
    //                    components:nil
    //                          from:myPort
    //                      reserved:0];

}

#pragma mark **- NSPortDelegate**

/**
 *  接收到主线程port消息
 */
- (void)handlePortMessage:(NSPortMessage *)message{
    NSLog(@"接收到父线程的消息...\n");

}

NSMessagePort

要与 NSMessagePort 对象建立本地连接,不能简单地在线程之间传递端口对象。 远程消息端口必须按名称获取。 在 Cocoa 中实现这一点需要使用特定名称注册本地端口,然后将该名称传递给远程线程,以便它可以获得适当的端口对象进行通信。 如下代码显示了要使用消息端口的情况下的端口创建和注册过程

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];

CFMessagePortRef

使用 Core Foundation 在应用程序的主线程和工作线程之间设置双向通信通道

//将 Core Foundation 消息端口附加到新线程

#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;

配置了主线程后,剩下的唯一事情就是让新创建的工作线程创建自己的端口并签入。以下代码展示了工作线程的入口点函数。 该函数提取主线程的端口名称并使用它来创建回到主线程的远程连接。 然后该函数为自己创建一个本地端口,将该端口安装到线程的运行循环中,并向主线程发送一个包含本地端口名称的签入消息。

//设置线程结构
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();

}

一旦它进入它的运行循环,所有未来发送到线程端口的事件都由 ProcessClientRequest 函数处理。