RunLoop
是什么?你了解嘛。RunLoop
也是作为一名iOS manager必须了解的一个知识点,开发中可能只有用到timer
的时候,接触过runloop
.其实,对于iOS App来说,runloop是一个非常重要的东西,可以说runloop是支持程序运行的不可缺少的一部分。
什么是RunLoop
RunLoop顾名思义,就是运行循环,一个如此抽象的描述,可以理解为在程序运行过程中,循环做一些事情。那么他的应用范畴有哪些呢?比如
- Timer
- performSelector
- GCD
- 事件响应、手势识别、界面刷新
- 网络请求
- autoreleasePool
吃惊嘛?真的上面说到的这么多的事都是runloop去处理的嘛?你可能Timer,GCD都用的很溜,可是却没想过,是什么支撑他们可以实现他们本身的功能的。甚至没想过,App为何可以打开之后一直停留在App内,而一个命令行程序为什么执行完就退出了呢?
RunLoop的基本作用
没错,RunLoop的基本作用就是保持程序可以持续运行
,处理App的各种事件
,比如触摸,定时器,界面更新等。RunLoop 的目的是,当有事件要去处理时保持线程忙,当没有事件要处理时让线程进入休眠,所以RunLoop还可以帮助我们节省CPU资源
,帮助程序提高性能
。
简单来说,RunLoop 是用来监听输入源,进行调度处理的。这里的输入源可以是输入设备、网络、周期性或者延迟时间、异步回调。RunLoop 会接收两种类型的输入源:一种是来自另一个线程或者来自不同应用的异步消息;另一种是来自预订时间或者重复间隔的同步事件。
但是RunLoop这块的知识,我们研究起来会感觉比较难,比较底层,而且源码都是C语言,理解起来也比较不容易,所以,下面我们是抱着了解的态度去学习吧,把重点的地方认真理解,其他比如runloop的处理流程等,作为一个了解就可以了。
iOS中有两套API来访问和使用RunLoop
- Foundation: NSRunLoop
- Core Foundation: CFRunLoopRef
NSRunLoop
和CFRunLoopRef
都是RunLoop
对象,NSRunLoop是基于CFRunLoopRef的一层Objective-C的封装,
CFRunLoopRef是完全开源的,源码在官网,大家感兴趣可以下载源码研究一下。
RunLoop与线程
RunLoop与线程的关系,也是面试中常遇到的问题,下面先说一下结论:
- 每条线程都有唯一一个与之对应的RunLoop对象,不会对应多个runloop,但是runloop可以嵌套
- 线程刚创建的时候并没有RunLoop对象,RunLoop会在第一次获取他时创建
- RunLoop保存在全局的Dictionary中,线程为key,RunLoop是value
- RunLoop会在线程结束时销毁
- 主线程的RunLoop已经自动创建,子线程默认没有开启RunLoop
对于以上结论呢,在后面的源码分析中,会一步步证实。
先说一下主线程的RunLoop已经自动创建,但是上面有说了线程刚创建的时候并没有RunLoop对象,RunLoop会在第一次获取他时创建,那主线程的RunLoop是在什么时候获取的呢?
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
// Setup code that might create autoreleased objects goes here.
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
主线程的RunLoop就是在UIApplicationMain这个函数中获取的,所以main函数执行完,主线程就有了自己的RunLoop,所以程序有RunLoop的支持也就不会退出。
获取RunLoop
Foundation
和Core Foundation
都分别提供了获取RunLoop的方法
[NSRunLoop mainRunLoop];//主线程对应的runloop
[NSRunLoop currentRunLoop];//当前线程对应的runloop
CFRunLoopGetCurrent();
在源码中找到__CFRunLoop的定义:
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
这里面我们最主要关注CFRunLoopModeRef
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
};
这里的_sources0
,_sources1
,_observers
,_timers
是不是开始变得熟悉了。CFMutableSetRef
可以理解为一个set集合类,内部的元素无序且不重复。
RunLoop的运行逻辑
上面看到的_sources0
,_sources1
,_observers
,_timers
都分别在RunLoop中处理什么逻辑呢?他们各司其职,分别处理文章开头说过的RunLoop应用范畴内的任务:
- sources0:触摸事件处理、performSelector:onThread:
- sources1: 基于port的线程间通信、系统事件捕捉
- timers: NSTimer、performSelector:withObject:afterDelay:
- observers: 用于监听RunLoop的状态、UI刷新、Autorelaese Pool
CFRunLoopModeRef
CFRunLoopModeRef
代表RunLoop的运行模式,一个RunLoop可以包含多个mode,每个mode又可以包含多个sources0,sources1,observers,timers
。
RunLoop启动时只能选择其中一个mode作为currentMode,如果需要切换mode,只能退出当前RunLoop,再重新选择一个mode。这里要注意,切换mode并不会导致程序退出,哪怕是主线程的RunLoop切换,也不会。
但是,mode中如果没有任何的sources0,sources1,observers,timers
,RunLoop就会立刻退出。
常见的mode有两种:
- kCFRunLoopDefaultMode :App的默认mode,通常主线程是在这个mode下运行
- UITrackingRunLoopMode :界面跟踪mode,用于
scrollview
追踪滑动触摸,保证界面滑动时不受其他mode影响
CFRunLoopObserverRef
CFRunLoopObserverRef是用来监听RunLoop状态的,状态是一个CFRunLoopActivity类型的枚举,共有下面这几种:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), //即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
创建observer 有两种方法,一种是带着block的,另外一种需要一个监听的方法
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
#if __BLOCKS__
CF_EXPORT CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity)) API_AVAILABLE(macos(10.7), ios(5.0), watchos(2.0), tvos(9.0));
#endif
demo中我用了两种方法分别测试了监听,注意添加observer到runloop后,还需要调用CFRelease释放一下
//kCFRunLoopCommonModes 默认包括kCFRunLoopDefaultMode UITrackingRunLoopMode
// 创建observer 的两种方法
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// switch (activity) {
// case kCFRunLoopExit:{
// CFRunLoopMode model = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
// NSLog(@"kCFRunLoopExit - %@",model);
// CFRelease(model);
// }
// break;
// case kCFRunLoopEntry:{
// CFRunLoopMode model = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
// NSLog(@"kCFRunLoopEntry- %@",model);
// CFRelease(model);
// }
// break;
//
// default:
// break;
// }
// });
// 添加observer到runloop
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
用上面这段demo的代码,我们就可以监听到,timer是可以唤醒RunLoop的,以及scrollview
滑动前后,mode的切换是需要退出loop再进入的。
RunLoop处理逻辑
关于RunLoop处理逻辑,我们只做一个了解就可以了,可以看看下面这种图,是大体的处理步骤,在研究源码的时候,可以对照这张图,帮助理解。
源码分析
开始分析源码,第一步肯定是要找到RunLoop的入口,比如断点在touchesBegan
中,控制台通过 bt
命令,查看所有的调用栈,就可以找到CFRunLoopRunSpecific
然后可以在源码中通过搜索找到CFRunLoopRunSpecific
函数的实现,源码确实晦涩难懂,我们找到关键代码主要看调用流程就可以了,核心是调用了__CFRunLoopRun
函数,得到result最后返回。__CFRunLoopRun
中的实现就更加复杂了,当然也是在__CFRunLoopRun
中,就可以找到上面RunLoop处理逻辑
的每一个步骤对应的源码。
为了便于阅读就不再直接贴源代码,放一段伪代码方便大家阅读:
int32_t __CFRunLoopRun()
{
// 通知即将进入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知将要处理timer和source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 处理非延迟的主线程调用
__CFRunLoopDoBlocks();
// 处理Source0事件
__CFRunLoopDoSource0();
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks();
}
/// 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort();
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();
// 即将进入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待内核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// 等待。。。
// 从等待中醒来
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 处理因timer的唤醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();
// 处理异步方法唤醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 处理Source1
else
__CFRunLoopDoSource1();
// 再次确保是否有同步的方法需要调用
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即将退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}
现在只要了解上面的伪代码知道核心的方法__CFRunLoopRun内部其实是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数以后就一直处于“等待-处理”的循环之中,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是Runloop精华所在。
RunLoop休眠
RunLoop休眠,就是线程阻塞和普通的线程阻塞是不一样的 ,他是真的会让线程休眠 ,不做任何事,CPU也不分配资源,一直等待线程被唤醒,要做到这样的休眠,只有在内核层面的API才能办到。
所以RunLoop的休眠这里还存在一个用户态和内核态的切换,从用户态切换到内核态进入休眠,当收到唤醒线程的消息后,又切换到用户态处理消息。
这里使用的是mach_msg()方法,监听唤醒runloop的端口,这里还没深入研究过,就不展开说了。感兴趣的可以留言一起讨论学习一下。