Run Loops 阅读笔记

run loop是一个事件处理循环,可以向其添加自己的事件也可以接受系统的事件。run loop在没有事件的时候让线程睡眠。
每个线程都有自己的run loop,所以不需要自己创建,但是非主线程的run loop需要手动run起来。
run loop接受两种类型的源,input source和timer source。
run loop会发送通知,通过注册观察这些通知可以了解run loop的行为。需要使用Core Foundation的接口来注册这些通知。

Run Loop Modes

Run loop 的 mode就是source和观察者的分组。在run的时候,用那种mode run,只有这个mode相关联的source才被监控,也只有这个mode关联的observer才会收到run loop的通知。其他的source会被hold on。系统提供了默认的几个mode,也可以定义自己的mode。

系统定义的Mode
Default,一般模式;
Modal,模态对话框使用的模式,仅仅接受对话框的source
Event Tracking,鼠标移动等跟踪任务(ScrollView的滚动是不是?)
Common models,这是一个模式组合,包含了Default,Modal和Event Tracking三种模式。亦可以向其中添加自定义的模式。
Connection这个是和NSConnection一起用的,用于监控回复,用户一般不会用到,文档也没详细写。

Input Sources

input source有两种类型,它们的处理方式是一致的,不同的是port-based input source是系统内核发出的,而custom input source是用户从另外的线程或者进程发出的。在创建一个input source的时候同样要指定一个或者多个run loop的mode。

port-based sources,在Cocoa中你不需要创建一个port-based的input source,而是将NSPort对象直接添加到run loop。创建的过程被封装起来了。但是如果使用Core Foundation 的API,就需要手动创建port和input source了。

custom input source,创建custom input source必须使用Core Foundation层的API了。基本的设计是通过一些列回调函数处理事件的。

Cocoa已经封装好了一个custom input source, 叫做Cocoa Perform Selector Source。也就是常见的Perform系列函数:
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

Timer Sources

Timer source是线程提醒自己将来做一件事。timer的时间准确性是非实时的,如果在时间到了的时候run loop不在timer的mode上,就不会处理timer的事件,或者在timer的mode上,但是上一个run loop很耗时,还没有执行完毕,也会delay事件的执行。但是一个repeat的timer有自我纠正机制,即,如果第一个timer事件被delay了,那么第二个不受影响,如果没有引起delay的事件,会恢复准确性。但是如果第一个事件被delay超过一个循环,事件将发生合并,类似于“掉帧”的情况。

Run Loop Observers

Run loop的如下的时间点可以被监控:
The entrance to the run loop.
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.

Configuring the Run Loop

Run Loop Object在Cocoa中是NSRunLoop,在Core Foundation中是CFRunLoopRef,提供了加入input source,timer和注册Run Loop观察者的接口。每个线程都有一个唯一的run loop object,使用currentRunLoop或者CFRunLoopGetCurrent来获取。
在运行Run Loop之前至少往其中添加一个input source或者timer,否则Run Loop会立即退出。为了保证一个run loop“活着”,添加一个repeat的timer会不停的唤起线程,不如添加一个input source节省资源。
Apple的官方文档有示例代码:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW44

线程安全
如果使用Core Foundation的API操作Run Loop,是有线程安全保障的,可以在任何线程中调用。
如果使用的是Cocoa的API,不能保证线程安全。如果要修改Run Loop的配置,只能在那个run loop的线程中做。如果添加的input source或者timer是属于其他线程的,会引起Crash。

Configuring Custom Input Sources

1.Main Thread创建Worker Thread并安装好input source。创建custom input source使用CFRunLoopSourceCreate函数;将input source添加到run loop使用CFRunLoopAddSource函数;
2.Main Thread唤起Worker Thread的Run Loop。唤醒run loop使用CGRunLoopWakeUp函数;
3.Main Thread将要处理的任务放入缓冲区(需要线程同步,保护缓冲区)
4.Main Thread向input source发送信号,input source收到信号后激活Worker Thread的Run Loop处理任务。向input source发送信号使用CFRunLoopSignal函数;

void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode){
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"RunLoopSourceScheduleRoutine");
    });
}
void RunLoopSourcePerformRoutine (void *info){
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"RunLoopSourcePerformRoutine");
    });
}
void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode){
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"RunLoopSourceCancelRoutine");
    });
}
-(void)installSource{
    __weak typeof(self) weakSelf = self;
    self.thread = [[NSThread alloc] initWithBlock:^{
        CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
            RunLoopSourceScheduleRoutine,
            RunLoopSourceCancelRoutine,
            RunLoopSourcePerformRoutine};

        CFRunLoopSourceRef runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
        CFRunLoopRef runloop = CFRunLoopGetCurrent();

        CFRunLoopAddSource(runloop, runLoopSource, kCFRunLoopDefaultMode);
        weakSelf.source = runLoopSource;
        weakSelf.runloop = runloop;

        BOOL done = NO;
        do{
            CFRunLoopRunResult res = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 5, NO);
            if(res == kCFRunLoopRunStopped || res == kCFRunLoopRunFinished){
                done = YES;
            }
        }while (!done);
    }];

    [self.thread start];
}

1.自定义的input source不能使用GCD创建的thread,因为CGD背后是一个线程池。需要自定义线程。
2.CFRunLoopAddSource添加了input source之后,do while循环中的CFRunLoopRunInMode返回值就不会是Stop或者Finish,而是timeout(如果没有事件被处理的话),这样线程就不会退出。
3.自己创建的线程需要调用start开启。

-(void)signal{
    if(self.source && self.runloop){
        CFRunLoopSourceSignal(self.source);
        CFRunLoopWakeUp(self.runloop);
    }
}

signal是发送信号的代码,这里仅仅是发送信号,并没带数据。如果希望带上数据,应该通过CFRunLoopSourceContext的第二个参数带过去。在回调函数变RunLoopSourceScheduleRoutine的第一个参数就是这个info。

-(void)removeSource{
    if(self.source && self.runloop){
        CFRunLoopRemoveSource(self.runloop, self.source, kCFRunLoopDefaultMode);
        CFRelease(self.source);
        self.source = NULL;
    }
}

Configuring Timer Sources

创建timer source比较简单,创建一个timer并添加 到runloop上即可。具体参数看下API就行。

Configuring Port-Based Input Sources

在iOS上使用Port-Based的input source,最好还是使用CoreFoundation的API,因为Cocoa中有写对象和方法不支持iOS。如果是在MacOS上创建一个port对象加入到run loop中,将这个port交给其他线程使用。
CoreFoundation 代码示例:

-(void)launchThread{
    CFStringRef mainThreadPortName;
    CFMessagePortRef mainThreadPort;
    CFRunLoopSourceRef mainThreadSource;
    CFMessagePortContext mainThreadContext = {0, NULL, NULL, NULL, NULL};
    Boolean shouldFreeInfo;

    mainThreadPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.myapp.MainThread"));
    mainThreadPort = CFMessagePortCreateLocal(NULL, mainThreadPortName, &MainThreadHandler, &mainThreadContext, &shouldFreeInfo);

    CFRelease(mainThreadPortName);
    if(mainThreadPort != NULL){
        mainThreadSource = CFMessagePortCreateRunLoopSource(NULL, mainThreadPort, 0);
        if(mainThreadSource){
            CFRunLoopAddSource(CFRunLoopGetCurrent(), mainThreadSource, kCFRunLoopDefaultMode);
            CFRelease(mainThreadSource);
        };
        CFRelease(mainThreadPort);
    }

    //子线程:
    [NSThread detachNewThreadWithBlock:^{
        CFStringRef workerPortName = CFStringCreateWithFormat(NULL, NULL, CFSTR("com.MyApp.Worker"));
        CFMessagePortContext workerContext = {0, mainThreadPort, NULL, NULL, NULL};
        Boolean shouldFreeInfo;

        CFMessagePortRef workerPort = CFMessagePortCreateLocal(NULL, workerPortName, &ProcessClientRequest, &workerContext, &shouldFreeInfo);
        if (shouldFreeInfo){
            CFRelease(workerPortName);
            return;
        }

        CFRunLoopSourceRef workerSource = CFMessagePortCreateRunLoopSource(NULL, workerPort, 0);
        if (!workerSource){
            CFRelease(workerPortName);
            CFRelease(workerPort);
            return;
        }

        CFRunLoopAddSource(CFRunLoopGetCurrent(), workerSource, kCFRunLoopDefaultMode);
        CFRelease(workerPort);
        CFRelease(workerSource);

        // Package up the port name and send the check-in message.
//        CFDataRef returnData = nil;
        CFDataRef outData;
        CFIndex stringLength = CFStringGetLength(workerPortName);
        UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);

        CFStringGetBytes(workerPortName,
                         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);
        CFRelease(workerPortName);

        // Enter the run loop.
        CFRunLoopRun();
    }];
}

创建port,通过port创建source,然后添加到当前的run loop中。
创建线程,通过创建线程的参数将port的名字或者port对象本身传过去,这里利用了block的捕获机制传了过去。工作线程创建自己的port,然后添加到run loop。并向主线程的port发送自己的port的名字。
主线程的handler拿到名字之后,获取工作线程的port然后持有用于以后通信。

CFDataRef MainThreadHandler(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, kCFStringEncodingUTF8, FALSE);

        messagePort = CFMessagePortCreateRemote(NULL, threadPortName);
        if(messagePort){
            g_workerPort = messagePort;
//            CFRelease(messagePort);
        }

        CFRelease(threadPortName);
        CFAllocatorDeallocate(NULL, buffer);
    }else{
        // Process other messages.
    }

    return NULL;
}

主线程向工作线程发送信息的代码:

- (IBAction)addTask:(UIButton *)sender {
    CFStringRef dataStr = CFStringCreateWithFormat(NULL, NULL, CFSTR("hello"));
    CFDataRef outData;
    CFIndex stringLength = CFStringGetLength(dataStr);
    UInt8* buffer = CFAllocatorAllocate(NULL, stringLength, 0);

    CFStringGetBytes(dataStr,
                     CFRangeMake(0,stringLength),
                     kCFStringEncodingUTF8,
                     0,
                     FALSE,
                     buffer,
                     stringLength,
                     NULL);

    outData = CFDataCreate(NULL, buffer, stringLength);

    CFMessagePortSendRequest(g_workerPort, 1234, outData, 0.1, 0.0, NULL, NULL);

    // Clean up thread data structures.
    CFRelease(outData);
    CFRelease(dataStr);
    CFAllocatorDeallocate(NULL, buffer);
}

之后工作线程的回调函数会收到消息。

猜你喜欢

转载自blog.csdn.net/Q52077987/article/details/80608726