RunLoop解析
什么是RunLoop?
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
事件循环 Event Loop
- 没有消息需要处理时,休眠以避免资源占用。
- 有消息需要处理时,立刻被唤醒。
应用程序一般运行在用户态上面,当发生系统调用,需要一些底层API的话,是运行在内核态的。同时用户态的有些内容可以对内核态进行一些线程的调度和管理,包括进程间的通信。
main()
函数为何会保持一致运行的状态而不退出?
因为
main()
函数在内部会调用UIApplicationMain()
函数,UIApplicationMain()
内部会启动一个主线程的runloop,进行消息的接收,消息的处理,已经处理后的等待这么一个循环,而这个循环不是一个简单的for循环或while循环,他是发生了一个用户态到内核态的切换,以及内核态到用户态的切换。
RunLoop的数据结构
NSRunLoop
是CFRunLoop
的封装,提供了面向对象的API。
- CFRunLoop
- CFRunLoopMode
- Source/Timer/Oberver
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;//--对应(runloop和线程关系)
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
cfrunloop结构.png
CFRunLoopMode
runloopmode.pngCFRunLoopSource
-
source0
需要手动唤醒线程。
-
source1
具备唤醒线程的能力。
CFRunLoopTimer
基于事件的定时器
和NSTimer
是toll-free bridged
的。
CFRunLoopOberver
观测时间点:
-
kCFRunLoopEntry
RunLoop入口时机,当runloop准备启动的时候,系统会给一个回调通知。
-
kCFRunLoopBeforeTimers
另一个通知:通知观察者runloop将要对timer的一些相关事件进行处理
-
kCFRunLoopBeforeSources
另一个通知:通知观察者runloop将要对source的一些相关事件进行处理。
-
kCFRunLoopBeforeWaiting
通知对应观察者,当前runloop将要进入休眠状态,这个通知标志着用户即将开始用户态到内核态的切换。
-
kCFRunLoopAfterWaiting
这个通知标志着内核态切换到用户态后的不久时间。
-
kCFRunLoopExit
代表runloop退出的通知
各个数据结构之间的关系
关系.png线程与runloop是一对一的关系,runloop与Mode之间是1对多的关系,Mode与source,timer,observer是一对多的关系
Runloop的Mode
runloop_mode.png当runloop运行在mode1当中时,我们只能处理mode1当中的事件,此时如果mode2,mode3对应的一些回调事件需要处理,如果不是运行在对应mode下面是没办法处理的,这就是mode有多个的原因。
CommonMode的特殊性
NSRunLoopCommonModes
- CommonMode不是实际存在的一种Mode。
- 是同步Source/Timer/Observer到多个mode中的一种技术方案。
如何将timer添加到两个mode当中?
RunLoop事件循环机制
void CFRunLoopRun()
RunLoop的核心
runloop核心.png调用mach_msg()
函数系统会从核心态到用户态的切换,需要出现source1或Timer事件,或手动唤醒。
RunLoop与NSTimer
为什么在滑动tableView时,banner不会再发生滚动?
当我们滑动tableview的时候mode会由:kCFRunLoopDefaultMode
切换为UITrackingRunLoopMode
。所以出于之前mode上的timer事件就不会生效。如何解决这个问题,让其生效?可以通过void CFRunLoopAddTimer(runloop,timer, commenMode)
函数,把NSTimert添加到commenMode
中。
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);
if (modeName == kCFRunLoopCommonModes) {
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
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;
}
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);
}
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == __kCFRunLoopSourceTypeID) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == __kCFRunLoopObserverTypeID) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == __kCFRunLoopTimerTypeID) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
RunLoop与多线程
关系:
- 线程与RunLoop是一一对应的。
- 自己创建的线程默认是没有RunLoop的。
怎样实现一个常驻线程
-
为当前线程开启/创建一个RunLoop。
CFRunloopGetCurrent
或[NSRunLoop currentRunLoop]
都可以为当前线程创建一个RunLoop,因为获取方法本身的过程中,没有它会自己创建。 -
向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
-
启动该RunLoop。
#import "MCObject.h"
@implementation MCObject
static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;
+ (NSThread *)threadForDispatch
{
if (thread == nil) {
@synchronized(self) {
if (thread == nil) {
// 线程的创建
thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
[thread setName:@"com.imooc.thread"];
//启动
[thread start];
}
}
}
return thread;
}
+ (void)runRequest
{
// 创建一个Source
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 如果可以运行
while (runAlways) {
@autoreleasepool {//每次运行循环可以对资源进行一个释放
// 令当前RunLoop运行在DefaultMode下面
//运行的mode和上面添加source的mode要一致,不一致就会死循环
//这个循环在RunLoop运行过程中会由用户态向核心态转换,当前线程会休眠,while就会暂停,所以不会死循环
//第二个参数是运行时间,给个很大的数,代表遥远的未来
//资源被处理是否立即返回,这里是立即返回true
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
}
}
// 某一时机 静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
总结
-
什么是RunLoop,它是怎样做到有事做事,没事休息的?
-
RunLoop与线程是怎样的关系?
-
如何实现一个常驻线程
-
怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?