RunLoop的深入分析(深入理解runloop)
Runloop可以保证线程的持续运行,并且让所在线程可以随时响应并处理事件。
主要内容:
Runloop的认识
Runloop对象的认识
Runloop的执行过程
具体使用
Runloop的源码:下载地址RunLoop的官方文档:官方文档
1、Runloop的认识
1.1 RunLoop是什么
Runloop本质是一个对象,可以实现事件循环机制,也就是让所在线程可以随时响应并处理事件。
Runloop可以看做是闲等待的do-while的循环。它和普通的do-while循环一样处在运行状态,但是在等待的过程中处于运行状态的同时并不会消耗CPU性能。
作用:
保持程序持续运行
程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
节省CPU资源,提高程序性能
线程在没有消息处理时休眠,有消息到来时立刻被唤醒
响应事件
定时器(Timer)、方法调用(PerFormSelecotr)
GCD Async Main Queue
事件响应、手势识别、界面刷新
网络请求
自动释放池autoreleasePool
1.2 闲等待的验证
RunLoop可以节省资源,是一种闲等待的do...while查看两种类型下的CPU发现,RunLoop执行循环不会使用CPU资源,而do...while一直在使用CPU资源
正常的do...while
Runloop循环
1.3 主线程Runloop的启动
我们知道Runloop可以保证所在线程的持续运行,而在iOS中程序是可以持续运行的,这说明主线程的Runloop起到了作用,那么它是什么时候开启的呢?
既然主线程一开起来,就需要持续运行,那么猜测应该是在开启主线程的时候开启的Runloop的。因此我们在程序的入口main函数中查找
进入到UIApplicationMain中发现无法看到具体实现
启动后通过在控制台打印可以确实进行了RunLoop的执行
2、Runloop对象的认识
接下来会通过源码查看Runloop在底层到底是个什么东西,
我们在上层使用NSRunloop,属于Foundation框架。 但是在打印堆栈的时候就可以发现NSRunLoop其实是基于CoreFoundation框架的CFRunLoopRef的封装。所以我们往下层需要在CoreFoundation源码中的CFRunLoopRef对象进行分析。
例如:
2.1 CFRunLoopRef对象
CFRunLoopRef就是Runloop本身,是NSRunloop在底层的实现
源码:
struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; /* locked for accessing mode list */ __CFPort _wakeUpPort; // //通过该函数CFRunLoopWakeUp内核向该端口发送消息可以唤醒runloop Boolean _unused; volatile _per_run_data *_perRunData; // reset for runs of the run loop pthread_t _pthread;//所属线程 uint32_t _winthread; CFMutableSetRef _commonModes;//它的commonModes CFMutableSetRef _commonModeItems;//包含的commonModeItems CFRunLoopModeRef _currentMode;//当前mode CFMutableSetRef _modes;//包含的所有modes struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFTypeRef _counterpart; };复制代码
说明:
可以看出Runloop是有自己所属线程的
同时包含有commonModes和commonModeItems,这个可以称作通用mode,它包含了其他的mode,详细的下面会说
一个Runloop包含多个mode,并且可以获取到当前的mode
因此Runloop和mode是一对多关系,但是同一时间只能使用一个mode
2.2 CFRunLoopModeRef对象
CFRunLoopModeRef对象是Runloop的运行模式
typedef struct __CFRunLoopMode *CFRunLoopModeRef; struct __CFRunLoopMode { CFRuntimeBase _base; pthread_mutex_t _lock; CFStringRef _name;//mode名称,运行模式是通过名称来识别的 Boolean _stopped;//mode是否被终止 char _padding[3]; //整个结构体最核心的部分 --------------------------------------------------------------------------------- CFMutableSetRef _sources0;//事件源0 CFMutableSetRef _sources1;//事件源1 CFMutableArrayRef _observers;//观察者 CFMutableArrayRef _timers;//定时器 --------------------------------------------------------------------------------- CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef __CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中 CFIndex _observerMask; #if USE_DISPATCH_SOURCE_FOR_TIMERS dispatch_source_t _timerSource; dispatch_queue_t _queue; Boolean _timerFired; 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是通过名称来识别的,不是通过ID之类的
mode可以存储四种item,_sources0、_sources1、_observers、_timers
每种item都可以存储多个。
_sources0、_sources1是Set结构存储的
_observers、_timers是数组结构存储的
注意:
因此我们可以得出结论一个Runloop会有多个运行模式mode,但是一次只能允许一个mode,而mode可以有多个事件,有四种类型,事件源0,事件源1,监听器,时间源。
Runloop管理mode,mode管理具体的事件
Runloop启动时只能选择其中一个mode
如果需要切换mode,只能退出当前的loop,选择mode后重新进入loop
如果mode中没有任何的_sources0、_sources1、_observers、_timers,Runloop会立即退出(下面看源码可知)
我们无法自己创建mode,当传入一个新的mode Name时,Runloop内部发现没有对应的mode时,会自动创建对应的CFRunLoopModeRef。
对于一个Runloop来说,其内部的mode只能增加不能删除
2.2.1 __CFRunLoopMode的五种运行模式
1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行 2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode 4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 5. kCFRunLoopCommonModes: 这是一个通用Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode 复制代码
kCFRunLoopCommonModes其实并不是一种真正的mode,只是一种通用的mode。NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode
其中mode在苹果文档中提及的有五个,而在iOS中公开暴露出来的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。
2.2.2 CommonModes的认识
CommonModes需要单独进行分析,它有其特殊的作用
看一下我们将_commonModes添加到Runloop之后会进行什么操作
源码:
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) { if (!CFSetContainsValue(rl->_commonModes, modeName)) { //获取所有的_commonModeItems CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL; //获取所有的_commonModes CFSetAddValue(rl->_commonModes, modeName); if (NULL != set) { CFTypeRef context[2] = {rl, modeName}; // 将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context); CFRelease(set); } } }复制代码
说明:
可以看到当将commonMode添加到Runloop中时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common标记的所有 Mode 里
也就是说当其他mode的事件标记为common时,相当于这个事件同时处于commonMode中。
所以commonMode可以同时含有其他mode中的事件。这也就是公共mode的意义。
2.2.3 ModeItem
ModeItem就是mode包含的事件项,包括_sources0、_sources1、_observers、_timers。
Runloop需要处理的消息:_sources0、_sources1、_timers
Runloop用来监听状态的对象:_observers
API:
//添加source事件源 CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); //添加监听者 CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); //添加时间源 CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode); //移除事件源 CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName); //移除监听者 CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName); //移除时间源 CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);复制代码
注意:
一个mode可以包含多个modeItem,一个modeItem也可以同时加入到多个mode中,但是一个item重复加入到同一个mode时只表现为有一个
如果一个mode中没有item,Runloop会直接退出,不进入循环
mode对外实现事件就是通过modeItem
2.2.4 mode的简单使用
来看一个NSTimer计时不准确的问题。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{ NSLog(@"WY:滑动中%@",[NSRunLoop currentRunLoop].currentMode); } - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView{ NSLog(@"WY:停止滑动%@",[NSRunLoop currentRunLoop].currentMode); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ NSLog(@"WY:触摸%@",[NSRunLoop currentRunLoop].currentMode); } -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; // 加入到RunLoop中才可以运行 // 1. 把定时器添加到RunLoop中,并且选择默认运行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode // [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // 当textFiled滑动的时候,timer失效,停止滑动时,timer恢复 // 原因:当textFiled滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了 // 2. 当我们将timer添加到UITrackingRunLoopMode模式中,此时只有我们在滑动textField时timer才会运行 // [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; // 3. 那个如何让timer在两个模式下都可以运行呢? // 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer // 3.2 使用站位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,下面两种模式被打上标签 //0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"} //2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"} // 因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行 [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; NSLog(@"%@",[NSRunLoop mainRunLoop]); } -(void)show { NSLog(@"-------"); }复制代码
说明:
可以看到我们使用kCFRunLoopDefaultMode时,当滑动ScrollView,NSTimer就暂停了
当我们使用UITrackingRunLoopMode就只能在滑动ScrollView时运行NSTimer。
而我们使用NSRunLoopCommonModes时,滑动或者不滑动ScrollView,均会运行NSTimer。
这是因为kCFRunLoopDefaultMode和UITrackingRunLoopMode的items默认都在NSRunLoopCommonModes的modeItems中
2.3 CFRunLoopSourceRef对象
CFRunLoopSourceRef是响应事件的对象,当有事件发生时响应
源码:
typedef struct __CFRunLoopSource * CFRunLoopSourceRef; struct __CFRunLoopSource { CFRuntimeBase _base; uint32_t _bits; pthread_mutex_t _lock; CFIndex _order;//执行顺序 CFMutableBagRef _runLoops;//包含多个RunLoop //版本 union { CFRunLoopSourceContext version0; /* immutable, except invalidation */ CFRunLoopSourceContext1 version1; /* immutable, except invalidation */ } _context; };复制代码
说明:
__CFRunLoopSource结构体有两种类型CFRunLoopSourceContext和CFRunLoopSourceContext1。
union是联合体,二者取其一。
2.3.1 source0
Source0用来实现用户事件
源码:
typedef struct { CFIndex version; void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); Boolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info); void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode); void (*perform)(void *info);//回调函数 } CFRunLoopSourceContext;复制代码
说明:
Source0 只包含了一个回调(函数指针),它并不能主动触发事件。
使用时,你需要先调用 CFRunLoopSourceSignal (source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp (runloop) 来唤醒 RunLoop,让其处理这个事件。
2.3.2 source1
Source1用来实现系统事件和基于Port的线程间通信,Source1可以主动触发事件,主动唤醒事件
源码:
typedef struct { CFIndex version; void * info; const void *(*retain)(const void *info); void (*release)(const void *info); CFStringRef (*copyDescription)(const void *info); Boolean (*equal)(const void *info1, const void *info2); CFHashCode (*hash)(const void *info); #if TARGET_OS_OSX || TARGET_OS_IPHONE mach_port_t (*getPort)(void *info); void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info); #else void * (*getPort)(void *info);//操作port void (*perform)(void *info);//回调函数 #endif } CFRunLoopSourceContext1;复制代码
说明:
Source1包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
这种Source 能主动唤醒 RunLoop 的线程
总结:
2.4 CFRunLoopTimerRef对象
CFRunLoopTimerRef是基于时间的触发器,属于时间源
源码:
typedef struct __CFRunLoopTimer * CFRunLoopTimerRef; struct __CFRunLoopTimer { CFRuntimeBase _base; uint16_t _bits; pthread_mutex_t _lock; CFRunLoopRef _runLoop;//所属Runloop CFMutableSetRef _rlModes;//包含timer的mode集合 CFAbsoluteTime _nextFireDate; CFTimeInterval _interval; /* immutable */ CFTimeInterval _tolerance; /* mutable */ uint64_t _fireTSR; /* TSR units */ CFIndex _order; /* immutable */ CFRunLoopTimerCallBack _callout;//timer的回调 CFRunLoopTimerContext _context;//上下文对象 };复制代码
CFRunLoopTimerRef 是基于时间的触发器,也有一个回调函数。
当时间点到达时就可以唤醒Runloop执行这个回调函数
总结:
2.5 CFRunLoopObserverRef对象
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
源码:
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef; struct __CFRunLoopObserver { CFRuntimeBase _base; pthread_mutex_t _lock; CFRunLoopRef _runLoop;//监听的RunLoop CFIndex _rlCount;//添加该Observer的RunLoop对象个数 CFOptionFlags _activities; /* immutable */ CFIndex _order;//同时间最多只能监听一个 CFRunLoopObserverCallBack _callout;//监听的回调 CFRunLoopObserverContext _context;//上下文用于内存管理 };复制代码
观测的时间点有以下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入RunLoop kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop kCFRunLoopAllActivities = 0x0FFFFFFFU };复制代码
总结:
简单使用:
- (IBAction)clickTest:(id)sender { // 创建子线程并开启 NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil]; thread.name = @"wyThread"; self.thread = thread; [self.thread start]; } //在子线程中增加一个RunLoop -(void)show { // 注意:打印方法一定要在RunLoop创建开始运行之前,如果在RunLoop跑起来之后打印,RunLoop先运行起来,已经在跑圈了就出不来了,进入死循环也就无法执行后面的操作了。 // 但是此时点击Button还是有操作的,因为Button是在RunLoop跑起来之后加入到子线程的,当Button加入到子线程RunLoop就会跑起来 NSLog(@"%s",__func__); // 1.创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入 // 添加Source [NSMachPort port] 添加一个端口 [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; // 添加一个Timer NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; //创建监听者 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; } }); // 给RunLoop添加监听者 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);//这里并不是创建RunLoop,只是把当前的RunLoop放到这个线程中 // 2.子线程需要开启RunLoop [[NSRunLoop currentRunLoop] run];//run用来开启RunLoop CFRelease(observer); }复制代码
结果:
2021-11-28 17:42:09.194876+0800 RunloopTest[19348:4762406] -[ViewController show] 2021-11-28 17:42:09.195422+0800 RunloopTest[19348:4762406] RunLoop进入 2021-11-28 17:42:09.195535+0800 RunloopTest[19348:4762406] RunLoop要处理Timers了 2021-11-28 17:42:09.195626+0800 RunloopTest[19348:4762406] RunLoop要处理Sources了 2021-11-28 17:42:09.195745+0800 RunloopTest[19348:4762406] RunLoop要睡眠了 2021-11-28 17:42:11.197577+0800 RunloopTest[19348:4762406] RunLoop被唤醒了 2021-11-28 17:42:11.197860+0800 RunloopTest[19348:4762406] <NSThread: 0x600001f9c600>{number = 9, name = wyThread} 2021-11-28 17:42:11.197988+0800 RunloopTest[19348:4762406] RunLoop要处理Timers了 2021-11-28 17:42:11.198094+0800 RunloopTest[19348:4762406] RunLoop要处理Sources了 2021-11-28 17:42:11.198335+0800 RunloopTest[19348:4762406] RunLoop要睡眠了 2021-11-28 17:42:13.200238+0800 RunloopTest[19348:4762406] RunLoop被唤醒了 2021-11-28 17:42:13.200582+0800 RunloopTest[19348:4762406] <NSThread: 0x600001f9c600>{number = 9, name = wyThread} ...复制代码
2.6 RunLoop类的关系
上面逐个认识了每个对象,接下来这些对象的相互关系
说明:
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。
每次调用 RunLoop的主函数时,只能指定其中一个 Mode,这个Mode被称作CurrentMode。
如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
如果一个 mode中一个Source/Timer/Observer 都没有,则RunLoop会直接退出,不进入循环。
3、RunLoop和线程的关系
上文我们知道Runloop是作用于线程的,这里着重看下是如何作用的。
一般在日常开发中,对于RunLoop的获取主要有以下两种方式
// 主运行循环 CFRunLoopRef mainRunloop = CFRunLoopGetMain(); // 当前运行循环 CFRunLoopRef currentRunloop = CFRunLoopGetCurrent();复制代码
接下来通过CFRunLoopGetMain()来探索
3.1 探索RunLoop和线程的关系
3.1.1 进入CFRunLoopGetMain源码
源码:
//得到主RunLoop CFRunLoopRef CFRunLoopGetMain(void) { CHECK_FOR_FORK(); static CFRunLoopRef __main = NULL; // no retain needed //这里的pthread_main_thread_np()就是主线程,通过_CFRunLoopGet0得到主线程的RunLoop if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed return __main; }复制代码
说明:
返回的是一个RunLoop,通过CFRunLoopRef接收
这里做的是调用_CFRunLoopGet0函数通过主线程获取RunLoop
3.1.2 进入_CFRunLoopGet0
源码:
//通过线程获取RunLoop CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {//这里也可以看到传入了一个线程pthread_t //如果t不存在,则标记为主线程(即默认情况,默认是主线程) if (pthread_equal(t, kNilPthreadT)) { t = pthread_main_thread_np(); } __CFLock(&loopsLock); //如果__CFRunLoops不存在,也就是当前没有任何一个RunLoop if (!__CFRunLoops) { __CFUnlock(&loopsLock); //1、创建全局字典CFMutableDictionaryRef,标记为kCFAllocatorSystemDefault CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); //2、通过主线程生成一个主RunLoop CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // 3、进行绑定 dict[@"pthread_main_thread_np"] = mainLoop //线程指针和RunLoop分别作为key和value存储到一个可变字典中,可以说明是一一对应关系 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } //如果已经有__CFRunLoops了,就通过线程指针在__CFRunLoops获取对应的RunLoop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); //发现如果不存在,也就是该线程还没有RunLoop,再创建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); } if (pthread_equal(t, pthread_self())) { _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL); if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop); } } return loop; }复制代码
说明:
RunLoop是基于线程来管理的,它们一一对应,共同存储在一个全局区的runLoopDict中,线程是key,RunLoop是value。
RunLoop的创建:主线程所对应RunLoop在程序一启动创建主线程的时候系统就会自动为我们创建好,子线程所对应的RunLoop并不是在子线程创建出来的时候就创建好的,而是在我们使用该子线程所对应的RunLoop时才创建出来的,换句话说,如果你不获取一个子线程的RunLoop,那么它的RunLoop就永远不会被创建。
RunLoop的获取:我们可以通过一个指定的线程从runLoopDict中获取它所对应的RunLoop。
RunLoop的销毁:系统在创建RunLoop的时候,会注册一个回调,确保线程在销毁的同时,也销毁掉其对应的RunLoop。
3.2 子线程使用RunLoop
代码:
- (void)viewDidLoad { [super viewDidLoad]; // 子线程runloop 默认不启动 self.isStopping = NO; LGThread *thread = [[LGThread alloc] initWithBlock:^{ NSLog(@"%@---%@",[NSThread currentThread],[[NSThread currentThread] name]); //通过控制变量isStopping可以停止当前线程,线程停止,RunLoop也就结束,RunLoop结束NSTimer也就停止了 [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"hello word"); // 退出线程--结果runloop也停止了 if (self.isStopping) { [NSThread exit]; } }]; [[NSRunLoop currentRunLoop] run]; }]; thread.name = @"wy.com"; [thread start]; } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ self.isStopping = YES; }复制代码
运行结果:
2021-11-27 21:21:55.106242+0800 02-Runloop与线程的关系[78338:4082258] <LGThread: 0x600000c44040>{number = 7, name = wy.com}---wy.com 2021-11-27 21:21:56.109209+0800 02-Runloop与线程的关系[78338:4082258] hello word 2021-11-27 21:21:57.109159+0800 02-Runloop与线程的关系[78338:4082258] hello word 2021-11-27 21:21:58.108583+0800 02-Runloop与线程的关系[78338:4082258] hello word 2021-11-27 21:22:15.109605+0800 02-Runloop与线程的关系[78338:4082258] -[LGThread dealloc]---线程销毁了复制代码
说明:
当线程开启后,我们如果不通过[NSRunLoop currentRunLoop]获取RunLoop,子线程是没有RunLoop的
有RunLoop也并不会直接开启,需要通过run方法来启动
NSTimer是在子线程中的,而我们知道NSTimer是基于RunLoop实现的,如果不开启子线程的RunLoop就无法启动计时
当启动子线程的RunLoop后,NSTimer会不断执行。不断的打印hello word
我们想要停止执行RunLoop时,就将线程销毁掉,线程销毁RunLoop销毁,NSTimer也就停止执行了。
4、Runloop的执行过程
4.1 事件添加过程
以时间源添加为例源码:
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) { CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return; if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return; __CFRunLoopLock(rl); /* 1、如果添加到kCFRunLoopCommonModes */ if (modeName == kCFRunLoopCommonModes) { //如果不存在,就先将生成一个_commonModes CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL; if (NULL == rl->_commonModeItems) { rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks); } //将rlt添到RunLoop的_commonModeItems中 CFSetAddValue(rl->_commonModeItems, rlt); if (NULL != set) { CFTypeRef context[2] = {rl, rlt}; /* add new item to all common-modes */ //添加新的item到所有的set集合中 CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context); CFRelease(set); } } else { /* 2、添加到非commonMode类型 */ //通过名称获取到mode CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true); //如果是第一次设置NSTimer,则创建_timers if (NULL != rlm) { if (NULL == rlm->_timers) { CFArrayCallBacks cb = kCFTypeArrayCallBacks; cb.equal = NULL; rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb); } } //判断是否匹配 if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) { __CFRunLoopTimerLock(rlt); if (NULL == rlt->_runLoop) { rlt->_runLoop = rl; } else if (rl != rlt->_runLoop) { __CFRunLoopTimerUnlock(rlt); __CFRunLoopModeUnlock(rlm); __CFRunLoopUnlock(rl); return; } //添加mode到Timer中 CFSetAddValue(rlt->_rlModes, rlm->_name); __CFRunLoopTimerUnlock(rlt); __CFRunLoopTimerFireTSRLock(); __CFRepositionTimerInMode(rlm, rlt, false); __CFRunLoopTimerFireTSRUnlock(); if (!_CFExecutableLinkedOnOrAfter(CFSystemVersionLion)) { // Normally we don't do this on behalf of clients, but for // backwards compatibility due to the change in timer handling... if (rl != CFRunLoopGetCurrent()) CFRunLoopWakeUp(rl); } } if (NULL != rlm) { __CFRunLoopModeUnlock(rlm); } } __CFRunLoopUnlock(rl); }复制代码
说明:
可以看到有两种,一种添加到kCFRunLoopCommonModes,一种是非kCFRunLoopCommonModes
第一种情况需要添加到RunLoop的_commonModeItems中,这样就称为公共的Item
第二种情况需要创建一个NSTimer对象,并将其中的RunLoop设置为当前RunLoop,再设置mode也设置到NSTimer中
4.2 事件执行过程
上层是通过run方法来执行的,我们先打印来看下
4.2.1 查看堆栈信息
计时器
GCD主队列:
注:子线程默认没有RunLoop,但可以手动添加,下面会给子线程添加RunLoop来验证
block
事件响应
实现监听Observer
4.2.2 RunLoop的所有响应类型
上面可以看到对于一些事件在底层是RunLoop来响应的。那么RunLoop都有哪些响应呢?
可以在源码中查看一下
//主线程队列 static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() __attribute__((noinline)); static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(void *msg) { _dispatch_main_queue_callback_4CF(msg); asm __volatile__(""); // thwart tail-call optimization } //Observer监听的响应 static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline)); static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) { if (func) { func(observer, activity, info); } asm __volatile__(""); // thwart tail-call optimization } //Timer时间源的响应 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__() __attribute__((noinline)); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(CFRunLoopTimerCallBack func, CFRunLoopTimerRef timer, void *info) { if (func) { func(timer, info); } asm __volatile__(""); // thwart tail-call optimization } //block的响应 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__() __attribute__((noinline)); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(void (^block)(void)) { if (block) { block(); } asm __volatile__(""); // thwart tail-call optimization } //事件源0的响应 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__() __attribute__((noinline)); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) { if (perform) { perform(info); } asm __volatile__(""); // thwart tail-call optimization } //事件源1的响应 static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__() __attribute__((noinline)); static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__( #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info), mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply, #else void (*perform)(void *), #endif void *info) { if (perform) { #if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI *reply = perform(msg, size, kCFAllocatorSystemDefault, info); #else perform(info); #endif } asm __volatile__(""); // thwart tail-call optimization }复制代码
总结
我们可以看到所有响应类型有6种
每种响应的过程是一样的,只是最后执行的不一样类型不一样
在底层其实是通过CoreFoundation框架来执行的
执行过程是CFRunLoopRunSpecific->__CFRunLoopRun->__CFRunLoopDoXXX->最后的执行
接下来在底层就按照这个顺序来查看执行的具体过程
上层执行run方法在底层其实是CFRunLoopRun()
4.2.3 查看源码CFRunLoopRun()
上层执行run方法在底层其实是CFRunLoopRun()
源码
//runLoop的启动,可以看到就是do...while循环 void CFRunLoopRun(void) { /* DOES CALLOUT */ int32_t result; do { result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false); CHECK_FOR_FORK(); } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result); }复制代码
说明:
这里很明显看到RunLoop的执行其实就是一个do...while循环,只不过是一个闲等待的循环
4.2.4 CFRunLoopRunSpecific()
源码:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); //首先根据modeName找到对应mode 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; } volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl); //再拿到先前的mode CFRunLoopModeRef previousMode = rl->_currentMode; //将获取的新的mode赋给当前的RunLoop rl->_currentMode = currentMode; int32_t result = kCFRunLoopRunFinished; //1、通知 Observers: RunLoop 即将进入 loop。 if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); //2、对RunLoop进行run,也就是启动RunLoop,开始跑圈 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 通知 Observers: RunLoop 即将退出。 if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result; }复制代码
说明:
这里有三种情况,第一种是启动RunLoop,第二种是对某种事件源进行响应,第三种是结束RunLoop
启动RunLoop和结束RunLoop都是通过Observer来监听实现的
RunLoop的mode是通过mode名称来查询的,查询到后再赋值给当前的RunLoop
4.2.5 __CFRunLoopRun()
这个函数代码较多,而且我们只需要看核心的即可,所以只摘出来核心代码
源码:
//核心函数 /* rl, rlm are locked on entrance and exit */ static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){ //通过GCD开启一个定时器,然后开始跑圈 dispatch_source_t timeout_timer = NULL; ... dispatch_resume(timeout_timer); int32_t retVal = 0; //处理事务,即处理items do { // 通知 Observers: 即将处理timer事件 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 通知 Observers: 即将处理Source事件 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources) // 处理Blocks __CFRunLoopDoBlocks(rl, rlm); // 处理sources0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); // 处理sources0返回为YES if (sourceHandledThisLoop) { // 处理Blocks __CFRunLoopDoBlocks(rl, rlm); } // 判断有无端口消息(Source1) if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { // 处理消息 goto handle_msg; } // 通知 Observers: 即将进入休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); // 等待被唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY); // user callouts now OK again __CFRunLoopUnsetSleeping(rl); // 通知 Observers: 被唤醒,结束休眠 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); handle_msg: if (被timer唤醒) { // 处理Timers __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()); }else if (被GCD唤醒){ // 处理gcd __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); }else if (被source1唤醒){ // 被Source1唤醒,处理Source1 __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) } // 处理block __CFRunLoopDoBlocks(rl, rlm); 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; }复制代码
说明:
这里通过do...while循环对所有的事件源进行循环处理,可以看到整个的循环过程
do...while循环可以保持持续运行
通过休眠和唤醒可以节省CPU资源,这一轮的消息处理完之后就开始进入休眠状态
一直等到被NSTimer、GCD、source1等事件进行唤醒就继续处理事件
判断如果是Source1,会主动进入到handle_msg唤醒
NSTimer的时间点到了之后也会主动唤醒进入到handle_msg唤醒
4.2.6 以Block执行为例
源码:
static Boolean __CFRunLoopDoBlocks(CFRunLoopRef rl, CFRunLoopModeRef rlm) { // Call with rl and rlm locked ... CFSetRef commonModes = rl->_commonModes; CFStringRef curMode = rlm->_name; struct _block_item *prev = NULL; struct _block_item *item = head; while (item) { struct _block_item *curr = item; item = item->_next; Boolean doit = false; if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) { //条件一:block所处的mode == 当前的mode //条件二:block所处的mode == commonMode,并且block所处的mode添加到了公共mode中 doit = CFEqual(curr->_mode, curMode) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)); } else { doit = CFSetContainsValue((CFSetRef)curr->_mode, curMode) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(commonModes, curMode)); } if (!doit) prev = curr; if (doit) { if (prev) prev->_next = item; if (curr == head) head = item; if (curr == tail) tail = prev; void (^block)(void) = curr->_block; CFRelease(curr->_mode); free(curr); if (doit) { //开始执行block __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block); did = true; } Block_release(block); // do this before relocking to prevent deadlocks where some yahoo wants to run the run loop reentrantly from their dealloc } } ... return did; }复制代码
说明:
rl是当前线程的RunLoop,rlm是当前RunLoop的mode,item是取出runloop中的modeItem
判断执行的条件有两种,第一种:事件源所在的mode是否是当前的mode,如果是就可以执行
第二种,事件源所在的mode是否是CommonModes,并且当前mode也属于CommonModes,如果是就可以执行
如果doit为YES,也就是上面的条件满足,就可以执行__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);。这和我们上面查看的堆栈信息是一致的
4.2.7 总结
黄色:表示通知 Observer 各个阶段;
蓝色:处理消息的逻辑;
绿色:分支判断逻辑;
我们将一个事件添加到RunLoop中,有两种情况
第一种:添加到CommonModes中,此时是将事件添加到CommonModesItems,作为公共事件,被标记为common的Item,相当于属于被标记为common的任何mode。
第二种:添加到非CommonModes中。此时先获取到这个mode,将时间源添加到这个mode中的_timers数组中,并且在时间源对象__CFRunLoopTimer中也设置相应的mode和RunLoop。
在执行RunLoop时,所有的事件源都是在do...while循环中不断执行的
当我们run启动RunLoop时,会先启动RunLoop,之后一直处在循环中,处理事件,处理结束后进行休眠,在休眠时被唤醒继续执行,执行完再睡眠,以此循环往复
我们在启动循环、结束循环,事件处理的过程中都会被Observer进行监听
事件的处理有三种,Source0,Source1,Timer
Source0不会主动唤醒,Timer和Source1、以及主队列可以唤醒
5、常见使用
5.1 常驻流程
当我们创建一个子线程时,当子线程任务执行完成后线程便销毁了。这是因为子线程默认是没有RunLoop的,想要让子线程可以持续运行,可以给加一个RunLoop并启动,就属于常驻线程了。
5.2 NSTimer的使用
NSTimer的使用是基于RunLoop的,而这个RunLoop有多种运行模式mode
不同的mode在不同的情形下有不同的响应事件
在滑动时会从默认mode切换到滑动mode,而主线程中NSTimer是默认添加到默认mode中,所以在滑动mode时并不会响应NSTimer
可以放到CommonModes中,因为系统提供的滑动mode和默认mode都默认属于CommonModes中,而我们将一个事件放到CommonModeItems时,只要标记为Common的mode都会去响应这个事件
CADisplayLink是内核计算的时间,与RunLoop无关,因此会准时
5.3 AutoreleasePool
自动释放池是依赖于线程的RunLoop的,RunLoop启动时创建一个池子,RunLoop休眠时释放掉这个池子
RunLoop有Observer进行监听,当监听到将要启动池子时,会进行线程池的开辟,而且优先级是最高的,因为执行其他的事务都有可能使用到池子
当监听到将要销毁池子时,会进行线程池的销毁,而且优先级最低,其他事务执行完成后最后回收垃圾。
5.4 事件响应、手势识别、界面刷新
应用层面的事件响应都是Source1来实现的,当RunLoop处于唤醒状态会执行事件。 系统层面的响应是由Source0,其他线程通过端口向该线程发送msg,之后就唤醒这个线程的Runloop了
5.5. 主线程的使用是通过GCD来唤醒的
在源码中可以看到主线程的执行会唤醒RunLoop,主队列是基于RunLoop启动的
5.6 Block的执行是基于Runloop的
在源码中可以看到Runloop的执行过程中会主动的执行Block
作者:文乙
链接:https://juejin.cn/post/7035556124543680519
伪原创工具 SEO网站优化 https://www.237it.com/