iOS多线程开发---Run Loop

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS多线程开发---Run Loop相关的知识,希望对你有一定的参考价值。

 

 

四,配置Run Loop源---配置源的过程就是源的创建调用过程
配置过程分为以下几个阶段---定义/创建(一个源)---安装(将输入源安装到所在Run Loop中)---注册(将输入源注册到客户端,协调输入源的客户端)---调用(通知输入源,开始工作)
4-1,定义自定义输入源
创建自定义输入源需要定义以下内容
1)输入源要处理的信息
2)使感兴趣的客户端知道如何和输入源交互的调度例程
3)处理其他任何客户发送请求的例程
4)使输入源失效的取消例程
Figure 3-2 Operating a custom input source
技术分享
上图的处理流程:主线程(Main Thread)发起任务(Task)给工作线程(Worker Thread),主线程会给命令缓冲区(send command-->Command Buffer),通知输入源(signal source-->Input Source),并唤醒工作线程(Wake Up-->Worker Thread)。工作线程收到唤醒命令,Run Loop会调用输入源的处理程序,由它来执行命令缓冲区中相应的命令。
注:因为主线程和输入源所在工作线程都可以访问命令缓冲区(Command Buffer),因此这些访问必须使同步的
1)定义输入源(The custom input source object definition
下面代码中,定义了RunLoopSource对象,它管理命令缓冲区,并以此来接收其他线程的消息。RunLoopContext对象是一个用来传递RunLoopSource对象(RunLoopSource* source)和“run loop引用”(CFRunLoopRef runLoop)给程序主线程的一个容器
======
// Listing 3-3 The custom input source object definition

@interface RunLoopSource : NSObject

{

    CFRunLoopSourceRef runLoopSource;

    NSMutableArray* commands;

}

 

- (id)init;

- (void)addToCurrentRunLoop;

- (void)invalidate;

 

// handler method

- (void)sourecFired;

// 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

===当将输入源附加到run loop时,调用这个协调调度例程,将源注册到客户端(可以理解为其他线程)

// Listing 3-4 Scheduling a run loop source

//当source添加进runloop的时候,调用此回调方法 <== CFRunLoopAddSource(runLoop, source, mode);

 

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

}

===在输入源被告知(signal source)时,调用这个处理例程,这儿只是简单的调用了 [obj sourceFired]方法

 

// Listing 3-5 Performing work in the input source 

//当sourcer接收到消息的时候,调用此回调方法(CFRunLoopSourceSignal(source);CFRunLoopWakeUp(runLoop);

void RunLoopSourcePerformRoutine (void *info)

{

    RunLoopSource* obj = (RunLoopSource*)info;

    [obj sourceFired];

}

===如果使用CFRunLoopSourceInvalidate/CFRunLoopRemoveSource函数把输入源从run loop里面移除的话,系统会调用这个取消例程,并且把输入源从注册的客户端(可以理解为其他线程)里面移除

 

// Listing 3-6 Invalidating an input source == 

//当source 从runloop里删除的时候,调用此回调方法 <== CFRunLoopRemoveSource(runLoop, source, mode); 

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

}

2)安装输入源到Run Loop---分两步首先初始化一个输入源,然后将这个输入源添加到当前Run Loop里面

 

// List 3-7 Installing the run loop source

 

 

- (id)init

{

    /*

     // Setup the context.

     context.version = 0;

     context.info = self;

     context.retain = NULL;

     context.release = NULL;

     context.copyDescription = CFCopyDescription;

     context.equal = CFEqual;

     context.hash = CFHash;

     context.schedule = RunLoopSourceScheduleRoutine;

     context.cancel = RunLoopSourceCancelRoutine;

     context.perform = RunLoopSourcePerformRoutine;

     */

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

    //Add the new CFRunLoopSourceRef to the indicated runloop, 并且回调RunLoopSourceScheduleRoutine函数

    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);

}

3)协调输入源的客户端(将输入源注册到客户端)

输入源的主要工作就是将与输入源相关联的线程置于休眠状态,直到有事件发生。要达到这个目的,首先客户端(其他线程)知道有该输入源信息,并且有办法与之通信。

通知客户端关于这个输入源信息的方法之一,就是当该输入源开始安装到你的run loop上面后发送注册请求给相关的客户端,该输入源可以注册到任意数量的客户端

也可以通过由代理将输入源注册到感兴趣的客户端

===下面显示了应用委托(AppDelegate)定义的注册方法及从应用委托(AppDelegate)中移除的方法

 

- (void)registerSource:(RunLoopContext *)sourceInfo

{

    NSMutableArray *sourceToPing;

    [sourceToPing addObject:sourceInfo];

}

 

- (void)removeSource:(RunLoopContext *)sourceInfo

{

    id objToRemove = nil;

    

    NSMutableArray *sourceToPing;

    

    for(RunLoopContext *context in sourceToPing)

    {

        if([context isEqual:sourceInfo])

        {

            objToRemove = context;

            break;

        }

    }

    

    if(objToRemove)

    {

        [sourceToPing removeObject:objToRemove];

    }

}

注:上面两个函数分别在RunLoopSourceScheduleRoutine/RunLoopSourceCancelRoutine函数中被调用

4)通知输入源---客户端(其他线程)发数据到输入源,分两步首先发信号给输入源(signal source),然后唤醒输入源的run loop

===下面显示了客户端发送数据到输入源的方法,在本例中这个方法被放在RunLoopSource对象里面

 

// Listing 3-9 Waking up the run loop

- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runLoop

{

    //当手动调用此方法的时候,将会触发 RunLoopSourceContext的performCallback

    CFRunLoopSourceSignal(runLoopSource);

    CFRunLoopWakeUp(runLoop);

}

注:1,输入源就是一类事件(命令)处理机制。他是线程间的事件(命令)异步通讯机制,所以不能试图通过这个机制实现进程间的通讯

2,因为CFRunLoopWakeUp函数不是信号安全的,所以对run loop的唤醒,不能在应用信号处理例程(RunLoopSourcePerformRoutine)里面使用。

 

4-2,配置定时源

配置源的过程其实是源在相关run loop的使用过程,包括定义(创建),安装(添加到相关run loop),注册(注册到其他需要交互的线程),以及使用四个过程。但是因为定时源在预设的时间点同步方式传递消息,是线程通知自己做某事情的一种方法,所以配置定时源,主要工作是创建,和安装。其他两个过程run loop自己就可以完成。

创建定时源有两种方式

===一种是使用NSTimer对象创建,一种是使用CFRunLoopTimerRef对象创建,及将创建的事例安装到Run Loop中

 

// Listing 3-10 Creating and scheduling timers using NSTimer

- (void)createAndScheduleTimerToRunLoopUsingNSTimer

{

    // get the current run loop

    NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];

    

    // create and schedule the first timer  === 可以配置到不同的run loop模式

    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 === 只能在run loop的NSDefaultRunLoopMode下有效

    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(myDoFireTimer2:) userInfo:nil repeats:YES];

}

 

 

// Listing 3-11 Creating and scheduling a timer using Core Foundation

- (void)createAndScheduleTimerToRunLoopUsingCFRunLoopTimerRef

{

    // get the current run loop

    CFRunLoopRef runLoop = CFRunLoopGetCurrent();

    CFRunLoopTimerContext context = {0, NULL, NULL, NULL, NULL};

    CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 0.1, 0.3, 0, 0, &myCFTimerCallBack, &context);

    

    // add the CFRunLoopTimerRef to run loop kCFRunLoopCommonModes mode

    CFRunLoopAddTimer(runLoop, timer, kCFRunLoopCommonModes);

}

 

4-2,配置基于端口的输入源

ios系统提供了基于端口的输入源对象,用以线程或进程间通讯

1)配置NSMachPort对象---本地线程间通信,通过传递端口对象变量进行端口间通讯

基本机制:A线程(父线程)创建NSMachPort对象,并加入A线程的run loop。当创建B线程(辅助线程)时,将创建的NSMachPort对象传递到主体入口点,B线程(辅助线程)就可以使用相同的端口对象将消息传回A线程(父线程)。

A,实现A线程(父线程)的代码

===在下面代码中,创建一个NSPort对象,并把这个端口对象安装(add)到线程的Run Loop里面,同时创建一个新的线程,并把NSPort对象作为参数传入到新线程的主体入口点

 

// Listing 3-12 Main thread launch sub-thread method

- (void)lanchThread

{

    NSPort *myPort = [NSPort port];

    if(myPort)

    {

        // This class handles incoming port message

        [myPort setDelegate:self];

        

        // install the port as an input source

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

        

        // Detach the thread.Let the worker release the port--加载一个子线程

        [NSThread detachNewThreadSelector:@selector(LaunchThreadWithPort:) toTarget:[MyWorkerThread Class] withObject:myPort];

    }

}

===NSPort是通过代理模式传送消息,下面这段代码是NSPortDelegate的代理方法,用以从B线程回传消息((NSPortMessage *)message)

 

#pragma mark

#define kCheckinMessage     100

#pragma mark NSPortDelegate Method

// This is the NSPort Delegate Method

// It handle responses from the worker thread(B线程)

- (void)handlePortMessage:(NSPortMessage *)message

{

    uint32_t mssgId = [message msgid];

    NSPort *distantPort = nil;

    

    if(message == kCheckinMessage)

    {

        // get the worker thread‘s communication port

        distantPort = [message sendPort];

        

        // Retain and save the worker port for later use

        [self storeDistantPort:distantPort];

    }

    else

    {

        // Handle other message

        ......

    }

}

B,B线程(辅助线程)的实现代码

===下面是辅助线程的类方法,里面创建了辅助线程实例,并把传入的NSPort保存下来。同时,通过NSMachPort发送一个Check-In Message(NSPortMessage类型,在这个Message类型中设置了SendPort/ReceivePort,链接这两个端口)给A线程。

 

// Listing 3-14 Launching the worker thread using Mach ports

+ (void)LaunchThreadWithPort:(id)inData

{

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

 

    // set up the connection between this thread and the main thread

    NSPort *distantPort = (NSPort *)inData;

  

    MyWorkerThread *workObj = [[self alloc] init];

    [workObj sendCheckinMessage:distantPort];

    [distantPort release];

 

    // Let the run loop process things

    do

    {

        

    }while (![workerObj shouldExit]);

    

    [workObj release];

    [pool release];

}

 

 

// Listing 3-15 Sending the check-in message using Mach ports

 

 

- (void)sendCheckinMessage:(NSPort *)outPort

{

    // Retain and save the remote port for future use

    [self setRemotePort:outPort]; // set NSPort *remotePort;

    

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

    }

}

===注:最终,B线程NSMachPort)端口传递消息给A线程的(NSPort)端口(handlePortMessage代理方法)。

 

2)配置NSMessagePort对象

===只能在一个设备内程序间通信,不能在不同设备间通信。将端口名称注册到NSMessagePortNameServer里面,其他线程通过这个端口名称从NSMessagePortNameServer来获取这个端口对象。

 

// Listing 3-9 Waking up the run loop

- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runLoop

{

    //当手动调用此方法的时候,将会触发 RunLoopSourceContext的performCallback

    CFRunLoopSourceSignal(runLoopSource);

    CFRunLoopWakeUp(runLoop);

    

    NSPort *localPort = [[NSMessagePort alloc] init];

    

    // configure the port and add it to the current run loop

    [localPort setDelegate:self];

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

    

    // register the port using the specific name, and The name is unique

    NSString *localPortName = [NSString stringWithFormat:@"MyPortName"];

    // there is only NSMessagePortNameServer in the mac os x system

    //[[NSMessagePortNameServer sharedInstance] registerPort:localPort name:localPortName];

}

3)在Core Fundation中配置基于端口的源

CFMessagePortRef---实现本地设备内程序间通讯。CFMessagePort objects provide a communications channel that can transmit arbitrary data between multiple threads or processes on the local machine.

 

 

You create a local message port with CFMessagePortCreateLocal and make it available to other processes by giving it a name, either when you create it or later with CFMessagePortSetName. Other processes then connect to it using CFMessagePortCreateRemote, specifying the name of the port.

To listen for messages, you need to create a run loop source with CFMessagePortCreateRunLoopSource and add it to a run loop with CFRunLoopAddSource.

Important: If you want to tear down the connection, you must invalidate the port (using CFMessagePortInvalidate) before releasing the runloop source and the message port object.

Your message port’s callback function will be called when a message arrives. To send data, you store the data in a CFData object and call CFMessagePortSendRequest. You can optionally have the function wait for a reply and return the reply in another CFData object.

Message ports only support communication on the local machine. For network communication, you have to use a CFSocket object.

 

Functions by Task Creating a CFMessagePort Object

* CFMessagePortCreateLocal---监听线程创建监听端口,Returns a local CFMessagePort object. * CFMessagePortCreateRemote---工作线程用来获取监听线程的端口对象,Returns a CFMessagePort object connected to a remote port.

Configuring a CFMessagePort Object

* CFMessagePortCreateRunLoopSource * CFMessagePortSetInvalidationCallBack * CFMessagePortSetName 

Using a Message Port

* CFMessagePortInvalidate * CFMessagePortSendRequest---工作线程给监听端口发送请求 

Examining a Message Port

* CFMessagePortGetContext * CFMessagePortGetInvalidationCallBack * CFMessagePortGetName * CFMessagePortIsRemote * CFMessagePortIsValid 

Getting the CFMessagePort Type ID

* CFMessagePortGetTypeID

 

 

 

===主线程代码,创建CFMessagePortRef本地端口作为监听端口,并注册到当前run loop。MainThreadResponseHandler函数是消息回调函数,当有消息从工作线程传递过来时将调用这个函数

 

// Listing 3-17 Attaching a Core Foundation message port to a new thread

#define kThreadStackSize (8 *4096)

 

void MySpawnThread( void )

{

    // 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 for 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

        rlSource = CFMessagePortCreateRunLoopSource(NULL, myPort, 0);

        if(rlSource != NULL)

        {

            // 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

    NSThread *thread = [[NSThread alloc] initWithTarget:[MyThreadClass Class] selector:@selector(ServerThreadEntryPoint:) object:@"com.myapp.MainThread"];

    [thread setStackSize:kThreadStackSize];

    [thread start];

}

 

 

// Listing 3-18 Receiving the checkin me

 

#define kCheckinMessage 100

 

 

CFDataRef MainThreadResponseHandler(CFMessagePortRef local, SInt32 msgid, CFDataRef data, void*info)

{

    if(msgid == kCheckinMessage)

    {

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

#if 0

        CFMessagePortRef messagePort;

        

        // You must obtain a remote message port by name for future reference

        messagePort = CFMessagePortCreateRemote(NULL, threadPortName);

        if(messagePort)

        {

            // Retain and save the thread‘s com port for future reference

            AddPortToListOfActiveThreads(messagePort);

            // Since the port is retained by the previous function, release it here

            CFRelease(messagePort);

        }

#endif

        // clean up

        CFRelease(threadPortName);

        CFAllocatorDeallocate(NULL, buffer);

    }

    else

    {

        // handle other messages

    }

    return NULL;

}

===工作线程代码,获取主线程端口,通过CFMessagePortSendRequest传递消息到主线程端口

 

// Listing 3-19 Setting up the thread structures

+ (void)ServerThreadEntryPoint:(NSString *)name

{

    // create the remote port to main thread

    CFMessagePortRef mainThreadPort;

    CFStringRef portName = (CFStringRef)name;

    mainThreadPort = CFMessagePortCreateRemote(NULL, portName);

    

#if 1

    // Create a port for the worker thread.

    CFStringRef myPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Thread-%d"),CFRunLoopGetTypeID());

    //  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)

    {

        CFRunLoopSourceRef 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);

            

            // 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, &returnData);

            

            // Clean up thread data structures.

            CFRelease(outData);

            CFAllocatorDeallocate(NULL, buffer);

            

            // Enter the run loop.

            CFRunLoopRun();

        }

    

    }

#endif

}

以上是关于iOS多线程开发---Run Loop的主要内容,如果未能解决你的问题,请参考以下文章

深入理解run loop

多线程 Thread 线程同步 synchronized

多线程配合协程

iOS 多线程:『RunLoop』详尽总结

iOS开发多线程--技术方案

IOS-Run loop学习总结