iOS runloop 机制与使用
runloop是运行循环,iOS中,APP处于随时待命的状态,处理包括:触摸事件、UI刷新事件、定时器事件、Selector事件等就是runloop的功劳,如iOS中的主线程。这种机制叫Event loop(事件循环),实现思路大概如下:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
实现这种机制的关键在于:如何管理事件/消息,在没有消息\事件的时候,线程休眠避免占用资源,在触发事件、收到消息时候立刻被唤醒。
RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain( )
和 CFRunLoopGetCurrent( )
。 这两个函数内部的逻辑大概是下面这样:
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
runloop和线程的关系是一一对应的,线程创建时并没有runloop,你不主动创建就不会有。runloop的创建是在线程内第一次获取时(通过上面说的两个函数获取),runloop在线程结束时候销毁。你只能在线程内部获取其runloop(主线程除外)
runloop的对外接口
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中CFRunLoopModeRef并没有对外暴露,只是通过CFRunloopRef的接口进行了封装,他们的关系如下:
image.png
一个runloop内可以有多个model,一个model内可以有多个Source/Timer/Observer。每次调用runloop的主函数时,只能指定其中一个model,这个model被称为CurrentMode
。要切换model只能退出runloop再重新指定model。这样做的目的是分割开不同组的Source/Timer/Observer,让其互不影响。
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
CFRunLoopSourceRef 事件源
CFRunloopSourceRef包含两种source,source0和source1
1.source0只包含一个函数回调(函数指针),他并不能主动触发事。使用时你得先调用CFRunLoopSourceSignal(Source)将这个Source标记为待处理,再调用CFRunloopSourceWakeUp(runloop)唤醒当前runloop处理该source。
2.source1包含一个mach_port和一个函数回调(函数指针),被用于通过内核和其他线程相互发送消息。这种source能主动唤醒线程。
CFRunloopTimerRef 基于时间的触发器
CFRunloopTimerRef和NSTimer是toll-free bridged(对象桥接)的,可以混用。其包含一个回调函数(函数指针)和一个时间参数。当其加入runloop中时,runloop会注册一个时间点,在时间点到时runloop会被唤醒执行那个回调函数。
CFRunLoopObserverRef 观察者
每个observe都包含有一个回调函数(函数指针),当runloop状态发生改变时,observe通过回调函数接收到这些变化,可监听的状态有如下几种:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};
runloop的model
CFRunLoopModel和CFRunLoop的结构大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
这里有个概念叫"CommonModes",一个model可以把自己标记为"common"属性(把自己的modeName添加到对应runloop的_commonModes中去即可).当runloop的内容发生变化时,runloop会自动把commonModeItems内的Source/Timer/Observe同步到被标记为"common"属性的mode中.
应用场景举例:
主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
这时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.7 target:self selector:@selector(timeAction) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
CFRunLoop对外暴露管理mode的接口有两个
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
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。
runloop的实现原理
实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。