RunLoop源码剖析---图解RunLoop
源码面前,了无秘密
前言
我们在iOS APP
中的main
函数如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
我们在macOS
下的main
函数如下:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
-
对比这两个程序:
-
iOS App
启动后会一直运行,等待用户触摸、输入等,在接收到点击后,就会立即响应,完成本次响应后,会等待下次用户操作。只要用户不主动退出或者程序闪退,会一直在循环等待。 - 而
macOS
下的命令行程序,启动后,执行程序,执行完毕后会立即退出。
-
-
两者最大的区别是:是否能持续响应用户输入
什么是RunLoop?
- 之所以,
iOS App
能持续响应,保证程序运行状态,在于其有一个事件循环——Event Loop
- 事件循环机制,即线程能随时响应并处理事件的机制。这种机制要求线程不能退出,而且需要高效的完成事件调度与处理。
- 事件循环在很多编程语言,或者说不同的操作系统层面都支持。比如
JS
中的事件循环、Windows
下的消息循环,在iOS/macOS
下,该机制就称为RunLoop
。
如果大家对上面的专业术语不太了解,下面我举一个生活中的🌰
进程是一家工厂,线程是一个流水线,
RunLoop
就是流水线上的主管;当工厂接到商家的订单分配给这个流水线时,RunLoop
就启动这个流水线,让流水线动起来,生产产品;当产品生产完毕时,RunLoop
就会暂时停下流水线,节约资源。
RunLoop
管理流水线,流水线才不会因为无所事事被工厂销毁;而不需要流水线时,就会辞退RunLoop
这个主管,即退出线程,把所有资源释放。
- 事件循环在本质上是如下一个编程实现:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
-
RunLoop
实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面Event Loop
的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入quit
的消息),函数返回。 - 下面我们用一张图来看看这个过程
RunLoop的作用
在这里我会先简单介绍一下RunLoop的作用,有一个总体的印象,然后我会在后面仔细给大家介绍它的每个作用,和部分作用的一些应用场景。
-
保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的
RunLoop
,RunLoop
保证主线程不会被销毁,也就保证了程序的持续运行 -
处理App中的各种事件:
- 定时器(
Timer
)、方法调用(PerformSelector
) GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- 自动释放池
AutoreleasePool
- 定时器(
-
节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,
RunLoop
就告诉CUP
,现在没有事情做,我要去休息,这时CUP
就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop
就会立马起来去做事情
RunLoop在何处开启?
- 我们还记得前言中那个
main
函数么?不记得也不要紧,我把它再次贴出来
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
- 我们知道主线程一开起来,就会跑一个和主线程对应的
RunLoop
,那么我们猜测RunLoop
一定是在程序的入口main
函数中开启。 - 我们进入
UIApplicationMain
UIKIT_EXTERN int UIApplicationMain(int argc, char * _Nullable argv[_Nonnull], NSString * _Nullable principalClassName, NSString * _Nullable delegateClassName);
- 我们发现它返回的是一个int数,那么我们对main函数做一些修改
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"开始");
int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"结束");
return result;
}
}
- 运行程序,我们发现只会打印开始,并不会打印结束,这说明在
UIApplicationMain
函数中,开启了一个和主线程相关的RunLoop
,导致UIApplicationMain
不会返回,一直在运行中,也就保证了程序的持续运行。 - 下面我把上面几个点用一张图总结一下
RunLoop对象
RunLoop对象的获取
- 我在前面提到过RunLoop其实也是一个对象,下面我们来介绍一下它
Fundation框架 (基于CFRunLoopRef的封装)
NSRunLoop对象
-
NSRunLoop
是基于CFRunLoopRef
的封装,提供了面向对象的API
,但是这些API
不是线程安全的。
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
CoreFoundation
CFRunLoopRef对象
-
CFRunLoopRef
是在CoreFoundation
框架内的,它提供了纯C
函数的API
,所有这些API
都是线程安全的。
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
- 我们通过一张图对上面知识的进行总结一下
- 我们来看看上面两个函数的底层实现
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
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;
}
- 我们发现不论是
CFRunLoopGetCurrent()
还是CFRunLoopGetMain()
的底层实现都会调用一个_CFRunLoopGet0
函数,那么这个函数到底是怎么实现的呢?我在这里先留一个悬念,我会在RunLoop和线程中仔细讲解。
CFRunLoopRef对象源码剖析
- 由于
NSRunLoop
对象是基于CFRunLoopRef
的,并且CFRunLoopRef
是基于c
语言的,线程安全,所以我们来分析一下CFRunLoopRef
的源码
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock;
__CFPort _wakeUpPort; //通过该函数CFRunLoopWakeUp内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData;
pthread_t _pthread;//RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode;//当前运行的mode
CFMutableSetRef _modes;//存储的是CFRunLoopModeRef
struct _block_item *_blocks_head;//do blocks的时候用到
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
- 除了记录一些属性外,我们主要来看这两个成员变量
pthread_t _pthread;//RunLoop对应的线程
CFRunLoopModeRef _currentMode;//当前运行的mode
CFMutableSetRef _modes;//存储的是CFRunLoopModeRef
-
它为什么需要记录线程呢?加着上面的悬念,种种迹象都表明
RunLoop
和线程中有着千丝万缕的联系。我会在RunLoop
和线程这一节中仔细讲解。 -
_currentMode
和_modes
又是什么东西呢?CFRunLoopModeRef
其实是指向__CFRunLoopMode
结构体的指针,所以RunLoop
和Mode
又有什么不可告人的秘密呢?我会在RunLoop的Mode
这一节中仔细讲解
RunLoop和线程
在上面我们留下了两个问题,现在我们会在这一节中解释
_CFRunLoopGet0
这个函数是怎么实现的,和RunLoop和线程
到底有什么关系。
- 首先我们看看
_CFRunLoopGet0
源码
// 全局的Dictionary,key是pthread_t, value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
// 访问__CFRunLoops的锁
static CFLock_t loopsLock = CFLockInit;
// 获取pthread 对应的 RunLoop。
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
// pthread为空时,获取主线程
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
// 第一次进入时,创建一个临时字典dict
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
//释放dict
CFRelease(dict);
}
//释放mainrunloop
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建
// 从全局字典里获取对应的RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果取不到,就创建一个新的RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
if (!loop) {
//把newLoop存入字典__CFRunLoops,key是线程t
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
//如果传入线程就是当前线程
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
//注册一个回调,当线程销毁时,销毁对应的RunLoop
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
- 通过以上代码我们可以得出一下结论
- RunLoop是基于线程来管理的,它们一一对应,共同存储在一个全局区的runLoopDict中,线程是key,RunLoop是value。
- RunLoop的创建:主线程所对应RunLoop在程序一启动创建主线程的时候系统就会自动为我们创建好,而子线程所对应的RunLoop并不是在子线程创建出来的时候就创建好的,而是在我们获取该子线程所对应的RunLoop时才创建出来的,换句话说,如果你不获取一个子线程的RunLoop,那么它的RunLoop就永远不会被创建。
- RunLoop的获取:我们可以通过一个指定的线程从runLoopDict中获取它所对应的RunLoop。
- RunLoop的销毁:系统在创建RunLoop的时候,会注册一个回调,确保线程在销毁的同时,也销毁掉其对应的RunLoop。
- 下面我用一张图来总结一下上面的源码
RunLoop的相关类
在上面一节我们介绍了RunLoop和线程的关系,在__CFRunLoop这个结构体中发现还有一个CFRunLoopModeRef类,这又是什么,在
CoreFoundation
里面还有没有关于RunLoop
的类呢?答案是肯定的,这也就是我们这一节所要介绍的重点
RunLoop的相关类之间关系
-
首先我们要知道RunLoop相关的类有5个
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
-
那么每个类都是什么呢?
- 第一个类我在前面已经剖析过了,它就是
RunLoop
对象所属于的类 -
CFRunLoopModeRef
是RunLoop
当前的一个运行模式,什么是运行模式呢?我会在RunLoop和Mode
这一节仔细讲解 -
CFRunLoopSourceRef
和CFRunLoopTimerRef
是RunLoop处理的消息类型 -
CFRunLoopObserverRef
监听RunLoop运行状态的一个类
- 第一个类我在前面已经剖析过了,它就是
-
现在我们用一张图来看看各个类之间的关系
- 一个
RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source/Timer/Observer
。 - 每次调用
RunLoop
的主函数时,只能指定其中一个Mode
,这个Mode
被称作CurrentMode
。 - 如果需要切换
Mode
,只能退出Loop
,再重新指定一个Mode
进入。这样做主要是为了分隔开不同组的Source/Timer/Observer
,让其互不影响。 - 如果一个
mode
中一个Source/Timer/Observer
都没有,则RunLoop
会直接退出,不进入循环。
各个类的作用
-
我们已经知道了每个类是什么,现在我们需要了解一下每个类具体是干什么的
-
CFRunLoopRef
是一个CFRunLoop
结构体的指针,所以说它的职责就是CFRunLoop
的职责,运行循环,处理事件,保持运行 -
CFRunLoopModeRef
运行模式,模式下对应多个处理源,具体有哪些模式我会在RunLoop和Mode这一节仔细讲解 -
CFRunLoopSourceRef
是事件产生的地方。Source
有两个版本:Source0
和Source1
。-
Source0
触摸事件处理 -
Source1
基于Port的线程见通信
-
-
CFRunLoopTimerRef
NSTimer的运用 -
CFRunLoopObserverRef
用于监听RunLoop的状态,UI刷新,自动释放池
-
-
下面我们用一张图来总结它的职责
RunLoop中的Mode
千呼万唤始出来,犹抱琵琶半遮面。在前面我们不止一次提到了这一节,可见这一节是多么的重要,那么现在我们就来给大家仔细讲解一下这一节到底有什么秘密,看完这节后希望前面的问题都能解决,并把相互之间的关系给链接起来,那么我们现在开始吧
- 首先我们要回顾一下在RunLoop的相关类之间关系这一节中所讲述的知识点和关系图,如果忘了请再次回到此处仔细阅读一遍,下面的讲解都给予你有了上面的基础。
- 那张图的重点就是一个
RunLoop
包含若干个Mode
,每个Mode
又包含若干个Source/Timer/Observer
。这句话真的是点晴之笔,一句话就把5个相关类的关系说的一清二楚。
CFRunLoopModeRef
__CFRunLoopMode源码剖析
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;//sources0
CFMutableSetRef _sources1;//sources1
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 */
};
- 一个
CFRunLoopModeRef
对象有一个name
,若干source0
,source1
,timer
,observer
和port
,可以看出事件都是由mode
在管理,而RunLoop
管理着Mode
。 - 下面我们用一张图来总结一下
__CFRunLoopMode的五种运行模式
- 系统默认注册的五个Mode
1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2. UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode
CommonModes
-
其中,需要着重说明的是,在 RunLoop 对象中,有一个叫
CommonModes
的概念。 -
先看
RunLoop
对象的组成:
//简化版本
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode;//当前运行的mode
CFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
};
-
一个
Mode
可以将自己标记为Common
属性,通过将其ModeName
添加到RunLoop
的commonModes
中。那么添加进去之后的作用是什么? -
每当
RunLoop
的内容发生变化时,RunLoop
都会自动将 _commonModeItems 里的Source/Observer/Timer
同步到具有Common
标记的所有Mode
里。其底层实现如下:
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);
}
}
}
-
Mode API
-
CFRunLoop
对外暴露的管理Mode
接口只有下面 2 个:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
什么是Mode Item?
从这个名字大概就能知道这是什么了?
Mode包含的元素
那么Mode到底包含哪些类型的元素呢?
-
RunLoop
需要处理的消息,包括time
以及source
消息,它们都属于Mode item
。 -
RunLoop
也可以被监听,被监听的对象是observer
对象,也属于Mode item
。 -
所有的
mode item
都可以被添加到Mode
中,Mode
中包含可以包含多个mode item
,一个item
可以被同时加入多个mode
。但一个item
被重复加入同一个mode
时是不会有效果的。如果一个mode
中一个item
都没有,则RunLoop
会直接退出,不进入循环。
-
Mode
暴露的管理mode item
的接口有下面几个:
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 name
来操作内部的mode
,当你传入一个新的mode name
但RunLoop
内部没有对应mode
时,RunLoop
会自动帮你创建对应的CFRunLoopModeRef
。对于一个RunLoop
来说,其内部的mode
只能增加不能删除。 -
苹果公开提供的
Mode
有两个:kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
) 和UITrackingRunLoopMode
,你可以用这两个Mode Name
来操作其对应的Mode
。 -
同时苹果还提供了一个操作
Common
标记的字符串:kCFRunLoopCommonModes
(NSRunLoopCommonModes
),你可以用这个字符串来操作Common Items
,或标记一个Mode
为Common
。使用时注意区分这个字符串和其他mode name
。 -
如下:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
Mode之间的切换
-
我们平时在开发中一定遇到过,当我们使用
NSTimer
每一段时间执行一些事情时滑动UIScrollView
,NSTimer
就会暂停,当我们停止滑动以后,NSTimer
又会重新恢复的情况,我们通过一段代码来看一下注意⚠️:代码中的注释也很重要,展示了我们探索的过程
-(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(@"-------");
}
- 由上述代码可以看出,
NSTimer
不管用是因为Mode的切换,因为如果我们在主线程使用定时器,此时RunLoop
的Mode
为kCFRunLoopDefaultMode
,即定时器属于kCFRunLoopDefaultMode
,那么此时我们滑动ScrollView
时,RunLoop
的Mode
会切换到UITrackingRunLoopMode
,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop
的Mode切换回kCFRunLoopDefaultMode
,所以NSTimer
就又管用了。
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;
};
-
从上面我们可看见有两个版本:
-
Source0
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal (source)
,将这个Source
标记为待处理,然后手动调用CFRunLoopWakeUp (runloop)
来唤醒RunLoop
,让其处理这个事件。 -
Source1
包含了一个mach_port
和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source
能主动唤醒RunLoop
的线程,其原理在下面会讲到。
-
-
下面我们用一张图来总结一下以上知识点
CFRunLoopTimerRef
是基于时间的触发器
typedef struct __CFRunLoopTimer * CFRunLoopTimerRef;
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _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
是基于时间的触发器,它和NSTimer
是toll-free bridged
的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop
时,RunLoop
会注册对应的时间点,当时间点到时,RunLoop
会被唤醒以执行那个回调。 - 下面我们用一张图来总结上面知识点
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
};
- 下面我用一个例子来展示一下,监听示例
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//创建监听者
/*
第一个参数 CFAllocatorRef allocator:分配存储空间 CFAllocatorGetDefault()默认分配
第二个参数 CFOptionFlags activities:要监听的状态 kCFRunLoopAllActivities 监听所有状态
第三个参数 Boolean repeats:YES:持续监听 NO:不持续
第四个参数 CFIndex order:优先级,一般填0即可
第五个参数 :回调 两个参数observer:监听者 activity:监听的事件
*/
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添加监听者
/*
第一个参数 CFRunLoopRef rl:要监听哪个RunLoop,这里监听的是主线程的RunLoop
第二个参数 CFRunLoopObserverRef observer 监听者
第三个参数 CFStringRef mode 要监听RunLoop在哪种运行模式下的状态
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的内存管理(Core Foundation)
凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
GCD本来在iOS6.0之前也是需要我们释放的,6.0之后GCD已经纳入到了ARC中,所以我们不需要管了
*/
CFRelease(observer);
}
2019-09-06 RunLoop醒来了
2019-09-06 RunLoop要处理Timers了
2019-09-06 RunLoop要处理Sources了
2019-09-06 RunLoop要处理Timers了
2019-09-06 RunLoop要处理Sources了
2019-09-06 RunLoop要休息了
2019-09-06 RunLoop醒来了
- 下面我们用一张图来总结我们上面的知识
RunLoop的内部逻辑
运行逻辑
image- 可以看到,在
RunLoop
中,接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。
1.来源按同步异步分类
1.1 Input sources
输入源传递异步事件,通常消息来自于其他线程或程序,按照是否来源于内核也分为下面几种:
- Port-Based Sources,基于 Port 的 事件,系统底层的,一般由内核自动发出信号。例如
CFSocketRef
,在应用层基本用不到。 - Custom Input Sources,非基于 Port 事件,用户手动创建的 Source,则必须从其他线程手动发送信号。
- Cocoa Perform Selector Sources, Cocoa 提供的
performSelector
系列方法,也是一种事件源。和基于端口的源一样,执行selector
请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个 selector 执行完后会自动从Run Loop
里面移除。
1.2 Timer sources
-
定时源则传递同步事件,发生在特定时间或者重复的时间间隔。
-
定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的
Run Loop
的特定模式相关。如果定时器所在的模式当前未被Run Loop
监视,那么定时器将不会开始直到Run Loop
运行在相应的模式下。 -
其主要包含了两部分:
- NSTimer
- performSelector:withObject:afterDelay:
2.来源按对象分类
2.1 Source1
-
对应于 Port-Based Sources,即基于 Port 的,通过内核和其他线程通信。
-
常用于接收、分发系统事件,大部分屏幕交互事件都是由
Source1
接收,包装成Event
,然后分发下去,最后由Source0
去处理。 -
所以,其包括:
- 基于 Port 的线程间通信;
- 系统事件捕捉;
2.2 Source0
-
是非 Port 事件。在应用中,触摸事件的最终处理,以及
perforSelector:onThread
都是包装成该类型对象,最后由开发者指定回调函数,手动处理该事件。 -
需要注意的是
perforSelector:onThread
是否有delay
,即是否延迟函数或者定时函数等类型。 -
perforSelector:onThread
不是delay
函数时, 是Source0
事件。 -
performSelector:withObject:afterDelay
有delay
时,则属于Timers
事件。
所以,其包括:
- 触摸事件处理
- performSelector:onThread
2.3 Timers
- 同上
源码详解
入口函数
// 共外部调用的公开的CFRunLoopRun方法,其内部会调用CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
// 调用RunLoop执行函数
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
RunLoop执行函数
// 经过精简的 CFRunLoopRunSpecific 函数代码,其内部会调用__CFRunLoopRun函数
/*
* 指定mode运行runloop
* @param rl 当前运行的runloop
* @param modeName 需要运行的mode的name
* @param seconds runloop的超时时间
* @param returnAfterSourceHandled 是否处理完事件就返回
*/
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
CHECK_FOR_FORK();
// RunLoop正在释放,完成返回
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
// 根据modeName 取出当前的运行Mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false
// 如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
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和上次的mode一致
rl->_currentMode = currentMode;
//初始化一个result为kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished;
if (currentMode->_observerMask & kCFRunLoopEntry )
// 1\. 通知 Observers: 进入RunLoop。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 2--11.RunLoop的运行循环的核心代码(这里为什么是2-11呢?请看下面源码)
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit )
// 12\. 通知 Observers: 退出RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
RunLoop消息处理函数
// 精简后的 __CFRunLoopRun函数,保留了主要代码
/**
* 运行run loop
*
* @param rl 运行的RunLoop对象
* @param rlm 运行的mode
* @param seconds run loop超时时间
* @param stopAfterHandle true:run loop处理完事件就退出 false:一直运行直到超时或者被手动终止
* @param previousMode 上一次运行的mode
*
* @return 返回4种状态
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//获取系统启动后的CPU运行时间,用于控制超时时间
uint64_t startTSR = mach_absolute_time();
//如果RunLoop或者mode是stop状态,则直接return,不进入循环
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
//mach端口,在内核中,消息在端口之间传递。 初始为0
mach_port_name_t dispatchPort = MACH_PORT_NULL;
//判断是否为主线程
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
//如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
#if USE_DISPATCH_SOURCE_FOR_TIMERS
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
//mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif
//GCD管理的定时器,用于实现runloop超时机制
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
//立即超时
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
}
//seconds为超时时间,超时时执行__CFRunLoopTimeout函数
else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
}
//永不超时
else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
//标志位默认为true
Boolean didDispatchPortLastTime = true;
//记录最后runloop状态,用于return
int32_t retVal = 0;
do {
//初始化一个存放内核消息的缓冲池
uint8_t msg_buffer[3 * 1024];
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
//取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;
//设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);
if (rlm->_observerMask & kCFRunLoopBeforeTimers)
// 2\. 通知 Observers: RunLoop 即将处理 Timer 回调。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
if (rlm->_observerMask & kCFRunLoopBeforeSources)
// 3\. 通知 Observers: RunLoop 即将处理 Source
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 4\. 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 5\. 处理 Source0 (非port) 回调(可能再次处理Blocks)
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
//如果没有Sources0事件处理 并且 没有超时,poll为false
//如果有Sources0事件处理 或者 超时,poll都为true
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 6\. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
//从缓冲区读取消息
msg = (mach_msg_header_t *)msg_buffer;
//接收dispatchPort端口的消息,(接收source1事件)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
//如果接收到了消息的话,前往第9步开始处理msg
goto handle_msg;
}
}
didDispatchPortLastTime = false;
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting))
// 7\. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
do {
msg = (mach_msg_header_t *)msg_buffer;
// 8\. RunLoop开始休眠:等待消息唤醒,调用 mach_msg 等待接收 mach_port 的消息。直到被下面某一个事件唤醒。
// • 一个基于 port 的Source 的事件。
// • 一个 Timer 到时间了
// • RunLoop 自身的超时时间到了
// • 被其他什么调用者手动唤醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
__CFRunLoopUnsetSleeping(rl);
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))
// 9\. 通知 Observers: RunLoop 结束休眠(被某个消息唤醒)
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 收到消息,处理消息。
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
//通过CFRunloopWake唤醒
} else if (livePort == rl->_wakeUpPort) {
//什么都不干,跳回2重新循环
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
} else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// 9.1 处理Timer:如果一个 Timer 到时间了,触发这个Timer的回调。
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
__CFArmNextTimerInMode(rlm, rl);
}
} else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
// 9.2 处理GCD Async To Main Queue:如果有dispatch到main_queue的block,执行block。
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
if (rls) {
mach_msg_header_t *reply = NULL;
// 9.3 处理Source1:如果一个 Source1 (基于port) 发出事件了,处理这个事件
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
}
}
// 10\. 处理Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 11\. 根据前面的处理结果,决定流程
// 11.1 当下面情况发生时,退出RunLoop
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 11.1.1 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 11.1.2 当前RunLoop已经被外部调用者强制停止了
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// 11.1.3 当前运行模式已经被停止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// 11.1.4 source/timer/observer一个都没有了
retVal = kCFRunLoopRunFinished;
}
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
// 11.2 如果没超时,mode里不为空也没停止,loop也没被停止,那继续loop。
} while (0 == retVal);
return retVal;
}
消息处理底层函数
-
当
RunLoop
进行回调时,一般都是通过一个很长的函数调用出去(call out)
,当你在你的代码中下断点调试时,打印堆栈(bt)
,就能在调用栈上看到这些函数。 -
下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:
{
// 1\. 通知 Observers: 进入RunLoop。
// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
// 2\. 通知 Observers: RunLoop 即将处理 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
// 3\. 通知 Observers: RunLoop 即将处理 Source
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
// 4\. 处理Blocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
// 5\. 处理 Source0 (非port) 回调(可能再次处理Blocks)
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
// 7\. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
// 8\. RunLoop开始休眠:等待消息唤醒,调用 mach_msg 等待接收 mach_port 的消息。
mach_msg() -> mach_msg_trap();
// 9\. 通知 Observers: RunLoop 结束休眠(被某个消息唤醒)
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
// 9.1 处理Timer:如果一个 Timer 到时间了,触发这个Timer的回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
// 9.2 处理GCD Async To Main Queue:如果有dispatch到main_queue的block,执行block。
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
// 9.3 处理Source1:如果一个 Source1 (基于port) 发出事件了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
// 10\. 处理Blocks
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
// 12\. 通知 Observers: 退出RunLoop
// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
休眠的实现
代码逻辑图
- 将上面的代码逻辑抽取下面得到如下:
- 黄色:表示通知 Observer 各个阶段;
- 蓝色:处理消息的逻辑;
- 绿色:分支判断逻辑;
代码流程图
imageRunLoop的应用
常驻线程
- 常驻线程的作用:我们知道,当子线程中的任务执行完毕之后就被销毁了,那么如果我们需要开启一个子线程,在程序运行过程中永远都存在,那么我们就会面临一个问题,如何让子线程永远活着,这时就要用到常驻线程:给子线程开启一个
RunLoop
注意:子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。 - 子线程开启
RunLoop
的代码,先点击屏幕开启子线程并开启子线程RunLoop
,然后点击button
。
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 创建子线程并开启
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(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);
// 2.子线程需要开启RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end
- 注意:创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃
NSTimer
1. 定时器的使用
-
定时器,在开发中一般使用
NSTimer
,可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也RunLoop
的特定模式相关。如果定时器所在的模式当前未被RunLoop
监视,那么定时器并不会被调用。 -
NSTimer
就是基于runLoop
在运行的, 当它被添加到runLoop
之后,runLoop
就会根据它的时间间隔来注册相应的时间点, 到时间点之后timer
就会唤醒runLoop
来触发timer
指定的事件. 因此在使用 timer 的时候我们必须先把 timer 添加到 runLoop 中, 并且还得添加到 runLoop 的指定模式中, 它才能起作用, 否则它是不起作用的. -
NSTimer
有两种创建方式。
// a. timerWithTimeInterval
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
// b. scheduledTimerWithTimeInterval
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- 其中,方式 a,仅仅创建,并返回,如果要是的
NSTimer
被调用,需要手动RunLoop
。
// 方式1:创建timer,手动添加到default mode,滑动时定时器停止
static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode: NSDefaultRunLoopMode];
- 方式 b,创建一个定时器之外, 还会把创建的这个定时器自动添加到当前线程的 runLoop 下, 并且是添加到了 runloop 的 defaultMode 下.
// 方式2:创建timer默认添加到default mode,滑动时定时器停止
static int count = 0;
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
- 这种方式下,不用再往 mode 里添加,也能正常调用 Timer。
2. 滑动时失效
-
在上面创建
NSTimer
的方式中,不管方式 a,还是 b,假如页面在滑动ScrollView
时,定时器都会停止调用。 -
因为在滑动
ScrollView
时,RunLoop
处于 UITrackingRunLoopMode 运行模式下,该模式中如果不手动添加对应的 Timer,是不会有定时器的,所以在滑动时,也就不会调用定时器的回调。 -
那么,解决方式,就是将定时器,添加到 UITrackingRunLoopMode 下。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
- 或者利用之前的
Common Mode
。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
3. 不准时
-
一个
NSTimer
注册到RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop
为了节省资源,并不会在非常准确的时间点回调这个 Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。 -
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
-
CADisplayLink
是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和NSTimer
并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和NSTimer
相似),造成界面卡顿的感觉。
AutoreleasePool
-
App
启动后,苹果在主线程RunLoop
里注册了两个Observer
,其回调都是_wrapRunLoopWithAutoreleasePoolHandler ()
。
observers = (
// activities = 0x1,监听的是Entry
"<CFRunLoopObserver>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1138221b1), context = <CFArray >{type = mutable-small, count = 1, values = (\n\t0 : <0x7ff6e6002058>\n)}}",
"<CFRunLoopObserver >{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1138221b1), context = <CFArray>{type = mutable-small, count = 1, values = (\n\t0 : <0x7ff6e6002058>\n)}}"
),
在主线程执行的代码,通常是写在诸如事件回调、Timer
回调内的。这些回调会被 RunLoop
创建好的 AutoreleasePool
环绕着,所以不会出现内存泄漏,开发者也不必显示创建Pool
了。
- 第一个
Observer
会监听 Entry(即将进入 Loop),其回调内会调用 objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个Observer
的order
是 - 2147483647 优先级最高,确保发生在所有回调操作之前。 - 第二个
Observer
会监听RunLoop
的 BeforeWaiting(准备进入休眠)和 Exit(即将退出 Loop)两种状态。- 在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。
- 即将退出
RunLoop
时会调用 objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer
的order
是 2147483647,优先级最低,确保发生在所有回调操作之后。
当然你如果需要显式释放,例如循环,可以自己创建AutoreleasePool
,否则一般不需要自己创建。
事件响应
-
苹果注册了一个
Source1
(基于 mach port 的) 用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback ()
。 -
当一个硬件事件 (触摸 / 锁屏 / 摇晃等) 发生后,首先由
IOKit.framework
生成一个 IOHIDEvent 事件并由SpringBoard
接收。这个过程的详细情况可以参考这里。SpringBoard
只接收按键 (锁屏 / 静音等),触摸,加速,接近传感器等几种Event
,随后用mach port
转发给需要的 App 进程。随后苹果注册的那个Source1
就会触发回调,并调用_UIApplicationHandleEventQueue ()
进行应用内部的分发。 -
_UIApplicationHandleEventQueue ()
会把IOHIDEvent
处理并包装成UIEvent
进行处理或分发,其中包括识别UIGesture / 处理屏幕旋转 / 发送给 UIWindow
等。通常事件比如UIButton
点击、touchesBegin/Move/End/Cancel
事件都是在这个回调中完成的。 -
比如,
UIButton
点击事件,通过Source1
接收后,包装成Event
,最后进行分发是由Source0
事件回调来处理的。
手势识别
"<CFRunLoopObserver 0x60000137cf00 [0x110c33b68]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x1133f4473), context = <CFRunLoopObserver context 0x60000097d9d0>}"
-
当上面的
_UIApplicationHandleEventQueue ()
识别了一个手势时,其首先会调用Cancel
将当前的touchesBegin/Move/End
系列回调打断。随后系统将对应的UIGestureRecognizer
标记为待处理。 -
苹果注册了一个
Observer
监测 BeforeWaiting(Loop 即将进入休眠)事件,这个Observer
的回调函数是_UIGestureRecognizerUpdateObserver ()
,其内部会获取所有刚被标记为待处理的GestureRecognizer
,并执行GestureRecognizer
的回调。 -
当有
UIGestureRecognizer
的变化 (创建 / 销毁 / 状态改变) 时,这个回调都会进行相应处理。
界面更新
"<CFRunLoopObserver>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x1152506ae), context = <CFRunLoopObserver context 0x0>}"
-
当在操作 UI 时,比如改变了
Frame
、更新了UIView/CALayer
的层次时,或者手动调用了UIView/CALayer
的setNeedsLayout/setNeedsDisplay
方法后,这个UIView/CALayer
就被标记为待处理,并被提交到一个全局的容器去。 -
苹果注册了一个
Observer
监听 BeforeWaiting(即将进入休眠)和 Exit (即将退出 Loop)事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的UIView/CAlayer
以执行实际的绘制和调整,并更新 UI 界面。 -
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
- 通常情况下这种方式是完美的,因为除了系统的更新,以及
setNeedsDisplay
等方法手动触发下一次RunLoop
运行的更新。但是如果当前正在执行大量的逻辑运算可能 UI 的更新就会比较卡,因此facebook
推出了 AsyncDisplayKit 来解决这个问题。AsyncDisplayKit
其实是将 UI 排版和绘制运算尽可能放到后台,将 UI 的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView 或 CALayer
的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit
在主线程RunLoop
中增加了一个Observer
监听即将进入休眠和退出RunLoop
两种状态,收到回调时遍历队列中的待处理任务一一执行。
PerformSelecter
perforSelector 有下面三类:
// 1.和RunLoop不相干,底层直接调用objc_sendMsg方法
- (id)performSelector:(SEL)aSelector withObject:(id)object;
// 2\. 和RunLoop相关,封装成Source0事件,依赖于RunLoop,若线程无对应的RunLoop,会调用objc_sendMsg执行
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// 3\. 和RunLoop相关,封装成Timers事件
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
- 在苹果开源的 objc 源码,我们从 NSObject.mm 得到如下源码:
- (id)performSelector:(SEL)sel {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}
- (id)performSelector:(SEL)sel withObject:(id)obj {
if (!sel) [self doesNotRecognizeSelector:sel];
return ((id(*)(id, SEL, id))objc_msgSend)(self, sel, obj);
}
-
从源码可以直接看出,在不包含
delay
时,其直接调用objc_msgSend
,与RunLoop
无关。 -
但是,找不到
- (void) performSelector: withObject: afterDelay:
的源码。苹果并没有开源。 -
我们通过 GNUstep 项目,找到该方法的 Foundation 的源码。
- (void) performSelector: (SEL)aSelector
withObject: (id)argument
afterDelay: (NSTimeInterval)seconds
{
NSRunLoop *loop = [NSRunLoop currentRunLoop];
GSTimedPerformer *item;
item = [[GSTimedPerformer alloc] initWithSelector: aSelector
target: self
argument: argument
delay: seconds];
[[loop _timedPerformers] addObject: item];
RELEASE(item);
[loop addTimer: item->timer forMode: NSDefaultRunLoopMode];
}
// GSTimedPerformer对象
@interface GSTimedPerformer: NSObject
{
@public
SEL selector;
id target;
id argument;
NSTimer *timer;
}
- 其实本质上,是转换为一个包含
NSTimer
定时器的GSTimedPerformer
对象,实质上是个Timers
事件,添加到RunLoop
中。
注意:GNUstep 项目只是一个开源实现,其实现和苹果实现大部分一致,所以可参考性很强,但并不是完全一致。
关于 GCD
-
在
RunLoop
的源代码中可以看到用到了 GCD 的相关内容,但是 RunLoop 本身和 GCD 并没有直接的关系。 -
当调用了
dispatch_async (dispatch_get_main_queue (), <#^(void) block#>)
时libDispatch
会向主线程RunLoop
发送消息唤醒RunLoop
,RunLoop
从消息中获取block
,并且在 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE 回调里执行这个block
。 -
不过这个操作仅限于主线程,其他线程
dispatch
操作是全部由libDispatch
驱动的。
网友评论