在咱们开发中运用的许多API都依赖的RunLoop来实现的,比方咱们了解的perform selector办法,比方咱们了解的Timer等等。

Cocoa Perform Selector

以下是Swift中NSObject中供给的perform selector办法簇:

/*  在指定线程履行办法: 主线程或许其他线程  */
open func performSelector(onMainThread aSelector: Selector, with arg: Any?, waitUntilDone wait: Bool, modes array: [String]?)
open func performSelector(onMainThread aSelector: Selector, with arg: Any?, waitUntilDone wait: Bool)
@available(iOS 2.0, *)
open func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool, modes array: [String]?)
@available(iOS 2.0, *)
open func perform(_ aSelector: Selector, on thr: Thread, with arg: Any?, waitUntilDone wait: Bool)
@available(iOS 2.0, *)
open func performSelector(inBackground aSelector: Selector, with arg: Any?)
/*  延迟时刻履行办法  */
open func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval, inModes modes: [RunLoop.Mode])
open func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)

NSObjct有一些办法能够在其它的线程上履行办法。从这些办法自身,其实咱们能够大概猜测出它们和RunLoop的联系:如delay和时刻有关,onThread和线程间通信有关,值得一提的是假如想要perform调用的办法履行,那么方针线程必须有一个现已激活的RunLoop,不然aSelector参数对应的办法是不会履行的。而RunLoop会在一次循环中一次性处理完一切入行列的perform的selector,而不是一次循环处理一个selector。

延时履行

在以上的办法簇中有两个延时的办法:

func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval, inModes modes: [RunLoop.Mode])
func perform(_ aSelector: Selector, with anArgument: Any?, afterDelay delay: TimeInterval)

这个办法会在当时线程的runLoop中设置一个timer,经过timer的callback来调用这个selector。这个timer被参加到默认的mode中(CFDefaultRunLoopMode),当然也能够手动指定timer被参加的mode。当这个timer被触发时,这个线程就会从runloop的音讯行列中取出对应的办法并履行,可是条件是这个runloop运转的mode正好是timer参加的mode,不然的话timer就会等候,直到runloop运转了指定的mode。

比方在ViewController中写一个5秒延时的办法,并将此timer参加到defaultMode中:

self.perform(#selector(hahah), with: nil, afterDelay: 5.0, inModes: [.default])

在控制台断点输出如下,能够很清晰的看到,便是在Timer触发之后(CFEUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION),在回调中调用这个具体的办法:#selector(hahah)

肆:RunLoop在系统中的使用

主线程履行

而在指定主线程中履行的perform办法中performSelector(onMainThread aSelector: Selector),许多文章都说这个也是设置一个Timer,可是经过测验发现并不是如此,并不是设置一个Timer来唤醒runloop,而是体系注册来一个source0事情,并手动来唤醒runloop。

@objc func hahah() {
    self.performSelector(onMainThread:  #selector(testPerformMainThread), with: nil, waitUntilDone: false)
}
@objc func testPerformMainThread() {
    NSLog("I want to see the world.")
}

经过咱们实践测验代码能够看出这儿RunLoop被唤醒之后履行了source0的回调,然后调用了**#selector(hahah)**办法。

肆:RunLoop在系统中的使用

其他线程履行

这儿就不做标示了,由于和上图是相同的,只不过这儿注意thread需求去创立一个runloop保活,不然是没办法在一个没有运转RunLoop的线程上去perform selector的。此处,也是经过source0来调用具体的办法(这儿的办法我没有改名:testPerformMainThread,期望不会引起歧义)

肆:RunLoop在系统中的使用

以上的这些Cocoa Perform Selector Sources根据苹果文档的描绘,它们不像根据Port的Source(即source1):一个perform selector source会在履行完它的selector之后,从runloop中被移除。

Timer

Timer

在Swift中咱们运用的是Timer类型,而在Objective-C中是NSTimer类型,它们的底层都是CFRunLoopTimerRef。网上的部分文章说Timer会提前注册好时刻点,然后一个一个的去履行,其实这个是不对的,它只会注册下一次时刻点,RunLoop被Timer唤醒之后,履行完回调之中的办法,又会继续注册下一个时刻点。

咱们能够从两个当地的源码看,其一是CFRunLoopAddTimer办法,在这个办法中有一个办法的调用顺序,它在添加完Timer之后会调用__CFArmNextTimerInMode办法。

CFRunLoopAddTimer -> _CFRepositionTimerInMode(rlm, rlt, false)
-> __CFArmNextTimerInMode(rlm, rlt->_runLoop)

另一个当地是__CFRunLoopRun办法,在Runloop在被Timer唤醒之后会调用到__CFRunLoopDoTimers办法, 它的办法链为:__CFRunLoopDoTimers -> __CFRunLoopDoTimer —> __CFArmNextTimerInMode 也便是说最后仍是会调用到__CFArmNextTimerInMode办法。

Bool __CFRunLoopRun() {
    
    if (livePort == rlm->_timerPort) {
        CFRUNLOOP_WAKEUP_FOR_TIMER();
        if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
            // Re-arm the next timer
            __CFArmNextTimerInMode(rlm, rl);
        }
    }
    
}

那么这个Timer运转的核心就在**__CFArmNextTimerInMode**中了:

static void __CFArmNextTimerInMode(CFRunLoopModeRef rlm, CFRunLoopRef rl) {
    uint64_t nextHardDeadline = UINT64_MAX;
    uint64_t nextSoftDeadline = UINT64_MAX;
    if (rlm->_timers) {
        // 1.设置每一个timer的下一次到期时刻
        for (...)	{
        }
       // 2.判断下一次时刻
       // - 假如时刻是合理的
        if (nextSoftDeadline < UINT64_MAX
            && (nextHardDeadline != rlm -> _timerHardDeadline
                || nextSoftDealline != rlm -> _timerSoftDeadline)) {
            // 3、到点了给_timerPort发送音讯
            if (rlm->_timerPort) {
                mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline));
            }
        // - 假如时刻是无限:那么就撤销timer
        } else if (nextSoftDeadline == UINT64_MAX) {
            if (rlm->_mkTimerArmed && rlm->_timerPort) {
                AbsoluteTime dummy;
                mk_timer_cancel(rlm->_timerPort, &dummy);
                rlm->_mkTimerArmed = false;
            }
        }
       rlm->_timerHardDeadline = nextHardDeadline;
       rlm->_timerSoftDeadline = nextSoftDeadline;
    }
}

上面的代码注释现已比较详细了,便是在时刻到了之后经过mk_timer_arm 办法来给timerPort发送音讯,那么假如由于滑动屏幕的时分切换了RunLoop运转的Mode呢?

核心代码是mk_timer_arm(rlm->_timerPort, __CFUInt64ToAbsoluteTime(nextSoftDeadline)); 到点之后依然会给timerPort发送音讯,**这个音讯会存在timerPort的音讯行列中!在RunLoop切换回timer所在的Mode之后,当履行到__CFRunLoopServiceMachPort**办法的时分,就会接收到这个timerPort的音讯行列中的音讯,然后处理Timer的回调事情。这也是为什么切换Mode之后,timer的回调会立马履行一次的原因。

CADisplayLink

CADispalyLink 供给了几个根本的API,从中咱们能够很直白的看出它和RunLoop是直接相相关的。

open func add(to runloop: RunLoop, forMode mode: RunLoop.Mode)
open func remove(from runloop: RunLoop, forMode mode: RunLoop.Mode)

CADispalyLink和Timer在某些方面是有相似之处的,在创立完之后也需求将其参加到RunLoop的Mode中。咱们能够设置display link的帧速率(preferredFramesPerSecond),帧速率也决定了一秒之内体系调用了target的这个办法多少次。可是实践上display link的帧速率是会受到设备的最大改写率制约的。

比方说设备的最大改写率是每秒60帧,咱们设置的preferredFramesPerSecond假如比这个值大,那么display link的帧速率也只能是60,不能超过设备的屏幕最大改写率。

接下来咱们要看一看display link是怎么唤醒runloop的:

func createDisplayLink() {
    let link = CADisplayLink.init(target: self, selector: #selector(step))
    link.preferredFramesPerSecond = 1
    link.add(to: RunLoop.main, forMode: .default)
}
@objc func step(displaylink: CADisplayLink) {
    print(displaylink.targetTimestamp)
}

经过打断点的办法栈中可知:RunLoop是被CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION也便是被source1所唤醒的,然后最后调用到target的step办法。很直白,也很简单。由于它是硬件设备的屏幕改写调度后台办理程序经过IPC通信,向当时前台进程的mach port 发送音讯唤醒了RunLoop。

肆:RunLoop在系统中的使用

DispatchSourceTimer

这个比较特别,一开始的时分我也以为它是和RunLoop有联系的,后来根据我自己测验,RunLoop现已处于休眠状况了,可是它仍是会定时触发callback,根据此我在opensource.apple.com中查看了libdispatch的源码,dispatch_source_timer是由GCD办理的定时器,并不是由RunLoop办理的,所以它其实合适RunLoop无关的。

GCD

GCD和RunLoop是处于同一层级的,从开源代码的文件夹就可窥一二,其中RunLoop源码在开源的CF代码中,GCD源码在开源的libdispatch源码中。可是它们有一个很特别的相关:GCD主行列的使命派发是经过Runloop来实现的,这儿在源码中有很清晰的显现:

// 在RunLoop被唤醒的源码中
if (livePort == dispatchPort) {
    ...
    CFRUNLOOP_WAKEUP_FOR_DISPATCH();
    ...
    __CFRUNLOOP_IS_SERVECING_THE_MAIN_DISPATCH_QUEUE__(msg)
    ...
}

GCD派发到主行列中的使命会唤醒RunLoop,可是其它使命行列中的使命并不会和RunLoop进行交互。举例:咱们知道DispatchSourceTimer和RunLoop是无关的,所以能够运用DispatchSourceTimer写一个延时使命来履行GCD的主行列派发:

func createSourceTimer() {
    sourceTimer = DispatchSource.makeTimerSource()
    sourceTimer?.schedule(deadline: .now(), repeating: 10.0, leeway: .nanoseconds(1))
    sourceTimer?.setEventHandler {
        DispatchQueue.main.async {
            NSLog("我想知道我是谁?")
        }
    }
    sourceTimer?.activate()
}

从办法栈可知,时刻到了之后,会给dispatchPort端口发送音讯,而这个端口承受音讯之后就会唤醒RunLoop,然后就会履行**__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__**函数,最后履行派发到主行列中的使命。可是要强调的是,仅限于主行列的使命派发,而dispatch到其它线程的使命是经过libDispatch来处理的。

肆:RunLoop在系统中的使用

事情呼应

一个硬件事情被iOS体系承受之后,一定会被体系处理然后再分发给应该处理该事情的进程,即当时正在前台的进程。

SpringBoard

咱们翻开iPhone能够看到许多不同App的icon,而且左右滑动,能够切换不同的页面,其实这是经过SpringBoard来办理的,它供给了一切App的使用发动服务,Icon的办理,状况栏的控制等等,它本质上便是iOS上的桌面程序,同时它是有BundleID的: **com.apple.springboard,**这一点正好能够验证它便是一个桌面程序。

可是在iOS6之后,SpringBoard的部分办法被分离到在BackBoardd中。BackBoardd是一个后台驻留程序,承当了曾经SpringBoard的部分作业。它的首要意图是处理来自硬件的信息,比方接触事情,按钮事情,加速度计信息。它经过BackBoardServices.framework来和SpringBoard通信。BackBoard勾连体系的IOKit以及用户进程(即使用程序),它也办理着使用程序的发动、暂停和完毕。

接触事情

以接触事情为例:在屏幕被接触之后(硬件事情),体系经过IOKit.framework处理该事情,IOKit将这个接触事情封装为IOHIDEvent目标,然后BackBoard调用当时CAWindowDisplayServer的**-contextIdAtPosition** 办法来得到touch事情应该要发往何处的contextID,这个contextID决定了哪个进程来承受这个touch事情。

假如前台并没有使用程序的话,那么就会经过mach port(IPC通信)将事情分发给SpringBoard来处理,这就意味着用户是操作的是iPhone的桌面,比方用户点击一个使用图标,它将发动这个使用。假如前台有使用程序的话,BackBoard得到contextID之后,会将这个事情经过mach port(IPC通信)分发给前台的这个使用程序

前台使用在承受到mach port 传递来的事情之后,它会唤醒主线程的RunLoop,触发Source1回调,Source1回调会调用__IOHIDEventSystemClientQueueCallback办法,这个办法会将事情交给source0来处理,source0将会调用__eventFetcherSourceCallback办法,在这个办法内部会调用__processEventQueue办法,在这个办法内部会对IOHIDEvent进行处理,将其转化为UIEevent目标,然后调用__dispatchPreprocessedEventQueue分发给UIApplication去寻觅相应的呼应视图。

界面更新

在对界面进行操作的时分,比方改变了UI的Frame,或许改变了UIView/CALayer的层次时,或许手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay办法后,这个UIView/CALayer就会被标记为待处理,并被提交到一个大局的容器中去。

Apple注册了一个注册了一个 Observer 监听 BeforeWaiting(行将进入休眠) 和 Exit (行将退出Loop) 事情,回调去履行一个函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv。这个函数里会遍历一切待处理的 UIView/CALayer 以履行实践的绘制和调整,并更新 UI 界面。这个函数内部得办法栈如下:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction(CA::Transaction*, double, double*);
                CA::Layer::layout_and_display_if_needed(CA::Transaction*);
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

关于改变页面的Frame确实是上述所示,那么关于动画也是在BeforeWaiting的时分才去commit_transition吗?答案是必定的。在滑动一个UITableView的过程中,经过控制台能够看到,也是在每一次被Observer监听到唤醒之后,才去调用改写UI的办法:

肆:RunLoop在系统中的使用

参考

1、SpringBoard.app

2、backboardd

3、developpaper.com/ios-event-h…

4、深化理解RunLoop

5、BackBoardServices.framework

  • 我正在参加技术社区创作者签约计划招募活动,点击链接报名投稿。