iOS开发-Runloop中自定义输入源Source

自定义输入源

根据苹果官方文档说明,基于端口的源(Source1)由内核自动发出信号,定制源(Source0)必须从另一个线程手动发出信号

定制源Source0

创建custom input source需要定义以下内容:

  • 您希望输入源处理的信息。
  • 一个调度函数(scheduler),让感兴趣的客户端知道如何联系输入源。
  • 执行任何客户机发送的请求的处理函数(handler)。
  • 用于使输入源无效的取消函数(cancellation)。

在本例中,应用程序的Main Thread维护对Input Source的引用、该Input Source的自定义命令缓冲区以及安装输入源的运行循环。当Main Thread有一个任务要传递给Worker Thread时,它会向Command Buffer发送一个命令以及Worker Thread启动任务所需的任何信息。(因为Worker ThreadInput SourceMain Thread都可以访问Command Buffer,所以必须同步访问。)一旦发出命令,Main Thread就向Input Source发出信号,并唤醒Worker ThreadRunloop。在接收到wake up命令后,Runloop调用Input Source的处理程序,该处理程序处理在Command Buffer中找到的命令。

iOS开发-Runloop中自定义输入源Source_第1张图片
可以在主线程直接通过激活子线程的souce0,在子线程做一些事情。

附上demo https://pan.baidu.com/s/10Ao5VJgnHwC2oJSAP_5WbQ 密码:xgta

下面讲解如何实现:

自定义输入源需要使用Core Foundation来配置Runloop源并将其附加到Runloop。尽管基本的处理程序是基于C的函数,但这并不妨碍您为这些函数编写包装器,并使用Objective-C或c++实现代码主体。

@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

回调函数

source0加入回调

RunLoopSourceScheduleRoutine回调会在你把自定义Source0加入到Runloop时,就进行回调。

我们会将source0封装到一个Context中后注册到中央代理(HCRunLoopDelegate)中。当某一方需要通过该source0触发操作时,他可以通过中央代理通信,使用Context对象中的source0来进行通信。

void RunLoopSourceScheduleRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
     
    NSLog(@"RunLoopSourceScheduleRoutine 对象地址 %p",info);
    RunLoopSource* obj = (__bridge RunLoopSource*)(info);
    HCRunLoopDelegate*   del = [HCRunLoopDelegate sharedDelegate];
    //这里将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];
}

执行回调

当某一方需要通过该source0触发操作时,他可以通过中央代理通信

- (IBAction)doSomething:(id)sender {
     
    //主线程通知子线程做事 - 这个步骤是可以重复发送的,例如需要在子线程中写入数据
    NSMutableArray *rlsources = [HCRunLoopDelegate sharedDelegate].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");
    }
}

最重要的回调之一RunLoopSourcePerformRoutine(void *info),当触发CFRunLoopSourceSignal时会回调

- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop
{
     
    //触发执行source0,会触发RunLoopSourcePerformRoutine
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(runloop);
}

回调函数RunLoopSourcePerformRoutine(void *info)内容如下

void RunLoopSourcePerformRoutine (void *info)
{
     
    NSLog(@"RunLoopSourcePerformRoutine 对象地址 %p",info);
    RunLoopSource*  obj = (__bridge RunLoopSource*)(info);
    NSLog(@"RunLoopSourcePerformRoutine 对象引用计数 %@",[obj valueForKey:@"retainCount"]);
    [obj sourceFired];
    obj = nil;
}

移除回调

如果您使用CFRunLoopSourceInvalidate函数Input Sources从其Runloop中删除,系统将调用Input Sources的取消回调。您可以使用这个回调通知客户端(使唤中央代理的一方),无法再使用该source0,同时需要中央代理移除source0的引用。

void RunLoopSourceCancelRoutine (void *info, CFRunLoopRef rl, CFStringRef mode)
{
     
    NSLog(@"RunLoopSourceCancelRoutine 对象地址 %p",info);
    RunLoopSource* obj = (__bridge RunLoopSource*)(info);
    HCRunLoopDelegate* del = [HCRunLoopDelegate sharedDelegate];
    RunLoopContext* theContext = [[RunLoopContext alloc] initWithSource:obj andLoop:rl];
    //移除引用
    [del performSelectorOnMainThread:@selector(removeSource:)
                          withObject:theContext waitUntilDone:YES];
}

触摸事件的联想

这里我断点调试了RunLoopSourcePerformRoutine()的调用堆栈

(lldb) bt
* thread #8, name = 'haocai', stop reason = breakpoint 2.1
  * frame #0: 0x00000001005dde84 RunloopSourceCode`-[RunLoopSource sourceFired](self=0x0000000281b1ece0, _cmd="sourceFired") at RunLoopSource.m:75:5
    frame #1: 0x00000001005dda00 RunloopSourceCode`RunLoopSourcePerformRoutine(info=0x0000000281b1ece0) at RunLoopSource.m:20:5
    frame #2: 0x00000001be824f1c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #3: 0x00000001be824e9c CoreFoundation`__CFRunLoopDoSource0 + 88
    frame #4: 0x00000001be824784 CoreFoundation`__CFRunLoopDoSources0 + 176
    frame #5: 0x00000001be81f6c0 CoreFoundation`__CFRunLoopRun + 1004
    frame #6: 0x00000001be81efb4 CoreFoundation`CFRunLoopRunSpecific + 436
    frame #7: 0x00000001bf1ed95c Foundation`-[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 300
    frame #8: 0x00000001005dcba0 RunloopSourceCode`-[ViewController timerThreadAction](self=0x0000000100c0c3e0, _cmd="timerThreadAction") at ViewController.m:63:9
    frame #9: 0x00000001bf31a4a0 Foundation`__NSThread__start__ + 984
    frame #10: 0x00000001be4b12c0 libsystem_pthread.dylib`_pthread_body + 128
    frame #11: 0x00000001be4b1220 libsystem_pthread.dylib`_pthread_start + 44
    frame #12: 0x00000001be4b4cdc libsystem_pthread.dylib`thread_start + 4

同时也断点了touchBegan方法来打印触摸事件堆栈

#pragma mark - 触摸事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
     
    NSLog(@"来了,老弟!!!");//这里断点观察触摸事件是由source0触发,并观察堆栈
}

堆栈如下

* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001005dca8c RunloopSourceCode`-[ViewController touchesBegan:withEvent:](self=0x0000000100c0c3e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000000282900750) at ViewController.m:54:5
    frame #1: 0x00000001eaef0f9c UIKitCore`forwardTouchMethod + 336
    frame #2: 0x00000001eaef0e38 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 60
    frame #3: 0x00000001eaeff3b8 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1584
    frame #4: 0x00000001eaf007ec UIKitCore`-[UIWindow sendEvent:] + 3140
    frame #5: 0x00000001eaee085c UIKitCore`-[UIApplication sendEvent:] + 340
    frame #6: 0x00000001eafa69d4 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 1768
    frame #7: 0x00000001eafa9100 UIKitCore`__handleEventQueueInternal + 4828
    frame #8: 0x00000001eafa2330 UIKitCore`__handleHIDEventFetcherDrain + 152
    frame #9: 0x00000001be824f1c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #10: 0x00000001be824e9c CoreFoundation`__CFRunLoopDoSource0 + 88
    frame #11: 0x00000001be824784 CoreFoundation`__CFRunLoopDoSources0 + 176
    frame #12: 0x00000001be81f6c0 CoreFoundation`__CFRunLoopRun + 1004
    frame #13: 0x00000001be81efb4 CoreFoundation`CFRunLoopRunSpecific + 436
    frame #14: 0x00000001c0a2079c GraphicsServices`GSEventRunModal + 104
    frame #15: 0x00000001eaec6c38 UIKitCore`UIApplicationMain + 212
    frame #16: 0x00000001005dd064 RunloopSourceCode`main(argc=1, argv=0x000000016f8278c8) at main.m:14:16
    frame #17: 0x00000001be2e28e0 libdyld.dylib`start + 4

是不是有相似的地方,触摸事件是Main runloop,由UIApplicationMain(mian.m文件中)函数开启

    frame #2: 0x00000001be824f1c CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
    frame #3: 0x00000001be824e9c CoreFoundation`__CFRunLoopDoSource0 + 88
    frame #4: 0x00000001be824784 CoreFoundation`__CFRunLoopDoSources0 + 176
    frame #5: 0x00000001be81f6c0 CoreFoundation`__CFRunLoopRun + 1004
    frame #6: 0x00000001be81efb4 CoreFoundation`CFRunLoopRunSpecific + 436

即发现触摸事件也是有source0触发,不过有些区别,直接会回调到了UIKit中的函数


与输入源的客户端的协调

要使得Input Source有效,你需要操作它并从另一个线程发出信号。Input Source输入源的意义在于将其关联的线程休眠,直到有事情要做为止。这事实上需要让应用程序中的其他线程知道输入源,并有方法与它通信。

通知客户端关于Input Source的一种方法是,当您的Input Source首次安装到其Runloop中时,发送注册请求。您可以向任意数量的客户注册您的Input Source,或者您可以简单地向某个中央代理(这里指的是HCRunLoopDelegate单例)注册它,然后将您的Input Source交付给感兴趣的客户。

这里我参考自己的理解做了一个demo,代码和官方文档相似,参考这里,百度网盘
附上链接:https://pan.baidu.com/s/10Ao5VJgnHwC2oJSAP_5WbQ 密码:xgta

demo能够实现将RunloopContext交互给外面对象持有,外面的对象可以通过RunloopContext对添加进Runloop的source0发送消息,且消息的执行永远在持有source0Runloop的那个线程中。也就能实现线程的转换,主要是子线程能够休眠,被主线程唤醒。


基于端口的Source1

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

在Core Foundation中,必须手动创建端口及其运行循环源。在这两种情况下,都使用与端口不透明类型(CFMachPortRefCFMessagePortRefCFSocketRef)相关联的函数来创建适当的对象。

按照官方文档的写法,这里iOS并没有NSPortMessage这个类,只有Mac os下有,iOS中也只能使用NSMachPort
在这里插入图片描述
贴下Mac os下头文件

/*	NSPortMessage.h
	Copyright (c) 1994-2018, Apple Inc. All rights reserved.
*/

#import 

@class NSPort, NSDate, NSArray, NSMutableArray;

NS_ASSUME_NONNULL_BEGIN

@interface NSPortMessage : NSObject {
     
    @private
    NSPort 		*localPort;
    NSPort 		*remotePort;
    NSMutableArray 	*components;
    uint32_t		msgid;
    void		*reserved2;
    void		*reserved;
}

- (instancetype)initWithSendPort:(nullable NSPort *)sendPort receivePort:(nullable NSPort *)replyPort components:(nullable NSArray *)components NS_DESIGNATED_INITIALIZER;

@property (nullable, readonly, copy) NSArray *components;
@property (nullable, readonly, retain) NSPort *receivePort;
@property (nullable, readonly, retain) NSPort *sendPort;
- (BOOL)sendBeforeDate:(NSDate *)date;

@property uint32_t msgid;

@end

NS_ASSUME_NONNULL_END

这里贴出iOS的NSMachPort的使用,转线程发送通知


https://github.com/lizelu/NotificationWithSubThread
Runloop Source0官方文档

你可能感兴趣的:(iOS开发,Runloop,source0,source1,自定义输入源)