NSRunLoop
是iOS
中的消息处理机制,执行完某个事件后线程不会退出,而是进入休眠状态,当再次监测到须要出发事件时,线程激活,继续处理事件,处理完成后再次进入休眠while
循环RunLoop
, 由于cocoa
框架为咱们建立了一个默认的RunLoop
RunLoop
的主要做用
App
中的各类事件(手势、定时器、Selector
等)CPU
资源、提升程序性能:该作任务的时候作任务,没事干的时候休息RunLoop
和线程的关系
RunLoop
对象RunLoop
保存在一个全局的Dictionary
里, 线程做为key
, RunLoop
做为value
RunLoop
对象, RunLoop
会在第一次获取线程时建立RunLoop
会在线程结束时销毁RunLoop
已经自动获取(建立), 子线程默认没有开启RunLoop
iOS
开发中RunLoop
有两套API
框架, 分别是
Foundation
的NSRunLoop
Core Foundation
的CFRunLoopRef
CFRunLoopRef
是基于C
语言的开源框架, 有兴趣的能够到源码地址下载源码, 不过没有C
语言功底的只怕很难看懂NSRunLoop
是对CFRunLoopRef
的有一层封装, 是OC
语法的框架RunLoop
对象// 获取当前线程的RunLoop [NSRunLoop currentRunLoop]; // 获取主线程的RunLoop [NSRunLoop mainRunLoop]; // 获取主线程的RunLoop CFRunLoopGetMain(); // 获取当前线程的RunLoop CFRunLoopGetCurrent(); 复制代码
NSRunLoop
是不开源的, 可是CFRunLoopRef
倒是开源的, 从源码地址下载CFRunLoopRef
的源码Core Foundation
中CFRunLoopRef
有如下5个相关的类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopRef
CFRunLoopRef
对象的主要核心代码以下html
typedef struct __CFRunLoop * CFRunLoopRef; struct __CFRunLoop { // 线程对象 pthread_t _pthread; // 无序集合 CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; // 当前mode CFRunLoopModeRef _currentMode; CFMutableSetRef _modes; }; 复制代码
主要属性介绍前端
CFMutableSetRef
是一个无序的集合, 在上面的代码中存储的都是CFRunLoopModeRef
对象_modes
存储的是全部的mode
对象_currentMode
是指当前的mode
CFRunLoopModeRef
typedef struct __CFRunLoopMode *CFRunLoopModeRef; struct __CFRunLoopMode { // mode名称 CFStringRef _name; Boolean _stopped; CFMutableSetRef _sources0; CFMutableSetRef _sources1; CFMutableArrayRef _observers; CFMutableArrayRef _timers; }; 复制代码
_name
: 该__CFRunLoopMode
的名称_sources0
和_sources1
: 一个无序集合, 存储的都是CFRunLoopSourceRef
对象_observers
: 一个有序集合数组,存储的都是CFRunLoopObserverRef
对象_timers
: 一个有序集合数组,存储的都是CFRunLoopTimerRef
对象CFRunLoopModeRef
表明RunLoop
的运行模式RunLoop
只能对应一个线程, 却包含若干个Mode
,每一个Mode
又包含若干个Source0/Source1/Timer/Observer
RunLoop
启动时只能选择其中一个Mode
,做为currentMode
一样只能有一个Mode
,只能退出当前Loop
,再从新选择一个Mode
进入, 不一样组的Source0/Source1/Timer/Observer
能分隔开来,互不影响Mode
里没有任何Source0/Source1/Timer/Observer
,RunLoop
会立马退出mode
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode; FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes; UIKIT_EXTERN NSRunLoopMode const UITrackingRunLoopMode; CF_EXPORT const CFRunLoopMode kCFRunLoopDefaultMode; CF_EXPORT const CFRunLoopMode kCFRunLoopCommonModes; 复制代码
kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
):App
的默认Mode
,一般主线程是在这个Mode
下运行UITrackingRunLoopMode
:界面跟踪Mode
,用于ScrollView
追踪触摸滑动,保证界面滑动时不受其余Mode
影响kCFRunLoopCommonModes
(NSRunLoopCommonModes
): 并非某一种特定的mode
, 而是通用模式, 包括kCFRunLoopDefaultMode
和UITrackingRunLoopMode
CFRunLoopObserverRef
是观察者,可以监听RunLoop
全部的状态改变。 能够监听的时间点有以下几种:数组
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { // 即将进入Loop kCFRunLoopEntry = (1UL << 0), // 即将处理Timer kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Source kCFRunLoopBeforeSources = (1UL << 2), // 即将进入休眠 kCFRunLoopBeforeWaiting = (1UL << 5), // 刚从休眠中唤醒 kCFRunLoopAfterWaiting = (1UL << 6), // 即将退出Loop kCFRunLoopExit = (1UL << 7), // 全部状态 kCFRunLoopAllActivities = 0x0FFFFFFFU }; 复制代码
在主线程监听全部的状态bash
// 建立observer CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"kCFRunLoopEntry"); break; case kCFRunLoopBeforeTimers: NSLog(@"kCFRunLoopBeforeTimers"); break; case kCFRunLoopBeforeSources: NSLog(@"kCFRunLoopBeforeSources"); break; case kCFRunLoopBeforeWaiting: NSLog(@"kCFRunLoopBeforeWaiting"); break; case kCFRunLoopAfterWaiting: NSLog(@"kCFRunLoopAfterWaiting"); break; case kCFRunLoopExit: NSLog(@"kCFRunLoopExit"); break; default: break; } }); // 吧observer添加到RunLoop中 CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); // 释放 CFRelease(observer); 复制代码
从上图能够看出消息类型大概能够分出两种, 第一种类型又能够细分为三种, 这三种都是异步执行的微信
监听程序的mach ports
,ports
能够简单的理解为:内核经过port
这种方式将信息发送,而mach
则监听内核发来的port
信息,而后将其整理,打包发给runloop
markdown
由开发人员本身发送, 苹果也提供了一个CFRunLoopSource
来帮助处理, 简单介绍核心实:数据结构
runloop
可以处理的样式,即第一步定义的输入源。它相似Mach
的功能NSObject类提供了不少方法供咱们使用,这些方法是添加到runloop的,因此若是没有开启runloop的话,不会运行架构
/// 主线程 - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait; /// 指定线程 - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array; - (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait; /// 针对当前线程, 延迟调用 - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes; - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay; /// 取消,在当前线程,和上面两个方法对应 + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument; + (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget; 复制代码
aSelector
, 通常状况下aSelector
会添加到指定线程的runloop
wait
参数设为YES
,那么aSelector
会直接在指定线程运行,再也不添加到runloop
;wait
参数设为YES
, 意味着要等待aSelector
执行完成以后才回去执行后面的逻辑根据苹果在文档里的说明,RunLoop
内部的逻辑大体以下:app
未查看RunLoop
的执行流程, 咱们能够新建一个项目, 并简单加一个触发事件, 以下所示框架
bt
命令后, 就能看到完整的执行流程了UIApplicationMain
CFRunLoopRunSpecific
__CFRunLoopRun
__CFRunLoopDoSources0
[UIResponder touchesBegan:withEvent:]
触发函数了CFRunLoop.c
文件, 搜索CFRunLoopRunSpecific
方法, 就是核心代码了, 一块儿来看看吧/// RunLoop的实现, 大概在文件的2622行 SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { { /// 首先根据modeName找到对应mode CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false); /// 1. 通知 Observers: RunLoop 即将进入 loop。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); /// __CFRunLoopRun中具体要作的事情 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); /// 11. 通知 Observers: RunLoop 即将退出。 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); return result; } // __CFRunLoopRun的实现, 进入loop, 大概在文件的2304行 static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) { int32_t retVal = 0; do { // 2. 通知 Observers: RunLoop 即将触发 Timer 回调。 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers); // 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources); // 4. 处理block __CFRunLoopDoBlocks(rl, rlm); // 5. 处理Source0 Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); // 若是处理Source0的结果是rrue if (sourceHandledThisLoop) { // 再次处理block __CFRunLoopDoBlocks(rl, rlm); } Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR); // 6. 若是有Source1 (基于port) 处于ready状态,直接处理这个Source1而后跳转去处理消息。 if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) { // 若是有Source1, 就跳转到handle_msg goto handle_msg; } // 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); // 7. 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程 // 一个基于 port 的Source 的事件。 // 一个 Timer 到时间了 // RunLoop 自身的超时时间到了 // 被其余什么调用者手动唤醒 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy); __CFRunLoopUnsetSleeping(rl); // 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了 __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 收到消息,处理消息。 handle_msg:; if (/* 被timer唤醒 */) { // 01. 处理Timer __CFRunLoopDoTimers(rl, rlm, mach_absolute_time()) } else if (/* 被gcd唤醒 */) { // 02. 处理gcd __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); } else { // 被Source1唤醒 // 处理Source1 sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop; } // 9. 处理Blocks __CFRunLoopDoBlocks(rl, rlm); // 10. 设置返回值, 根据不一样的结果, 处理不一样操做 if (sourceHandledThisLoop && stopAfterHandle) { // 进入loop时参数说处理完事件就返回。 retVal = kCFRunLoopRunHandledSource; } else if (timeout_context->termTSR < mach_absolute_time()) { // 超出传入参数标记的超时时间了 retVal = kCFRunLoopRunTimedOut; } else if (__CFRunLoopIsStopped(rl)) { // 被外部调用者强制中止了 retVal = kCFRunLoopRunStopped; } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) { // source/timer/observer一个都没有了 retVal = kCFRunLoopRunFinished; } // 若是没超时,mode里没空,loop也没被中止,那继续loop。 } while (0 == retVal); return retVal; } 复制代码
从上面的代码能够看到RunLoop
其内部是一个do-while
循环; 当你调用CFRunLoopRun()
时,线程就会一直停留在这个循环里;直到超时或被手动中止,该函数才会返回
RunLoop
的核心是基于mach port
的,其进入休眠时调用的函数是mach_msg()
Mach
自己提供的API
很是有限,并且苹果也不鼓励使用Mach
的API
API
很是基础,若是没有这些API
的话,其余任何工做都没法实施Mach
中,全部的东西都是经过本身的对象实现的,进程、线程和虚拟内存都被称为”对象”Mach
的对象间不能直接调用,只能经过消息传递的方式实现对象间的通讯。Mach
中最基础的概念,消息在两个端口 (port
) 之间传递,这就是Mach
的IPC
(进程间通讯) 的核心。Mach
的消息定义是在<mach/message.h>
头文件的,很简单:
typedef struct { mach_msg_header_t header; mach_msg_body_t body; } mach_msg_base_t; typedef struct { mach_msg_bits_t msgh_bits; mach_msg_size_t msgh_size; mach_port_t msgh_remote_port; mach_port_t msgh_local_port; mach_port_name_t msgh_voucher_port; mach_msg_id_t msgh_id; } mach_msg_header_t; 复制代码
Mach
消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口local_port
和目标端口remote_port
API
进行的,其option
标记了消息传递的方向:mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify
);
复制代码
mach_msg()
函数其实是调用了一个Mach
陷阱(trap
),即函数mach_msg_trap()
,陷阱这个概念在Mach
中等同于系统调用mach_msg_trap()
时会触发陷阱机制,切换到内核态;内核态中内核实现的mach_msg()
函数会完成实际的工做mach_msg()
, 若是没有消息就让线程休眠,有消息就唤醒线程RunLoop
的核心就是一个mach_msg()
(见上面代码的第7步),RunLoop
调用这个函数去接收消息,若是没有别人发送port
消息过来,内核会将线程置于等待状态iOS
的App
,而后在App
静止时点击暂停,你会看到主线程调用栈是停留在mach_msg_trap()
这个地方NSRunLoop
应用实践解决NSTimer
在滑动时中止工做的问题
CFRunLoopMode
主要使用的通常有三种Mode
DefaultMode
是App
平时所处的状态,TrackingRunLoopMode
是追踪ScrollView
滑动时的状态Timer
并加到DefaultMode
时,Timer
会获得重复回调,但此时滑动一个TableView
时,RunLoop
会将mode
切换为TrackingRunLoopMode
,这时Timer
就不会被回调,而且也不会影响到滑动操做#import "ViewController.h" @interface ViewController () // 在xib中添加一个可滚动的UITextView @property (weak, nonatomic) IBOutlet UITextView *textView; @property (assign, nonatomic) NSInteger timerCount; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // 添加一个定时器 [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerClick) userInfo:nil repeats:YES]; } - (void)timerClick { NSLog(@"--------%ld", (long)self.timerCount++); } - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { NSLog(@"touchesBegan"); } @end 复制代码
scheduledTimerWithTimeInterval
方式添加的NSTimer
会默认被添加到DefaultMode
中UITextView
的时候, 从打印结果能够看到, 定时器中止执行了, 结束滚动UITextView
的时候, 定时器方法会继续执行2019-08-20 21:46:01.986843+0800 RunLoop[86811:3484205] --------0
2019-08-20 21:46:02.986723+0800 RunLoop[86811:3484205] --------1
2019-08-20 21:46:03.986040+0800 RunLoop[86811:3484205] --------2
2019-08-20 21:46:04.986274+0800 RunLoop[86811:3484205] --------3
2019-08-20 21:46:05.272525+0800 RunLoop[86811:3484205] touchesBegan
2019-08-20 21:46:12.291035+0800 RunLoop[86811:3484205] --------4
2019-08-20 21:46:12.986318+0800 RunLoop[86811:3484205] --------5
2019-08-20 21:46:13.986197+0800 RunLoop[86811:3484205] --------6
2019-08-20 21:46:14.986735+0800 RunLoop[86811:3484205] --------7
复制代码
Timer
,在两个Mode
中都能获得回调Timer
分别加入这两个Mode
Timer
加入到顶层的RunLoop
的commonModeItems
中commonModeItems
被RunLoop
自动更新到全部具备Common
属性的Mode
里去CommonModes
并非一个真的模式,它只是一个标记NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { NSLog(@"--------%ld", (long)self.timerCount++); }]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 复制代码
欢迎您扫一扫下面的微信公众号,订阅个人博客!