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