根据苹果官方文档说明,基于端口的源(Source1
)由内核自动发出信号,定制源(Source0
)必须从另一个线程手动发出信号
创建custom input source
需要定义以下内容:
scheduler
),让感兴趣的客户端知道如何联系输入源。handler
)。cancellation
)。在本例中,应用程序的Main Thread
维护对Input Source
的引用、该Input Source
的自定义命令缓冲区以及安装输入源的运行循环。当Main Thread
有一个任务要传递给Worker Thread
时,它会向Command Buffer
发送一个命令以及Worker Thread
启动任务所需的任何信息。(因为Worker Thread
的Input Source
和Main Thread
都可以访问Command Buffer
,所以必须同步访问。)一旦发出命令,Main Thread
就向Input Source
发出信号,并唤醒Worker Thread
的Runloop
。在接收到wake up
命令后,Runloop
调用Input Source
的处理程序,该处理程序处理在Command Buffer
中找到的命令。
可以在主线程直接通过激活子线程的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
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
发送消息,且消息的执行永远在持有source0
的Runloop
的那个线程中。也就能实现线程的转换,主要是子线程能够休眠,被主线程唤醒。
Cocoa和Core Foundation为使用与端口相关的对象和函数创建基于端口的输入源提供内置支持。例如,在Cocoa中,根本不需要直接创建输入源。您只需创建一个端口对象,并使用NSPort的方法将该端口添加到运行循环中。port对象为您处理所需输入源的创建和配置。
在Core Foundation中,必须手动创建端口及其运行循环源。在这两种情况下,都使用与端口不透明类型(CFMachPortRef
、CFMessagePortRef
或CFSocketRef
)相关联的函数来创建适当的对象。
按照官方文档的写法,这里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官方文档