RunLoop
是什么?你了解嘛。RunLoop
也是作为一名iOS manager必须了解的一个知识点,开发中可能只有用到timer
的时候,接触过runloop
.其实,对于iOS App来说,runloop是一个非常重要的东西,可以说runloop是支持程序运行的不可缺少的一部分。
RunLoop顾名思义,就是运行循环,一个如此抽象的描述,可以理解为在程序运行过程中,循环做一些事情。那么他的应用范畴有哪些呢?比如
吃惊嘛?真的上面说到的这么多的事都是runloop去处理的嘛?你可能Timer,GCD都用的很溜,可是却没想过,是什么支撑他们可以实现他们本身的功能的。甚至没想过,App为何可以打开之后一直停留在App内,而一个命令行程序为什么执行完就退出了呢?
没错,RunLoop的基本作用就是保持程序可以持续运行
,处理App中的各种事件
,比如触摸,定时器等,除此之外,RunLoop还节省CPU资源
,帮助程序提高性能
。
但是RunLoop这块的知识,我们研究起来会感觉比较难,比较底层,而且源码都是C语言,理解起来也比较不容易,所以,下面我们是抱着了解的态度去学习吧,把重点的地方认真理解,其他比如runloop的处理流程等,作为一个了解就可以了。
iOS中有两套API来访问和使用RunLoop
NSRunLoop
和CFRunLoopRef
都是RunLoop
对象,NSRunLoop是基于CFRunLoopRef的一层Objective-C的封装,
CFRunLoopRef是完全开源的,源码在官网,大家感兴趣可以下载源码研究一下。
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的支持也就不会退出。
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集合类,内部的元素无序且不重复。
上面看到的_sources0
,_sources1
,_observers
,_timers
都分别在RunLoop中处理什么逻辑呢?他们各司其职,分别处理文章开头说过的RunLoop应用范畴内的任务:
CFRunLoopModeRef
代表RunLoop的运行模式,一个RunLoop可以包含多个mode,每个mode又可以包含多个sources0,sources1,observers,timers
。
RunLoop启动时只能选择其中一个mode作为currentMode,如果需要切换mode,只能退出当前RunLoop,再重新选择一个mode。这里要注意,切换mode并不会导致程序退出,哪怕是主线程的RunLoop切换,也不会。
但是,mode中如果没有任何的sources0,sources1,observers,timers
,RunLoop就会立刻退出。
常见的mode有两种:
scrollview
追踪滑动触摸,保证界面滑动时不受其他mode影响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的入口,比如断点在touchesBegan
中,控制台通过 bt
命令,查看所有的调用栈,就可以找到CFRunLoopRunSpecific
然后可以在源码中通过搜索找到CFRunLoopRunSpecific
函数的实现
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */ CHECK_FOR_FORK(); if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished; __CFRunLoopLock(rl); 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); CFRunLoopModeRef previousMode = rl->_currentMode; rl->_currentMode = currentMode; int32_t result = kCFRunLoopRunFinished; if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); __CFRunLoopModeUnlock(currentMode); __CFRunLoopPopPerRunData(rl, previousPerRun); rl->_currentMode = previousMode; __CFRunLoopUnlock(rl); return result; } 复制代码
源码确实晦涩难懂,我们找到关键代码主要看调用流程就可以了,核心是调用了__CFRunLoopRun
函数,得到result最后返回。__CFRunLoopRun
中的实现就更加复杂了,当然也是在__CFRunLoopRun
中,就可以找到上面RunLoop处理逻辑
的每一个步骤对应的源码。
RunLoop休眠,就是线程阻塞和普通的线程阻塞是不一样的 ,他是真的会让线程休眠 ,不做任何事,CPU也不分配资源,一直等待线程被唤醒,要做到这样的休眠,只有在内核层面的API才能办到。
所以RunLoop的休眠这里还存在一个用户态和内核态的切换,从用户态切换到内核态进入休眠,当收到唤醒线程的消息后,又切换到用户态处理消息。