[iOS] 初识Runloop
最近想写的topic太多了,但好多是和view相关的,所以吧就先谈一下Runloop了~
源码链接:https://opensource.apple.com/tarballs/CF/
1. RunLoop是什么?
RunLoop顾名思义就是一个不停止的循环,不断地重复:
休眠->有事件需要处理->处理事件->休眠
我们的app就是基于RunLoop来生存的,当app启动会自动创建主线程以及它所对应的RunLoop,然后RunLoop会一直跑着,适时响应我们的事件,例如触摸、timer之类的。
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"app start");
int code = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"app stop");
return code;
}
}
输出:
app start
如果我们把main函数改成上面那个样子,它并不会执行到app stop,因为UIApplicationMain会开启一个RunLoop并且一直死循环,不会结束,所以后面的就都不会执行了。
源码里面的CFRunLoopRun可看出当RunLoop不是stop或finish状态的时候,就会一直执行CFRunLoopRunSpecific和CHECK_FOR_FORK,的确是一个死循环。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
2. 处理哪些事件
- 定时器(Timer)、PerformSelector
- GCD Async Mian Queue (注意只有main queue哦)
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
3. RunLoop结构
我们先从RunLoop是神马开始看起:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
它里面的Mode非常重要:
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
每个RunLoop可以有很多个Mode,然后在各个mode间转换,每个mode有:
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
其中比较重要的是Mode包含和很多个set,
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
RunLoop只处理当前Mode里面的timer、source、observer触发的事件,其他Mode注册的timer之类的会暂停不响应。
RunLoop的Mode示意图Mode组成:
- Source1 : 基于Port的线程间通信
- Source0 : 触摸事件,PerformSelectors
- Timers : 定时器,NSTimer
- Observer : 监听器,用于监听RunLoop的状态
①Source1: 基于Port的线程通信
@interface TestRunLoop()<NSPortDelegate>
@end
@implementation TestRunLoop
- (instancetype)init {
self = [super init];
if (self) {
[self setup];
}
return self;
}
- (void)setup {
[self test1];
}
- (void)test1 {
//声明两个端口
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
threadPort.delegate = self;
//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop] addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//添加一个Port
NSLog(@"Thread: %@", [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
//components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
#pragma mark - NSPortDelegate
//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message {
NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
}
@end
输出:
Thread: <NSThread: 0x600000c13b40>{number = 5, name = (null)}
收到消息了,线程为:<NSThread: 0x600000c13b40>{number = 5, name = (null)}
hello
Stack:(
0 Example1 0x0000000109e46f53 -[TestRunLoop handlePortMessage:] + 275
1 Foundation 0x000000010a1f4390 __NSFireMachPort + 253
2 CoreFoundation 0x000000010b0c7096 __CFMachPortPerform + 150
3 CoreFoundation 0x000000010b0f3419 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__ + 41
4 CoreFoundation 0x000000010b0f2a7b __CFRunLoopDoSource1 + 459
5 CoreFoundation 0x000000010b0ed00a __CFRunLoopRun + 2490
6 CoreFoundation 0x000000010b0ec302 CFRunLoopRunSpecific + 626
7 Foundation 0x000000010a1fa044 -[NSRunLoop(NSRunLoop) runMode:beforeDate:] + 277
8 Example1 0x0000000109e46b37 __20-[TestRunLoop test1]_block_invoke + 295
9 libdispatch.dylib 0x000000010c9f4d7f _dispatch_call_block_and_release + 12
10 libdispatch.dylib 0x000000010c9f5db5 _dispatch_client_callout + 8
11 libdispatch.dylib 0x000000010c9f87b9 _dispatch_queue_override_invoke + 1022
12 libdispatch.dylib 0x000000010ca06632 _dispatch_root_queue_drain + 351
13 libdispatch.dylib 0x000000010ca06fca _dispatch_worker_thread2 + 130
14 libsystem_pthread.dylib 0x000000010cdde6b3 _pthread_wqthread + 583
15 libsystem_pthread.dylib 0x000000010cdde3fd start_wqthread + 13
)
通过栈可以看出来__CFRunLoopDoSource1会接收Port的消息并对他进行处理。
② Source0: 触摸事件,PerformSelectors
重写页面的touchBegan,可以看到是由__CFRunLoopDoSources0来处理的。
- (void) touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
}
Stack:(
0 Example1 0x00000001009601fd -[MainViewController touchesBegan:withEvent:] + 93
1 UIKitCore 0x0000000104b0ba09 forwardTouchMethod + 353
2 UIKitCore 0x0000000104b0b897 -[UIResponder touchesBegan:withEvent:] + 49
3 UIKitCore 0x0000000104b1ac48 -[UIWindow _sendTouchesForEvent:] + 1869
4 UIKitCore 0x0000000104b1c5d2 -[UIWindow sendEvent:] + 4079
5 UIKitCore 0x0000000104afad16 -[UIApplication sendEvent:] + 356
6 UIKit 0x000000011dd184af -[UIApplicationAccessibility sendEvent:] + 85
7 UIKitCore 0x0000000104bcb293 __dispatchPreprocessedEventFromEventQueue + 3232
8 UIKitCore 0x0000000104bcdbb9 __handleEventQueueInternal + 5911
9 CoreFoundation 0x00000001012d6be1 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
10 CoreFoundation 0x00000001012d6463 __CFRunLoopDoSources0 + 243
11 CoreFoundation 0x00000001012d0b1f __CFRunLoopRun + 1231
12 CoreFoundation 0x00000001012d0302 CFRunLoopRunSpecific + 626
13 GraphicsServices 0x000000010a2622fe GSEventRunModal + 65
14 UIKitCore 0x0000000104ae0ba2 UIApplicationMain + 140
15 Example1 0x000000010095f9a0 main + 112
16 libdyld.dylib 0x0000000103658541 start + 1
17 ??? 0x0000000000000001 0x0 + 1
)
③ Timer: 定时器
用定时器看一下调用栈是由__CFRunLoopDoTimer调用的block~
- (void)testTimer {
[NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
}];
}
Stack:(
0 Example1 0x0000000105c4b3bc __24-[TestRunLoop testTimer]_block_invoke + 92
1 Foundation 0x0000000106627135 __NSFireTimer + 83
2 CoreFoundation 0x00000001074f93e4 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
3 CoreFoundation 0x00000001074f8ff2 __CFRunLoopDoTimer + 1026
4 CoreFoundation 0x00000001074f885a __CFRunLoopDoTimers + 266
5 CoreFoundation 0x00000001074f2efc __CFRunLoopRun + 2220
6 CoreFoundation 0x00000001074f2302 CFRunLoopRunSpecific + 626
7 GraphicsServices 0x000000010f54e2fe GSEventRunModal + 65
8 UIKitCore 0x0000000109dccba2 UIApplicationMain + 140
9 Example1 0x0000000105c4a690 main + 112
10 libdyld.dylib 0x0000000108944541 start + 1
11 ??? 0x0000000000000001 0x0 + 1
)
④ Observer: 用于监听RunLoop的状态
- (void)testObserver {
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop进入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要处理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要处理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒来了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
输出截取stack部分:
Stack:(
0 Example1 0x0000000108a97144 __27-[TestRunLoop testObserver]_block_invoke + 1076
1 CoreFoundation 0x0000000109d430f7 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 23
2 CoreFoundation 0x0000000109d3d5be __CFRunLoopDoObservers + 430
3 CoreFoundation 0x0000000109d3de69 __CFRunLoopRun + 2073
4 CoreFoundation 0x0000000109d3d302 CFRunLoopRunSpecific + 626
5 GraphicsServices 0x000000011239c2fe GSEventRunModal + 65
6 UIKitCore 0x000000010e0e4ba2 UIApplicationMain + 140
7 Example1 0x0000000108a96040 main + 112
8 libdyld.dylib 0x000000010b6bb541 start + 1
9 ??? 0x0000000000000001 0x0 + 1
)
因为Observer回调太多啦所以就只截取一个Stack片段,但还是可以看出来是由__CFRunLoopDoObservers来处理Observer事件滴。
题外话:
上篇GCD源码分析的时候我们看dispatch_async的调用堆栈是没有RunLoop的身影,那个是因为用的自己创建的并行queue,如果是dispatch给main queue是不同的哦,这个是会出现RunLoop滴。
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
});
输出:
Stack:(
0 Example1 0x000000010b952a90 __20-[TestRunLoop setup]_block_invoke + 64
1 libdispatch.dylib 0x000000010e502d7f _dispatch_call_block_and_release + 12
2 libdispatch.dylib 0x000000010e503db5 _dispatch_client_callout + 8
3 libdispatch.dylib 0x000000010e511080 _dispatch_main_queue_callback_4CF + 1540
4 CoreFoundation 0x000000010cc008a9 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
5 CoreFoundation 0x000000010cbfaf56 __CFRunLoopRun + 2310
6 CoreFoundation 0x000000010cbfa302 CFRunLoopRunSpecific + 626
7 GraphicsServices 0x00000001152652fe GSEventRunModal + 65
8 UIKitCore 0x000000010fa07ba2 UIApplicationMain + 140
9 Example1 0x000000010b951dd0 main + 112
10 libdyld.dylib 0x000000010e578541 start + 1
11 ??? 0x0000000000000001 0x0 + 1
)
4. RunLoop与线程
每个线程都对应一个RunLoop,我们是不能创建的,只能通过下面的两种方法获取当前线程的RunLoop:
CFRunLoopGetMain() //获取主线程的RunLoop
CFRunLoopGetCurrent() //获取当前线程的RunLoop
or
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
我们来看一下源码里面是怎么get滴~
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());
}
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
……
//从字典里找thread为key的value是否存在
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
//如果不存在则创建新的RunLoop并放入字典
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&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
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
……
return loop;
}
可以看出来每个线程所对应的的RunLoop是存在字典里面的,thread作为key,RunLoop作为value。
主线程的RunLoop会自动创建,子线程不会自动创建RunLoop,除非主动调用CFRunLoopGetCurrent。
我们先尝试run一下面的代码,会发现木有任何log,因为子线程不会自动创建RunLoop,所以没有用于处理Timer的RunLoop,导致timer不会执行(这也是GCD source和timer不太一样的地方)。
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"Thread:%@", [NSThread currentThread]);
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
}];
});
我们自己主动在thread中创建一个timer,控制台就会有log输出啦~ (太多了Log就不粘过来了)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"Thread:%@", [NSThread currentThread]);
NSLog(@"Stack:%@", [NSThread callStackSymbols]);
}];
// 主动开启RunLoop
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
线程如果没有RunLoop,那么当任务执行完毕以后他就会被销毁,但是如果有RunLoop,就会一直活着,直到RunLoop退出。
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
[thread1 start];
__weak typeof(NSThread)*weakThread = thread1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"thread1: %@", weakThread);
});
- (void)run:(NSString *)string {
NSLog(@"task executed thread:%@", [NSThread currentThread]);
}
输出:
task executed thread:<NSThread: 0x60000305d900>{number = 5, name = (null)}
thread1: (null)
如果没有强引用,一般thread在执行完任务以后就会被销毁,如果想让thread仍旧活着,可以通过增加RunLoop的方法,只要线程对应的RunLoop在跑着,thread就不会被回收。
NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(run:) object:@"test"];
[thread1 start];
__weak typeof(NSThread)*weakThread = thread1;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"thread1: %@", weakThread);
});
- (void)run:(NSString *)string {
NSLog(@"task executed thread:%@", [NSThread currentThread]);
// 启动RunLoop并添加port监听防止RunLoop自己退出
NSPort *threadPort = [NSMachPort port];
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
输出:
task executed thread:<NSThread: 0x6000035f9b00>{number = 5, name = (null)}
thread1: <NSThread: 0x6000035f9b00>{number = 5, name = main}
这一次的thread在即使没有强引用的情况下,由于RunLoop的运行也没有被销毁,如果想要销毁这个thread,必须停止RunLoop。
那么要如何停止RunLoop呢?
方式一共有3种:
- 移除掉runloop中的所有事件源(timer和source)
- 设置一个超时时间 (run起来RunLoop时设置)
- 只要CFRunloop运行起来就可以用CFRunLoopStop()停止
总结一下如果想让线程一直活着,就得让他的RunLoop一直跑着,并且Mode不能为空哦,否则就会自动退出RunLoop哦。(注意只有Observer是不能保活RunLoop滴,也会被判断为空)
5. RunLoop工作流程
从stack我们可以看出来每次处理其实都是在__CFRunLoopRun方法内做的:
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
……
// 如果mode是空就return,直接stop当前的RunLoop
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
……
// 通知Observers在entry和exit的时候
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
……
}
// 精简后的 __CFRunLoopRun函数,保留了主要代码
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知Observers:即将处理Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知Observers:即将处理Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 处理Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 如果有Sources1,就跳转到handle_msg标记处
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
// 通知Observers:即将休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 进入休眠,等待其他消息唤醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒来
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已经唤醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是谁唤醒了RunLoop,进行相应的处理
if (被Timer唤醒的) {
// 处理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD唤醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1唤醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 执行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根据之前的执行结果,来决定怎么做,为retVal赋相应的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
根据源码很多的流程图都是下面酱紫的,但是在Reference里面关于误解的一篇有纠正“是否存在Source1”的判断其实是有问题的,以及唤醒的时候也是其实等待的是Source1,具体可以看作者的验证哈。
RunLoop运行流程每个RunLoop只能运行一个Mode,系统提供5种Mode:
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪Mode,所有UI交互事件,例如用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
- UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到
- kCFRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode,是一种模式集合。将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、UITrackingRunLoopMode。
通过CFRunLoopAddCommonMode可以将某个mode加到CommonModes里面。
其实这个Mode并不是一个真正的Mode,只是用于当你需要一个Timer/Source…在多个Mode下都可以被触发,就将它加入这个CommonMode,并确保你的多个Mode在CommonMode之中就OK啦。
由于每个时间点RunLoop只能运行在一种Mode下,是不可能将它设置为这个CommonMode的,CommonMode存在的意义就是让RunLoop运行在其他Mode时都可以触发规定的Source,它自身并没有什么东西所以只是一个占位Mode,RunLoop不会真的运行在这个Mode里面。
实现原理摘抄:
一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的"commonModes"中)。RunLoop 会自动将 _commonModeItems 加入到具有 "Common" 标记的所有Mode里。
例如,当将Timer加入到顶层的 RunLoop 的 "commonModeItems"中时,"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。
- 自定义Mode:可以设置自定义的运行模式Mode,可以用CFRunLoopAddCommonMode添加到NSRunLoopCommonModes中
其实在我们触发界面滑动的时候,会发现timer之类的是不工作的,就是因为RunLoop切换到了UITrackingRunLoopMode,于是kCFRunLoopDefaultMode里面的timer就不能被触发了。
我们来测试一下UI交互过程中会切换mode这个事儿~
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer arrive");
}];
然后在viewController里面加一个textView,如果滑动textView过程中会发现timer并不会打印log。
如果我希望Timer不要被UI交互打断要怎么办呢?
A: 可以将它加入到CommonMode而不是DefaultMode
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer arrive");
}];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
把代码改成上面酱紫即使在滑动TextView过程中,timer的日志也会不断地打印滴~
如果需要切换Mode,只能退出RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
(系统run起来一个RunLoop可以指定的Mode枚举值只有Default和Commons所以也木有好的切换案例。。。)
我们只能看一下RunLoop的几种启动方法了:
-
(void)run; //进入处理事件循环,如果没有事件则立刻返回。
注意:主线程上调用这个方法会导致无法返回(进入无限循环,虽然不会阻塞主线程),因为主线程一般总是会有事件处理。 -
(void)runUntilDate:(NSDate *)limitDate; //同run方法,增加超时参数limitDate,避免进入无限循环。
使用在UI线程(亦即主线程)上,可以达到暂停的效果,因为UI线程始终有事件处理,所以在limitDate时会返回继续之后的代码执行。 -
(BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate; //等待消息处理,好比在PC终端窗口上等待键盘输入。一旦有合适事件被处理了,则立刻返回;类同run方法,如果没有事件处理也立刻返回;有否事件处理由返回布尔值判断。同样limitDate为超时参数。
-
(void)acceptInputForMode:(NSString *)mode beforeDate:(NSDate *)limitDate; //似乎和runMode:差不多(测试过是这种结果,但确定是否有其它特殊情况下的不同),没有BOOL返回值。
官网文档也提到run和runUntilDate:会以NSDefaultRunLoopMode参数调用runMode:来处理事件。
这里尝试一下几种方式的区别:
- (void)test1 {
//声明两个端口
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
threadPort.delegate = self;
//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop] addPort:mainPort forMode:NSDefaultRunLoopMode];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//添加一个Port
[[NSRunLoop currentRunLoop] addPort:threadPort forMode:NSDefaultRunLoopMode];
// 每次替换这个地方的方法
// [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
// [[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"continue");
});
NSString *s1 = @"hello";
NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
//components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];
});
}
#pragma mark - NSPortDelegate
//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message {
NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);
//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];
NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);
}
三种方式的输出是不一样滴:
// runMode:
收到消息了,线程为:<NSThread: 0x600003a16f40>{number = 5, name = (null)}
hello
continue
// runUntil:
收到消息了,线程为:<NSThread: 0x600002485800>{number = 5, name = (null)}
hello
// run:
收到消息了,线程为:<NSThread: 0x60000042d300>{number = 5, name = (null)}
hello
也就是runUntil和run会循环处理事件,runUntil是有超时时间的,但是run是木有滴;而runMode只要处理一次事件就返回了,不会一直循环处理。
6. RunLoop生命周期
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
-
kCFRunLoopEntry:
每次RunLoop重新进入时的activity,RunLoop每一次进入一个Mode,就通知一次外部 kCFRunLoopEntry,之后会一直以该Mode运行,如果切换到其他Mode,会再次通知 kCFRunLoopEntry。 -
kCFRunLoopBeforeTimers:
这个在工作流程分析的时候已经看到过了,就是在处理timer和source之前发出的通知
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
注意虽然可能没有timer,但是这个通知是肯定每次都会发的,相当于是准备处理的通知 -
kCFRunLoopBeforeSources:
同上~ 在调用__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); 之后会马上调用的__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); -
kCFRunLoopBeforeWaiting:
这个activity表示当前线程即将可能进入睡眠,如果能够从内核队列上读出msg则继续运行任务,如果当前队列上没多余消息,则进入睡眠状态。 -
kCFRunLoopAfterWaiting:
这个activity是当前线程从睡眠状态中收到消息恢复执行任务的通知。每一次RunLoop从睡眠状态中恢复必调的一个activity。 -
kCFRunLoopExit:
切换Mode的时候会调用到这个activity,即RunLoop退出了。 -
kCFRunLoopAllActivities:
监听所有状态
在加了前面章节的Observer以后,如果执行CFRunLoopStop(CFRunLoopGetCurrent()),则会看到下面的log,如果不执行就Stop就不会看到前面的两个退出和进入的log:
RunLoop退出了
RunLoop进入
RunLoop要处理Timers了
RunLoop要处理Sources了
RunLoop要处理Timers了
RunLoop要处理Sources了
……
RunLoop要处理Timers了
RunLoop要处理Sources了
RunLoop要休息了
RunLoop醒来了
RunLoop要处理Timers了
RunLoop要处理Sources了
RunLoop要处理Timers了
RunLoop要处理Sources了
RunLoop要休息了
最后,其实还有很多东西木有提及,比如自动释放池、应用之类的,留一个小小的问题吧~ 也是看reference的时候看到的一个小考题:
(void)test{
NSLog(@"任务B");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSThread *thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"任务A");
}];
[thread start];
[self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:YES];
}
这个的打印结果会是什么嘞?
答案:
任务A
*** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[ViewController performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform'
因为start thread以后那个线程执行完任务就被销毁了,所以再给nil对象执行performSelector会crash。
Reference: