美文网首页
iOS runloop 机制与使用

iOS runloop 机制与使用

作者: Foxhoundsun | 来源:发表于2020-04-29 02:29 被阅读0次

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的实现原理

image.png

实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

相关文章

网友评论

      本文标题:iOS runloop 机制与使用

      本文链接:https://www.haomeiwen.com/subject/sadwwhtx.html