文章目录
Runloop
OSX/iOS
系统中,提供了两个这样的对象:NSRunLoop
和 CFRunLoopRef
。
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
CFRunLoopRef
的代码是开源的,你可以在这里 http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation
的源码来查看。
(Update: Swift
开源后,苹果又维护了一个跨平台的 CoreFoundation 版本:https://github.com/apple/swift-corelibs-foundation/ ,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。)
Runloop
是属于Object Thread线程的一部分,代码就只有两个文件CFRunloop.h和CFRunloop.c
https://pan.baidu.com/s/1AVF6qmJRtV8KQR5T0qf7Tg 百度网盘链接
对于我们iOS的工程来说,main函数入口即是一个runloop
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
看下UIApplicationMain的说明
// NSPrincipalClass key specified, the UIApplication class is used. The delegate class will be instantiated using init.
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
其内部维持了一个runloop,保证我们程序持续运行,不会return返回,同时处理APP中的各种事件(触摸、定时器、performSelector)。
源码分析
创建
苹果不允许直接创建 RunLoop
,它只提供了两个自动获取的函数:CFRunLoopGetMain()
和 CFRunLoopGetCurrent()
。
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, __kCFRunLoopTypeID, size, NULL);
if (NULL == loop) {
return NULL;
}
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}
static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFSpinLock(&loopsLock);
if (!__CFRunLoops) { //__CFRunLoops是一个CFDictionary
__CFSpinUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); //首先创建主线程RunLoop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//这里将dic赋值给__CFRunLoops,bool OSAtomicCompareAndSwapPtrBarrier( void *oldVal, void *newVal, void * volatile *theVal )函数会在oldVal和theVal相同的情况下将oldVal存储的值修改为newVal
CFRelease(dict);
}
CFRelease(mainLoop);
__CFSpinLock(&loopsLock);
}
//从__CFRunLoops中获取当前线程的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFSpinUnlock(&loopsLock);
if (!loop) {
//不存在则创建一个newLoop,并以pthreadPointer(t)为key的形式存入__CFRunLoops
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFSpinLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFSpinUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
其_CFRunLoopGet0
步骤为:
1.判断thread t是否为空,为空则指向主线程
2.判断__CFRunLoops字典是否存在,不存在则创建
3.从__CFRunLoops获取当前线程的runloop,若不存在则创建,并存入__CFRunLoops。CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
4.注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
对外接口
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中 CFRunLoopModeRef
类并没有对外暴露,只是通过 CFRunLoopRef
的接口进行了封装。他们的关系如下:
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer
。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer
,让其互不影响。
CFRunLoopSourceRef
是事件产生的地方。Source
有两个版本:Source0
和 Source1
。
• Source0
只包含了一个回调(函数指针),它并不能主动触发事件
。使用时,你需要先调用 CFRunLoopSourceSignal(source)
,将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop)
来唤醒 RunLoop,让其处理这个事件。
• Source1
包含了一个 mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source1
能主动唤醒 RunLoop
的线程,其原理在下面会讲到。
CFRunLoopTimerRef
是基于时间的触发器,它和 NSTimer
是toll-free bridged
的(即可以通过__bridge
直接转换),可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef
是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有的状态监听
};
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环,这也是RunLoop保活线程的关键(不能退出RunLoop,就必须要有一个item在mode里)。
关于Runloop的深入理解查看该篇博文 https://blog.ibireme.com/2015/05/18/runloop/ ,已经很详细了
事件循环
根据苹果官方文档的说明 The Run Loop Sequence of Events ,Runloop事件循环如下:
1.通知观察者运行循环已经进入 //kCFRunLoopEntry
2.通知观察者,任何准备就绪的计时器都将启动。 //kCFRunLoopBeforeTimers
3.通知观察者,任何非基于端口的输入源都将被触发。 //kCFRunLoopBeforeSources
3.1 执行被加入的block
4.启动任何准备启动的非基于端口的输入源。 //这里处理source0
4.1 执行被加入的block
5.如果基于端口的输入源已经准备好并等待启动,则立即处理事件。转到第9步。 //这里跳转第9步处理source1
6.通知观察者线程即将休眠。 //kCFRunLoopBeforeWaiting
7.将线程休眠,直到发生以下事件之一:
a.基于端口的输入源的事件到达。
b.一个计时器触发
c.为运行循环设置的超时值过期。
d.run循环被显式地唤醒。
8.通知观察者线程刚刚唤醒。 //kCFRunLoopAfterWaiting
9.处理挂起事件
9.1.如果用户定义的计时器触发,则处理计时器事件并重新启动循环。转到步骤2。
9.2.如果有dispatch到main_queue的block,执行block。
9.3.如果触发了输入源,则传递事件 //这里处理source1
(9.1 9.2 9.3 只会执行一种,为if-else判断)
a.执行加入到Loop的block
b.如果没超时,mode里没空,loop也没被停止,那继续loop。转到步骤2
10.通知观察者运行循环已经退出。 //kCFRunLoopExit
这里综合ibireme在博文中的说明,进行了补充。
何时需要启动Runloop
对于主线程来说,runloop会自动创建并Run,子线程中,你需要自己管理你的Runloop。如果您计划执行以下任何操作,则需要启动run loop:
- 使用端口或自定义输入源与其他线程通信。
- 在线程上使用计时器。
- 在Cocoa应用程序中使用任何performSelector…方法。
- 保活线程以执行周期性任务。
处理事件
runloop
源码中我们可以看出,处理的事件有下列6种
// main dispatch queue
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
// __CFRunLoopDoObservers
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
// __CFRunLoopDoBlocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
// __CFRunLoopDoSources0
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
// __CFRunLoopDoSource1
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
// __CFRunLoopDoTimers
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
在文件中搜索既可以找到,上面的可能有点陌生,其实我们常用,
在viewController.m中viewDidLoad方法中写上
- (void)viewDidLoad {
[super viewDidLoad];
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"TIMER_CALLBACK"); //断点1
}];
[self performSelector:@selector(fire) withObject:nil afterDelay:1.0];
//__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"hello word");//断点2
});
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
void (^block)(void) = ^{
NSLog(@"123");//断点3
};
block();
}
// __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
- (void)fire{
NSLog(@"performSeletor");//断点4
}
#pragma mark - 触摸事件
//__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"来了,老弟!!!");//断点5
}
分别断点,能监听到Runloop各种事件回调。
这按执行顺序给出
- 断点3 为 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
(lldb) bt
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 2.1
frame #0: 0x0000000102354a84 RunloopSourceCode`__29-[ViewController viewDidLoad]_block_invoke_3(.block_descriptor=0x00000001023581d0) at ViewController.m:32
frame #1: 0x00000001023549c8 RunloopSourceCode`-[ViewController viewDidLoad](self=0x0000000141d117e0, _cmd="viewDidLoad") at ViewController.m:35
frame #2: 0x0000000258b0d224 UIKitCore`-[UIViewController loadViewIfRequired] + 1012
frame #3: 0x0000000258b0d628 UIKitCore`-[UIViewController view] + 28
frame #4: 0x00000002590ebe64 UIKitCore`-[UIWindow addRootViewControllerViewIfPossible] + 136
frame #5: 0x00000002590ec40c UIKitCore`-[UIWindow _setHidden:forced:] + 272
frame #6: 0x00000002590fcce8 UIKitCore`-[UIWindow makeKeyAndVisible] + 48
frame #7: 0x00000002590af908 UIKitCore`-[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 3532
frame #8: 0x00000002590b4fe0 UIKitCore`-[UIApplication _runWithMainScene:transitionContext:completion:] + 1540
frame #9: 0x00000002589782a4 UIKitCore`__111-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:]_block_invoke + 776
frame #10: 0x000000025898083c UIKitCore`+[_UICanvas _enqueuePostSettingUpdateTransactionBlock:] + 160
frame #11: 0x0000000258977f28 UIKitCore`-[__UICanvasLifecycleMonitor_Compatability _scheduleFirstCommitForScene:transition:firstActivation:completion:] + 236
frame #12: 0x0000000258978818 UIKitCore`-[__UICanvasLifecycleMonitor_Compatability activateEventsOnly:withContext:completion:] + 1064
frame #13: 0x0000000258976b64 UIKitCore`__82-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:]_block_invoke + 744
frame #14: 0x000000025897682c UIKitCore`-[_UIApplicationCanvas _transitionLifecycleStateWithTransitionContext:completion:] + 428
frame #15: 0x000000025897b36c UIKitCore`__125-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:]_block_invoke + 220
frame #16: 0x000000025897c150 UIKitCore`_performActionsWithDelayForTransitionContext + 112
frame #17: 0x000000025897b224 UIKitCore`-[_UICanvasLifecycleSettingsDiffAction performActionsForCanvas:withUpdatedScene:settingsDiff:fromSettings:transitionContext:] + 244
frame #18: 0x000000025897ff24 UIKitCore`-[_UICanvas scene:didUpdateWithDiff:transitionContext:completion:] + 360
frame #19: 0x00000002590b35e8 UIKitCore`-[UIApplication workspace:didCreateScene:withTransitionContext:completion:] + 540
frame #20: 0x0000000258cafe04 UIKitCore`-[UIApplicationSceneClientAgent scene:didInitializeWithEvent:completion:] + 360
frame #21: 0x000000022f12e9fc FrontBoardServices`-[FBSSceneImpl _didCreateWithTransitionContext:completion:] + 440
frame #22: 0x000000022f13840c FrontBoardServices`__56-[FBSWorkspace client:handleCreateScene:withCompletion:]_block_invoke_2 + 256
frame #23: 0x000000022f137c14 FrontBoardServices`__40-[FBSWorkspace _performDelegateCallOut:]_block_invoke + 64
frame #24: 0x0000000102640c78 libdispatch.dylib`_dispatch_client_callout + 16
frame #25: 0x0000000102644840 libdispatch.dylib`_dispatch_block_invoke_direct + 232
frame #26: 0x000000022f169040 FrontBoardServices`__FBSSERIALQUEUE_IS_CALLING_OUT_TO_A_BLOCK__ + 40
frame #27: 0x000000022f168cdc FrontBoardServices`-[FBSSerialQueue _performNext] + 408
frame #28: 0x000000022f169294 FrontBoardServices`-[FBSSerialQueue _performNextFromRunLoopSource] + 52
frame #29: 0x000000022c744728 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #30: 0x000000022c7446a8 CoreFoundation`__CFRunLoopDoSource0 + 88
frame #31: 0x000000022c743f90 CoreFoundation`__CFRunLoopDoSources0 + 176
frame #32: 0x000000022c73eecc CoreFoundation`__CFRunLoopRun + 1004
frame #33: 0x000000022c73e7c0 CoreFoundation`CFRunLoopRunSpecific + 436
frame #34: 0x000000022e93f79c GraphicsServices`GSEventRunModal + 104
frame #35: 0x00000002590b6c38 UIKitCore`UIApplicationMain + 212
frame #36: 0x0000000102354d08 RunloopSourceCode`main(argc=1, argv=0x000000016daaf950) at main.m:14
frame #37: 0x000000022c2028e0 libdyld.dylib`start + 4
(lldb)
- 断点2为 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
(lldb) bt
thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
frame #0: 0x0000000102354a58 RunloopSourceCode`__29-[ViewController viewDidLoad]_block_invoke_2(.block_descriptor=0x0000000102358190) at ViewController.m:28
frame #1: 0x000000010263f6f4 libdispatch.dylib`_dispatch_call_block_and_release + 24
frame #2: 0x0000000102640c78 libdispatch.dylib`_dispatch_client_callout + 16
frame #3: 0x000000010264e6fc libdispatch.dylib`_dispatch_main_queue_callback_4CF + 1360
frame #4: 0x000000022c74432c CoreFoundation`__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 12
frame #5: 0x000000022c73f264 CoreFoundation`__CFRunLoopRun + 1924
frame #6: 0x000000022c73e7c0 CoreFoundation`CFRunLoopRunSpecific + 436
frame #7: 0x000000022e93f79c GraphicsServices`GSEventRunModal + 104
frame #8: 0x00000002590b6c38 UIKitCore`UIApplicationMain + 212
frame #9: 0x0000000102354d08 RunloopSourceCode`main(argc=1, argv=0x000000016daaf950) at main.m:14
frame #10: 0x000000022c2028e0 libdyld.dylib`start + 4
- 断点1为 CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
(lldb) bt
1. thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 4.1
2. frame #0: 0x0000000102354a18 RunloopSourceCode`__29-[ViewController viewDidLoad]_block_invoke(.block_descriptor=0x0000000102358150, timer=0x0000000281e55140) at ViewController.m:22
frame #1: 0x000000022d23a874 Foundation`__NSFireTimer + 84
frame #2: 0x000000022c744d60 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 28
frame #3: 0x000000022c744a90 CoreFoundation`__CFRunLoopDoTimer + 864
frame #4: 0x000000022c7442c4 CoreFoundation`__CFRunLoopDoTimers + 248
frame #5: 0x000000022c73f214 CoreFoundation`__CFRunLoopRun + 1844
frame #6: 0x000000022c73e7c0 CoreFoundation`CFRunLoopRunSpecific + 436
frame #7: 0x000000022e93f79c GraphicsServices`GSEventRunModal + 104
frame #8: 0x00000002590b6c38 UIKitCore`UIApplicationMain + 212
frame #9: 0x0000000102354d08 RunloopSourceCode`main(argc=1, argv=0x000000016daaf950) at main.m:14
frame #10: 0x000000022c2028e0 libdyld.dylib`start + 4
(lldb)
- 断点4为 CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
(lldb) bt
1. thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 5.1
2. frame #0: 0x0000000102354ab0 RunloopSourceCode`-[ViewController fire](self=0x0000000141d117e0, _cmd="fire") at ViewController.m:47
frame #1: 0x000000022d227400 Foundation`__NSFireDelayedPerform + 408
frame #2: 0x000000022c744d60 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 28
frame #3: 0x000000022c744a90 CoreFoundation`__CFRunLoopDoTimer + 864
frame #4: 0x000000022c7442c4 CoreFoundation`__CFRunLoopDoTimers + 248
frame #5: 0x000000022c73f214 CoreFoundation`__CFRunLoopRun + 1844
frame #6: 0x000000022c73e7c0 CoreFoundation`CFRunLoopRunSpecific + 436
frame #7: 0x000000022e93f79c GraphicsServices`GSEventRunModal + 104
frame #8: 0x00000002590b6c38 UIKitCore`UIApplicationMain + 212
frame #9: 0x0000000102354d08 RunloopSourceCode`main(argc=1, argv=0x000000016daaf950) at main.m:14
frame #10: 0x000000022c2028e0 libdyld.dylib`start + 4
(lldb)
- 断点5为 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
* frame #0: 0x0000000102354b14 RunloopSourceCode`-[ViewController touchesBegan:withEvent:](self=0x0000000141d117e0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x00000002819546c0) at ViewController.m:52
frame #1: 0x00000002590e0f9c UIKitCore`forwardTouchMethod + 336
frame #2: 0x00000002590e0e38 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 60
frame #3: 0x00000002590ef3b8 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1584
frame #4: 0x00000002590f07ec UIKitCore`-[UIWindow sendEvent:] + 3140
frame #5: 0x00000002590d085c UIKitCore`-[UIApplication sendEvent:] + 340
frame #6: 0x00000002591969d4 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 1768
frame #7: 0x0000000259199100 UIKitCore`__handleEventQueueInternal + 4828
frame #8: 0x0000000259192330 UIKitCore`__handleHIDEventFetcherDrain + 152
frame #9: 0x000000022c744728 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 24
frame #10: 0x000000022c7446a8 CoreFoundation`__CFRunLoopDoSource0 + 88
frame #11: 0x000000022c743fe0 CoreFoundation`__CFRunLoopDoSources0 + 256
frame #12: 0x000000022c73eecc CoreFoundation`__CFRunLoopRun + 1004
frame #13: 0x000000022c73e7c0 CoreFoundation`CFRunLoopRunSpecific + 436
frame #14: 0x000000022e93f79c GraphicsServices`GSEventRunModal + 104
frame #15: 0x00000002590b6c38 UIKitCore`UIApplicationMain + 212
frame #16: 0x0000000102354d08 RunloopSourceCode`main(argc=1, argv=0x000000016daaf950) at main.m:14
frame #17: 0x000000022c2028e0 libdyld.dylib`start + 4
(lldb)
如何创建Runloop 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的方式
只有对应用程序中的辅助线程,才需要启动run循环。运行循环必须至少有一个输入源或计时器要监视。如果没有附加,则run循环立即退出。
有几种方法可以启动运行循环,包括:
- 无条件地
exp:CFRunLoopRun(void)
- 有一个固定的时间限制
exp: - (void)runUntilDate:(NSDate *)limitDate;
- 在特定模式下
exp:CFRunLoopRunInMode()
- (void)skeletonThreadMain
{
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;
// Add your sources or timers to the run loop and do any other setup.
do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
// Check for any other exit conditions here and set the
// done variable as needed.
}
while (!done);
// Clean up code here. Be sure to release any allocated autorelease pools.
}
退出Runloop的方式
有两种方法可以使运行循环在处理事件之前退出:
- 将run循环配置为使用超时值运行。
- 告诉run循环停止。
如果能够管理超时值,则使用超时值当然是首选。指定超时值可以让运行循环在退出之前完成所有的正常处理,包括向运行循环观察者发送通知。
使用CFRunLoopStop
函数显式地停止运行循环会产生类似超时的结果。run循环发送任何剩余
的run-loop通知,然后退出。
与线程之间关系
主线程的runloop是系统自动创建,子线程的runloop需要我们自己创建。
线程结束的同时,runloop也会跟着停止。
- (void)viewDidLoad {
[super viewDidLoad];
self.isStopping = NO;
LGThread *thread = [[LGThread alloc] initWithBlock:^{
// thread.name = nil 因为这个变量只是捕捉
// LGThread *thread = nil
// thread = 初始化 捕捉一个nil进来
NSLog(@"%@---%@",[NSThread currentThread],[[NSThread currentThread] name]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"hello word"); // 退出线程--结果runloop也停止了
if (self.isStopping) {
[NSThread exit];
}
}];
[[NSRunLoop currentRunLoop] run];
}];
thread.name = @"lgcode.com";
[thread start];
}
在某个时机,isStopping置为YES时,线程退出,runloop跟着停止。
线程安全
线程安全性取决于使用哪个API来操作运行循环。Core Foundation
中的函数通常是线程安全的
,可以从任何线程调用。但是,如果您正在执行更改运行Runloop
配置的操作,最好还是尽可能从拥有运行Runloop的线程
开始执行。
Cocoa NSRunLoop
类本身并不像它的核心基础类那样线程安全。如果您使用NSRunLoop
类来修改您的Runloop
,那么您应该只从拥有该Runloop
的同一个线程进行修改。将输入源或计时器添加到属于不同线程
的Runloop
中可能会导致代码崩溃或以意想不到的方式运行。
自定义输入源
根据苹果官方文档说明,基于端口的源(Source1
)由内核自动发出信号,定制源(Source0
)必须从另一个线程手动发出信号。
定制源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/16hc7jHil5BPKY_LT5jtOhA
下面讲解如何实现:
自定义输入源
需要使用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
3个回调函数
RunLoopSourceScheduleRoutine
回调会在你把自定义Source加入到Runloop时,就进行回调。因为Input Source
在此场景下只有一个客户端,它使用scheduler
函数发送一条消息,将自己注册到该线程上的应用程序delegate
。当delegate
希望与Input Source
通信时,它使用RunLoopContext
对象中的信息来进行通信。
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];
}
最重要的回调之一是当您的Input Source
发出信号时用来处理自定义数据的回调RunLoopSourcePerformRoutine
void RunLoopSourcePerformRoutine (void *info)
{
RunLoopSource* obj = (RunLoopSource*)info;
[obj sourceFired];
}
如果您使用CFRunLoopSourceInvalidate函数
将Input Sources
从其Runloop
中删除,系统将调用Input Sources
的取消回调。您可以使用这个回调通知客户端,您的Input Sources
不再有效,他们应该删除对它的任何引用。下面代码显示了用RunLoopSource
对象注册的取消回调。这个函数向应用程序委托发送另一个RunLoopContext
对象,但这一次要求委托删除对Runloop Source
的引用。
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];
}
安装运行Input Source
- (id)init
{
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();
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
}
与输入源的客户端的协调
要使得Input Source
有效,你需要操作它并从另一个线程发出信号。Input Source
输入源的意义在于将其关联的线程休眠,直到有事情要做为止。这事实上需要让应用程序中的其他线程知道输入源,并有方法与它通信。
通知客户端关于Input Source
的一种方法是,当您的Input Source
首次安装到其Runloop
中时,发送注册请求。您可以向任意数量的客户注册您的Input Source
,或者您可以简单地向某个中央代理注册它,然后将您的Input Source
交付给感兴趣的客户。
- (void)registerSource:(RunLoopContext*)sourceInfo;
{
[sourcesToPing addObject:sourceInfo];
}
- (void)removeSource:(RunLoopContext*)sourceInfo
{
id objToRemove = nil;
for (RunLoopContext* context in sourcesToPing)
{
if ([context isEqual:sourceInfo])
{
objToRemove = context;
break;
}
}
if (objToRemove)
[sourcesToPing removeObject:objToRemove];
}
这里我参考自己的理解做了一个demo,代码和官方文档相似,参考这里,百度网盘
附上demo 链接:https://pan.baidu.com/s/16hc7jHil5BPKY_LT5jtOhA
demo能够实现将RunloopContext
交互给外面对象持有,外面的对象可以通过RunloopContext
对添加进Runloop的source0
发送消息,且消息的执行永远在持有source0
的Runloop
的那个线程中。也就能实现线程的转换吧。
基于端口的Source1
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 <Foundation/NSObject.h>
@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
未完待续...