iOS 2021 面试前的准备(总结各知识点方便面试前快速复习使用)(四)

1,576 阅读32分钟

29. 解释 Thread Local Data。

pthread_getspecificpthread_setspecific 这两个接口分别用于获取和设置线程本地存储区的数据,在不同的线程下相同的 pthread_key_t 读取的结果是不同的,即线程的本地存储空间是相互隔离的,这也是线程本地存储的关键所在。

 (这里还有一个隐藏点,我们不能以面向对象的思想看待这两个接口,调用 pthread_getspecificpthread_setspecific 时我们是不需要传入 pthread_t 对象的,如果我们想要在某条线程内读取其存储空间的数据,那么我们只能在当前线程内执行 pthread_getspecific 函数,存储同理,即我们想要操作哪条线程,那么我们只能在哪条线程内执行操作。)

 每个线程退出时调用 __CFFinalizeRunLoop 函数。

// Called for each thread as it exits
// 每个线程退出时调用

CF_PRIVATE void __CFFinalizeRunLoop(uintptr_t data) {
    CFRunLoopRef rl = NULL;
    
    if (data <= 1) {
        // 当 data 小于等于 1 开始执行销毁
        
        // static CFLock_t loopsLock = CFLockInit;
        // loopsLock 是一个全局的锁,执行加锁
        __CFLock(&loopsLock);
        
        // 从 __CFRunLoops 全局字典中读出当前线程的 run loop 对象
        if (__CFRunLoops) {
            // 以 pthreadPointer(pthread_self()) 为 key 读取 run loop
            rl = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(pthread_self()));
            
            // 这里的 retain 是为了下面继续使用 rl,这里从 __CFRunLoops 字典中移除 rl,它的引用计数会减 1
            if (rl) CFRetain(rl);
            CFDictionaryRemoveValue(__CFRunLoops, pthreadPointer(pthread_self()));
        }
        
        __CFUnlock(&loopsLock);
    } else {
        // 初始时是 PTHREAD_DESTRUCTOR_ITERATIONS-1 是 3,那么 __CFFinalizeRunLoop 函数需要调用两次减 1,才能真正的执行 run loop 对象的销毁工作 
        _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(data - 1), (void (*)(void *))__CFFinalizeRunLoop);
    }
    // 这里的判断主线程的 run loop 是绝对不能销毁的,只能销毁子线程的 run loop,话说除了我们自己开辟的子线程外,系统会创建启动了 run loop 的子线程吗?
    if (rl && CFRunLoopGetMain() != rl) { // protect against cooperative threads
        // 如果 _counterpart 存在则进行释放
        if (NULL != rl->_counterpart) {
            CFRelease(rl->_counterpart);
            rl->_counterpart = NULL;
        }
        
        // purge all sources before deallocation
        // 在销毁 run loop 之前清除所有来源
        
        // 取得 mode 数组
        CFArrayRef array = CFRunLoopCopyAllModes(rl);
        
        // 遍历 mode 数组,移除 mode 中的所有 sources
        for (CFIndex idx = CFArrayGetCount(array); idx--;) {
            CFStringRef modeName = (CFStringRef)CFArrayGetValueAtIndex(array, idx);
            __CFRunLoopRemoveAllSources(rl, modeName);
        }
        
        // 移除 common mode 中的所有 sources
        __CFRunLoopRemoveAllSources(rl, kCFRunLoopCommonModes);
        CFRelease(array);
    }
    // 释放 rl
    if (rl) CFRelease(rl);
}

 销毁 run loop 对象之前,要先将其从 __CFRunLoops 全局字典中移除,同时遍历其所有的 mode,依次移除每个 mode 中的所有 sources,最后销毁 run loop 对象。mode 销毁前同样也会释放所有的 mode item。


30. run loop 运行过程(CFRunLoopRun 函数摘要)分析。

 在指定的条件下,运行循环退出并返回以下值:

  • kCFRunLoopRunFinished 运行循环模式没有源或计时器。(当 run loop 对象被标记为正在销毁时也会返回 kCFRunLoopRunFinished)
  • kCFRunLoopRunStopped 运行循环已使用 CFRunLoopStop 函数停止。
  • kCFRunLoopRunTimedOut 时间间隔秒数(seconds)过去了。
  • kCFRunLoopRunHandledSource 已处理源。此退出条件仅适用于 returnAfterSourceHandledtrue 时。

 不能为 mode 参数指定 kCFRunLoopCommonModes 常量。运行循环总是以特定模式运行。只有在配置运行循环观察者时,以及仅在希望该观察者以多种模式运行的情况下,才能指定 common mode。

void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
    
        // 调用 CFRunLoopRunSpecific 函数,以 kCFRunLoopDefaultMode 启动当前线程的 run loop,
        // 运行时间传入的是 10^10 秒(2777777 个小时),
        // returnAfterSourceHandled 参数传入的是 false,
        // 指示 run loop 是在处理一个源之后不退出并持续处理事件。
        
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        
        CHECK_FOR_FORK();
        
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

CFRunLoopRunSpecific 函数内部会调用 __CFRunLoopRun 函数,然后可以把 result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); 此行的调用看作一个分界线。行前是,则是首先判断 rl 是否被标记为正在销毁,如果是的话则直接返回 kCFRunLoopRunFinished,否则继续往下执行,会根据 modeNamerl_modes 中找到其对应的 CFRunLoopModeRef,如果未找到或者 CFRunLoopModeRef 的 sources0/sources1/timers/block 为空,则也是直接返回 kCFRunLoopRunFinished。然后是修改 rl_perRunData_currentMode 同时还会记录之前的旧值,此时一切准备就绪,在调用之前会根据 rl_currentMode_observerMask 判断是否需要回调 run loop observer 观察者来告诉它们 run loop 要进入 kCFRunLoopEntry 状态了,然后调用 __CFRunLoopRun 函数正式启动 run loop。

__CFRunLoopRun 函数返回后则是,首先根据 rl_currentMode_observerMask 判断是否需要回调 run loop observer 观察者来告诉它们 run loop 要进入 kCFRunLoopExit 状态了。然后是把 run loop 对象恢复到之前的 _perRunData_currentMode(处理 run loop 的嵌套)。

 上面描述的可能不太清晰,看下面的代码和注释已经极其清晰了。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl,
                            CFStringRef modeName,
                            CFTimeInterval seconds,
                            Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    
    // 从 rl 的 _cfinfo 字段中取 rl 是否正在销毁的标记值,如果是的话,则直接返回 kCFRunLoopRunFinished
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    
    // CFRunLoop 加锁
    __CFRunLoopLock(rl);
    
    // 调用 __CFRunLoopFindMode 函数从 rl 的 _modes 中找到名字是 modeName 的 run loop mode,
    // 如果找不到的话第三个参数传的是 false 则不进行新建 run loop mode,则直接返回 NULL。 
    //(CFRunLoopMode 加锁)
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    // 如果 currentMode 为 NULL 或者 currentMode 里面是空的不包含 sources0/sources1/timers/block 则 return 
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        
        // 如果 currentMode 存在,则进行 CFRunLoopMode 解锁,
        // 对应了上面 __CFRunLoopFindMode(rl, modeName, false) 调用内部的 CFRunLoopMode 加锁 
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        
        // CFRunLoop 解锁
        __CFRunLoopUnlock(rl);
        
        // 返回 kCFRunLoopRunFinished
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    // __CFRunLoopPushPerRunData 函数内部是修改 rl 的 _perRunData 字段的各成员变量的值,并返回之前的 _perRunData,
    //(函数内部修改 _perRunData 的值其实是在标记 run loop 不同状态)
    //(这里的 previousPerRun 是用于下面的 __CFRunLoopRun 函数调用返回后,当前的 run loop 对象要回到之前的 _perRunData)。
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    
    // previousMode 记录 rl 当前的 run loop mode,相比入参传入的 modeName 取得的 run loop mode 而言,它是之前的 run loop mode,
    // 这个 previousMode 主要用于下面的那行 __CFRunLoopRun 函数调用返回后,当前的 run loop 对象要回到之前的 run loop mode。
    //(同上面的 previousPerRun 数据,也要把当前的 run loop 对象回到之前的 _perRunData 数据的状态)
    CFRunLoopModeRef previousMode = rl->_currentMode;
    
    // 更新 rl 的 _currentMode 为入参 modeName 对应的 run loop mode 
    rl->_currentMode = currentMode;
    
    // 临时变量 result,用于当函数返回时记录 run loop 不同的退出原因
    int32_t result = kCFRunLoopRunFinished;
    
    // 判断如果 currentMode 的 _observerMask 字段中包含 kCFRunLoopEntry 的值(_observerMask 内记录了需要观察 run loop 哪些状态变化),
    // 则告诉 currentMode 的 run loop observer 发生了一个 run loop 即将进入循环的状态变化。 
    if (currentMode->_observerMask & kCFRunLoopEntry) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 启动 run loop,__CFRunLoopRun 函数超长,可能是看源码以来最长的一个函数,下面会逐行进行细致的分析
    // ♻️♻️♻️♻️♻️♻️
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // ⬆️⬆️⬆️ __CFRunLoopRun 函数好像也是不会返回的,当它返回时就代表当前的 run loop 要退出了。 
    
    // 同上的 kCFRunLoopEntry 进入循环的回调,这里则是退出 run loop 的回调。
    // 如果 currentMode 的 _observerMask 中包含 kCFRunLoopExit 的值,
    // 即 run loop observer 需要观察 run loop 的 kCFRunLoopExit 退出状态切换
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    // CFRunLoopMode 解锁
    __CFRunLoopModeUnlock(currentMode);
    
    // 销毁 rl 当前的 _perRunData,并把 previousPerRun 重新赋值给 rl 的 _perRunData 
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    
    // 回到之前的 _currentMode 
    rl->_currentMode = previousMode;
    
    // CFRunLoop 解锁
    __CFRunLoopUnlock(rl);
    
    // 返回 result 结果
    return result;
}

 这里需要注意的一个点是 CFRunLoopRunSpecific 函数最后又把之前的 previousPerRunpreviousMode 重新赋值给 run loop 的 _perRunData_currentMode,它们正是用来处理 run loop 的嵌套运行的。

__CFRunLoopModeIsEmpty 函数内部主要用于判断 souces0/source1/timers 是否为空,同时还有判断 rl 的 block 链表中包含的 block 是否能在指定的 rlm 下执行。

__CFRunLoopDoObservers 函数是一个极重要的函数,它用于回调 run loop 发生了状态变化。

 当 run loop 的状态将要(注意这里是将要、将要、将要... kCFRunLoopExit 则除外,退出回调是真的退出完成以后的回调)发生变化时,首先根据 run loop 当前的 run loop mode 的 _observerMask 是否包含了此状态的变化,那么就可以调用 __CFRunLoopDoObservers 函数执行 run loop 状态变化的回调,我们在此状态变化里面可以做很多重要的事情,后面学习 run loop 的使用场景时我们再详细学习。(这里回顾一下前面看过的 run loop 都有哪些状态变化:即将进入 run loop、即将处理 source 事件、即将处理 timer 事件、即将休眠、休眠即将结束、run loop 退出)

 run loop observer 的回调函数。

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__() __attribute__((noinline));
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func,
                                                                          CFRunLoopObserverRef observer,
                                                                          CFRunLoopActivity activity,
                                                                          void *info) {
    // 就是简单的带着参数调用 func 函数                                                                      
    if (func) {
        func(observer, activity, info);
    }
    
    asm __volatile__(""); // thwart tail-call optimization
}

 __CFRunLoopRun 内层的 do while 循环主要是用于 "保持" run looop 的睡眠状态的,直到需要被唤醒了才会跳出这个 do while 循环。只有在下面的事件发生时才会进行唤醒:

  1. 基于端口的输入源(port-based input source)(source1)的事件到达。
  2. CFRunLoopMode 中的 timers 触发。(CFRunLoopMode 可添加多个 timer,它们共用一个 _timerPort 唤醒 run loop,并且会计算所有 timer 中最近的下次要触发的 timer 的时间)
  3. 为 run loop 设置的超时时间过期。
  4. run loop 被显式唤醒。(被其他什么调用者手动唤醒)

CFRunLoopWakeUp 函数内部,通过 run loop 的 _wakeUpPort 唤醒端口来唤醒 run loop 对象。

__CFRunLoopDoBlocks 函数内部是遍历 run loop 的 block 的链表,在指定的 rlm 下执行 block,执行完节点的 block 以后会把该节点从链表中移除,最后更新链表的头节点和尾节点。

__CFRunLoopDoSources0 函数是遍历收集 rlm 的 _source0 把 Valid、Signaled 的 CFRunLoopSourceRef 收集起来,然后执行以 source0 的 info 为参数执行 source0 的 perform 函数,且会把 CFRunLoopSourceRef 置为 UnsetSignaled,等待被再次标记并执行。

__CFRunLoopDoTimers 函数执行 CFRunLoopTimerRef 的回调函数并更新其 _fireTSR_nextFireDate


31. mach_msg 函数。

 Run Loop 最核心的事情就是保证线程在没有消息时休眠以避免系统资源占用,有消息时能够及时唤醒。Run Loop 的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件 Darwin 中的 Mach 来完成的。Mach 与 BSD、File System、Mach、Networking 共同位于 Kernel and Device Drivers 层。

 在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为 “对象”,和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。“消息”(mach msg)是 Mach 中最基础的概念,消息在两个端口 (mach port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

 Mach 是 Darwin 的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在 Mach 中,进程、线程间的通信是以消息(mach msg)的方式来完成的,而消息则是在两个 mach port 之间进行传递(或者说是通过 mach port 进行消息的传递)(这也正是 Source1 之所以称之为 Port-based Source 的原因,因为它就是依靠 mach msg 发送消息到指定的 mach port 来唤醒 run loop)。

 为了实现消息的发送和接收,mach_msg 函数实际上是调用了一个 Mach 陷阱 (trap),即函数 mach_msg_trap,陷阱这个概念在 Mach 中等同于系统调用。当在用户态调用 mach_msg_trap 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg 函数会完成实际的工作。

 run loop 的核心就是一个 mach_msg ,run loop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,会看到主线程调用栈是停留在 mach_msg_trap 这个地方

 (mach_msg 函数可以设置 timeout 参数,如果在 timeout 到来之前没有读到 msg,当前线程的 run loop 会处于休眠状态。)

 消息的发送和接收统一使用 mach_msg 函数,而 mach_msg 的本质是调用了 mach_msg_trap,这相当于一个系统调用,会触发内核态与用户态的切换。

 点击 App 图标,App 启动完成后处于静止状态(一般如果没有 timer 需要一遍一遍执行的话),此时主线程的 run loop 会进入休眠状态,通过在主线程的 run loop 添加 CFRunLoopObserverRef 在回调函数中可看到主线程的 run loop 的最后活动状态是 kCFRunLoopBeforeWaiting,此时点击 Xcode 控制台底部的 Pause program execution 按钮,从 Xcode 左侧的 Debug navigator 可看到主线程的调用栈停在了 mach_msg_trap。

 _timerPort 是 __CFRunLoopMode 的一个成员变量。在 macOS 下同时支持 dispatch_source 和 mk 构建 timer,在 iOS 下则只支持使用 mk。这里我们只关注 _timerPort。我们在 Cocoa Foundation 层会通过手动创建并添加计时器 NSTimer 到 run loop 的指定 run loop mode 下,同样在 Core Foundation 层会通过创建 CFRunLoopTimerRef 实例并把它添加到 run loop 的指定 run loop mode 下,内部实现是则是把 CFRunLoopTimerRef 实例添加到 run loop mode 的 _timers 集合中,当 _timers 集合中的计时器需要执行时则正是通过 _timerPort 来唤醒 run loop,且 run loop mode 的 _timers 集合中的所有计时器共用这一个 _timerPort。

 这里我们可以做一个验证,我们为主线程添加一个 CFRunLoopOberver 观察 main run loop 的状态变化和一个 1 秒执行一次的 NSTimer。程序运行后可看到一直如下的重复打印:(代码过于简单,这里就不贴出来了)

...
⏰⏰⏰ timer 回调...
🎯... kCFRunLoopBeforeTimers
🎯... kCFRunLoopBeforeSources
🎯... kCFRunLoopBeforeWaiting
🎯... kCFRunLoopAfterWaiting
⏰⏰⏰ timer 回调...
🎯... kCFRunLoopBeforeTimers
🎯... kCFRunLoopBeforeSources
🎯... kCFRunLoopBeforeWaiting
🎯... kCFRunLoopAfterWaiting
...

 计时器到了触发时间唤醒 run loop(kCFRunLoopAfterWaiting)执行计时器的回调,计时器回调执行完毕后 run loop 又进入休眠状态(kCFRunLoopBeforeWaiting)然后到达下次计时器触发时间时 run loop 再次被唤醒,如果不手动停止计时器的话则会这样一直无限重复下去。

32. 回顾 run loop mode item(Source0 和 Source1 的区别)。

 我们首先再次回顾一下 Source/Timer/Observer,因为 run loop 正是通过这些 run loop mode item 来向外提供功能支持的。

  1. CFRunLoopSourceRef 是事件产生的地方。Source 有两个版本:Source0 和 Source1。
  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 run loop,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息(mach_msg),这种 Source 能主动唤醒 run loop 的线程。

 Source0 中仅有一些回调函数会在 run loop 的本次循环中执行,而 Source1 中有 mach port 可用来主动唤醒 run loop。

  1. CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是 toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 run loop 时,run loop 会注册对应的时间点,当时间点到时,run loop会被唤醒以执行那个回调。
  2. CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 run loop 的状态发生变化时,观察者就能通过这个回调接收到。

33. run loop 控制自动释放池的 push 和 pop。

 自动释放池什么时候执行 pop 操作把池中的对象的都执行一次 release 呢?这里要分两种情况:

  • 一种是我们手动以 @autoreleasepool {...} 的形式添加的自动释放池,使用 clang -rewrite-objc 转换为 C++ 后其实是
struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

/* @autoreleasepool */ 
{ 
    // 直接构建了一个 __AtAutoreleasePool 实例,
    // 构造函数调用了 AutoreleasePoolPage 的 push 函数,构建了一个自动释放池。
    __AtAutoreleasePool __autoreleasepool;
    // ...
}

 可看到 __autoreleasepool 是被包裹在一对 {} 之中的,当出了右边花括号时自动释放池便会执行 pop 操作,也可理解为如下代码:

void *pool = objc_autoreleasePoolPush();
// {}中的代码
objc_autoreleasePoolPop(pool);

 在原始 main 函数中,打一个断点,并开启 Debug Workflow 的 Always Show Disassembly 可看到对应的汇编代码:

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);
}

 由于上面代码中自动释放池什么也没有放,Push 完便接着 Pop 了。

...
0x101319b78 <+32>:  bl     0x101319eb8               ; symbol stub for: objc_autoreleasePoolPush
0x101319b7c <+36>:  bl     0x101319eac               ; symbol stub for: objc_autoreleasePoolPop
...
  • 一种是由 run loop 创建的自动释放池。ibireme 大佬如是说:

 App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。  第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。  第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。  在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。深入理解RunLoop

 下面我们试着验证一下上面的结论,在 application:didFinishLaunchingWithOptions: 函数中添加一个断点,在控制台打印 po [NSRunLoop mainRunLoop],可看到在 main run loop 的 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode 模式下的 observers 中均有如下两个 CFRunLoopObserver。

"<CFRunLoopObserver 0x600001c30320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x60000235dc20 [0x7fff80617cb0]>{type = mutable-small, count = 0, values = ()}}"

"<CFRunLoopObserver 0x600001c30280 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54), context = <CFArray 0x60000235dc20 [0x7fff80617cb0]>{type = mutable-small, count = 0, values = ()}}"

 order 是 -2147483647 的 CFRunLoopObserver 优先级最高,会在其它所有 CFRunLoopObserver 之前回调,然后它的 activities 是 0x1,对应 kCFRunLoopEntry = (1UL << 0),即只观察 kCFRunLoopEntry 状态,回调函数是 _wrapRunLoopWithAutoreleasePoolHandler,添加一个 _wrapRunLoopWithAutoreleasePoolHandler 符号断点,添加一个 objc_autoreleasePoolPush 符号断点,运行程序,并在控制台 bt 打印函数堆栈,确实能看到如下的函数调用:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
  * frame #0: 0x00000001dd971864 libobjc.A.dylib`objc_autoreleasePoolPush // push 构建自动释放池
    frame #1: 0x00000001de78d61c CoreFoundation`_CFAutoreleasePoolPush + 16
    frame #2: 0x000000020af66324 UIKitCore`_wrapRunLoopWithAutoreleasePoolHandler + 56
    frame #3: 0x00000001de7104fc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32 // 执行 run loop observer 回调函数,
    frame #4: 0x00000001de70b224 CoreFoundation`__CFRunLoopDoObservers + 412
    frame #5: 0x00000001de70af9c CoreFoundation`CFRunLoopRunSpecific + 412
    frame #6: 0x00000001e090c79c GraphicsServices`GSEventRunModal + 104
    frame #7: 0x000000020af6cc38 UIKitCore`UIApplicationMain + 212
    frame #8: 0x0000000100a75b90 Simple_iOS`main(argc=1, argv=0x000000016f38f8e8) at main.m:77:12
    frame #9: 0x00000001de1ce8e0 libdyld.dylib`start + 4
(lldb) 

 在主线程中确实看到了 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ 执行 CFRunLoopObserver 的回调函数调用了 _wrapRunLoopWithAutoreleasePoolHandler 函数接着调用了 objc_autoreleasePoolPush 创建自动释放池。

 order 是 2147483647 的 CFRunLoopObserver 优先级最低,会在其它所有 CFRunLoopObserver 之后回调,然后它的 activities 是 0xa0(0b10100000),对应 kCFRunLoopBeforeWaiting = (1UL << 5) 和 kCFRunLoopExit = (1UL << 7),即观察 run loop 的即将进入休眠和 run loop 退出的两个状态变化,回调函数的话也是 _wrapRunLoopWithAutoreleasePoolHandler,我们再添加一个 objc_autoreleasePoolPop 符号断点,此时需要我们添加一些测试代码,我们添加一个 main run loop 的观察者,然后再添加一个主线程的 main run loop 的 timer,程序启动后我们可看到控制台如下循环打印:

 🎯... kCFRunLoopAfterWaiting
 ⏰⏰⏰ timer 回调...
 🎯... kCFRunLoopBeforeTimers
 🎯... kCFRunLoopBeforeSources
 🎯... kCFRunLoopBeforeWaiting
 🎯... kCFRunLoopAfterWaiting
 ⏰⏰⏰ timer 回调...

 主线程进入了一种 “休眠--被 timer 唤醒执行回调--休眠” 的循环之中,此时我们打开 _wrapRunLoopWithAutoreleasePoolHandler 断点发现程序进入,然后再打开 objc_autoreleasePoolPop 断点,然后点击 Continue program execution 按钮,此时会进入 objc_autoreleasePoolPop 断点,在控制台 bt 打印函数调用栈:

(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00000001dd9718f8 libobjc.A.dylib`objc_autoreleasePoolPop
    frame #1: 0x00000001de78cba0 CoreFoundation`_CFAutoreleasePoolPop + 28
    frame #2: 0x000000020af66360 UIKitCore`_wrapRunLoopWithAutoreleasePoolHandler + 116
    frame #3: 0x00000001de7104fc CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__ + 32
    frame #4: 0x00000001de70b224 CoreFoundation`__CFRunLoopDoObservers + 412
    frame #5: 0x00000001de70b7a0 CoreFoundation`__CFRunLoopRun + 1228
    frame #6: 0x00000001de70afb4 CoreFoundation`CFRunLoopRunSpecific + 436
    frame #7: 0x00000001e090c79c GraphicsServices`GSEventRunModal + 104
    frame #8: 0x000000020af6cc38 UIKitCore`UIApplicationMain + 212
    frame #9: 0x0000000100bc9b2c Simple_iOS`main(argc=1, argv=0x000000016f23b8e8) at main.m:76:12
    frame #10: 0x00000001de1ce8e0 libdyld.dylib`start + 4
(lldb)

 确实看到了 _wrapRunLoopWithAutoreleasePoolHandler 调用了 objc_autoreleasePoolPop。

 这样整体下来:Entry-->push ➡️ BeforeWaiting--->pop-->push ➡️ Exit-->pop,按照这样的顺序,保证了在每次 run loop 循环中都进行一次 push 和 pop。

 从上面 run loop observer 工作便知,每一次 loop,便会有一次 pop 和 push,因此我们得出:

  1. 如果手动添加 autoreleasePool,autoreleasePool 作用域里的自动释放对象会在出 pool 作用域的那一刻释放。
  2. 如果是 run loop 自动添加的 autoreleasePool,首先在 run loop 循环开启时 push 一个新的自动释放池,然后在每一次 run loop 循环将要进入休眠时 autoreleasePool 执行 pop 操作释放这次循环中所有的自动释放对象,并同时再 push 一个新的自动释放池在下一个 loop 循环中使用,这样保证 run loop 的每次循环中的创建的自动释放对象都得到释放,然后在 run loop 切换 mode 退出时,再执行最后一次 pop,保证在 run loop 的运行过程中自动释放池的 push 和 pop 成对出现。

34. NSTimer 的简单介绍和NSTimer 的循环引用问题。

 NSTimer 可以问的问题还挺多的,同事去腾讯面试时就被问到 NSTimer 是怎么执行的,下面有结合 run loop 的详细讲解。

 NSTimer.h 中提供了一组 NSTimer 的创建方法,其中不同构造函数的 NSInvocation、SEL、block 类型的参数分别代表 NSTimer 对象的不同的回调方式。其中 block 的回调形式是 iOS 10.0 后新增的,可以帮助我们避免 NSTimer 对象和其 target 参数的循环引用问题,timerWithTimeInterval...initWithFireDate 返回的 NSTimer 对象还需要我们手动添加到当前线程的 run loop 中,scheduledTimerWithTimeInterval... 构建的 NSTimer 对象则是默认添加到当前线程的 run loop 的 NSDefaultRunLoopMode 模式下的(必要的情况下我们还要再补一行把 timer 添加到当前线程的 run loop 的 NSRunLoopCommonModes 模式下)。

 block 回调的形式都有一个 API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); 标注,表示是 iOS 10 后新增的。

 下面五个方法返回的 NSTimer 对象需要手动调用 NSRunLoop 的 -(void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode; 函数添加到指定 run loop 的指定 mode 下。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;

 下面三个方法返回的 NSTimer 对象会被自动添加到当前线程的 run loop 的 default mode 下。

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

 如果使用 scheduledTimerWithTimeInterval... 则需要注意 run loop 的 mode 切换到 UITrackingRunLoopMode 模式时,计时器会停止回调,当滑动停止 run loop 切回到 kCFRunLoopDefaultMode 模式时计时器又开始正常回调,必要情况下我们需要把 timer 添加到 NSRunLoopCommonModes 模式下可保证 run loop 的 mode 切换不影响计时器的回调(此时的计时器对象会被同时添加到多个 common 标记的 run loop mode 的 _timers 中)。

 还有一个知识点需要注意一下,添加到 run loop 指定 mode 下的 NSTimer 会被 mode 所持有,因为它会被加入到 run loop mode 的 _timers 中去,如果 mode name 是 NSRunLoopCommonModes 的话,同时还会被加入到 run loop 的 _commonModeItems 中,所以当不再需要使用 NSTimer 对象计时时必须调用 invalidate 函数把它从 _timers 和 _commonModeItems 集合中移除。如下代码在 ARC 下打印各个计时器的引用计数可进行证实:

// timer 默认添加到 run loop 的 NSDefaultRunLoopMode 下,引用计数应该是 3 (觉得这里应该是 2 呀?)
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; // 3

// 起始引用计数是 1
NSTimer *timer2 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; // 1
// 把 timer2 添加到 run loop 的 NSDefaultRunLoopMode 时引用计数 +1  
// 被 timer2 和 NSDefaultRunLoopMode 的 _timers 持有
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:NSDefaultRunLoopMode]; // 2

NSTimer *timer3 = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) { }]; // 1
[[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSDefaultRunLoopMode]; // 2

// 把 timer3 添加到 run loop 的 NSRunLoopCommonModes 时引用计数 +3 
// 被 timer3、UITrackingRunLoopMode 的 _timers、NSDefaultRunLoopMode 的 _timers、run loop 的 _commonModeItems 持有
[[NSRunLoop currentRunLoop] addTimer:timer3 forMode:NSRunLoopCommonModes]; // 4

// timer3 调用 invalidate 函数后引用计数变回 1
// 被从两个 _timers 和 _commonModeItems 中移除后 -3
[timer3 invalidate]; // 1

 NSTimer 创建时会持有传入的 target:

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep NS_DESIGNATED_INITIALIZER;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;

 使用以上三个函数构建或初始化 NSTimer 对象时,NSTimer 对象会持有传入的 target 的,因为 NSTimer 对象回调时要执行 target 的 aSelector 函数,如果此时 target 同时也持有 NSTimer 对象的话则会构成循环引用导致内存泄漏,一般在 ViewController 中添加 NSTimer 属性会遇到此问题。解决这个问题的方法通常有两种:一种是将 target 分离出来独立成一个对象(在这个对象中弱引用 NSTimer 并将对象本身作为 NSTimer 的 target),控制器通过这个对象间接使用 NSTimer;另一种方式的思路仍然是转移 target,只是可以直接增加 NSTimer 扩展(分类),让 NSTimer 类对象做为 target,同时可以将操作 selector 封装到 block 中,示例代码如下。(类对象全局唯一且不需要也不能释放)iOS刨根问底-深入理解RunLoop

#import "NSTimer+Block.h"

@implementation NSTimer (Block)

- (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)seconds repeats:(BOOL)repeats block:(void (^)(void))block {
    // target 传入的是 self.class 即 NSTimer 类对象,然后计时器的回调函数就是 NSTimer 类对象的 runBlock: 函数,runBlock 是一个类方法,
    // 把回调的 block 放在 userInfo 中,然后在计时器的触发函数 runBlock: 中根据 NSTimer 对象读出其 userInfo 即为 block,执行即可。
    return [self initWithFireDate:date interval:seconds target:self.class selector:@selector(runBlock:) userInfo:block repeats:repeats];
}

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)seconds repeats:(BOOL)repeats block:(void (^)(void))block {
    // self 即为 NSTimer 类对象
    return [self scheduledTimerWithTimeInterval:seconds target:self selector:@selector(runBlock:) userInfo:block repeats:repeats];
}

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)seconds repeats:(BOOL)repeats block:(void (^)(void))block {
    // self 即为 NSTimer 类对象
    return [self timerWithTimeInterval:seconds target:self selector:@selector(runBlock:) userInfo:block repeats:repeats];
}

#pragma mark - Private methods

+ (void)runBlock:(NSTimer *)timer {
    // 从入参 timer 对象中读出 block 执行
    if ([timer.userInfo isKindOfClass:NSClassFromString(@"NSBlock")]) {
        void (^block)(void) = timer.userInfo;
        block();
    }
}

@end

 iOS 10.0 以后苹果也提供了 block 形式的 NSTimer 构建函数,我们直接使用即可。(大概现在还有 iOS 10.0 之前的用户吗)

 看到这里会发现计时器是不能暂停的,invalidate 函数是移除计数器使用的,所以无论是重复执行的计时器还是一次性的计时器只要调用 invalidate 方法则会变得无效,只是一次性的计时器执行完操作后会自动调用 invalidate 方法。所以想要暂停和恢复计时器的只能 invalidate 旧计时器然后再新建计时器,且当我们不再需要使用计时器时必须调用 invalidate 方法。


35. NSTimer(CFRunLoopTimerRef)的执行流程。

 CFRunLoopTimerRef 与 NSTimer 是可以 toll-free bridged(免费桥接转换)的。当 timer 加到 run loop 的时候,run loop 会注册对应的触发时间点,时间到了,run loop 若处于休眠则会被唤醒,执行 timer 对应的回调函数。下面我们沿着 CFRunLoopTimerRef 的源码来完整分析一下计时器的流程。

 首先是 CFRunLoopTimerRef 的创建函数:(详细分析可参考前面的:iOS 从源码解析Run Loop (四):Source、Timer、Observer 创建以及添加到 mode 的过程)

CFRunLoopTimerRef CFRunLoopTimerCreate(CFAllocatorRef allocator,
                                       CFAbsoluteTime fireDate,
                                       CFTimeInterval interval,
                                       CFOptionFlags flags,
                                       CFIndex order,
                                       CFRunLoopTimerCallBack callout,
                                       CFRunLoopTimerContext *context);

allocator 是 CF 下为新对象分配内存的分配器,可传 NULL 或 kCFAllocatorDefault。

fireDate 是计时器第一次触发回调的时间点,然后后续沿着 interval 间隔时间连续回调。

interval 是计时器的连续回调的时间间隔,如果为 0 或负数,计时器将触发一次,然后自动失效。

order 优先级索引,指示 CFRunLoopModeRef 的 _timers 中不同计时器的回调执行顺序。当前忽略此参数,传递 0。

callout 计时器触发时调用的回调函数。

context 保存计时器的上下文信息的结构。该函数将信息从结构中复制出来,因此上下文所指向的内存不需要在函数调用之后继续存在。如果回调函数不需要上下文的信息指针来跟踪状态,则可以为 NULL。其中的 void * info 字段内容是 callout 函数执行时的参数。

 CFRunLoopTimerCreate 函数中比较重要的是对触发时间的设置:

...
// #define TIMER_DATE_LIMIT    4039289856.0
// 如果入参 fireDate 过大,则置为 TIMER_DATE_LIMIT
if (TIMER_DATE_LIMIT < fireDate) fireDate = TIMER_DATE_LIMIT;

// 下次触发的时间
memory->_nextFireDate = fireDate;
memory->_fireTSR = 0ULL;

// 取得当前时间
uint64_t now2 = mach_absolute_time();
CFAbsoluteTime now1 = CFAbsoluteTimeGetCurrent();

if (fireDate < now1) {
    // 如果第一次触发的时间已经过了,则把 _fireTSR 置为当前
    memory->_fireTSR = now2;
} else if (TIMER_INTERVAL_LIMIT < fireDate - now1) {
    // 如果第一次触发的时间点与当前是时间差距超过了 TIMER_INTERVAL_LIMIT,则把 _fireTSR 置为 TIMER_INTERVAL_LIMIT
    memory->_fireTSR = now2 + __CFTimeIntervalToTSR(TIMER_INTERVAL_LIMIT);
} else {
    // 这里则是正常的,如果第一次触发的时间还没有到,则把触发时间设置为当前时间和第一次触发时间点的差值
    memory->_fireTSR = now2 + __CFTimeIntervalToTSR(fireDate - now1);
}
...

 这一部分代码保证计时器第一次触发的时间点正常。下面看一下把创建好的 CFRunLoopModeRef 添加到指定的 run loop 的指定的 run loop mode 下。

 CFRunLoopAddTimer 函数主要完成把 CFRunLoopTimerRef rlt 插入到 CFRunLoopRef rl 的 CFStringRef modeName 模式下的 _timer 集合中,如果 modeName 是 kCFRunLoopCommonModes 的话,则把 rlt 插入到 rl 的 _commonModeItems 中,然后调用 __CFRunLoopAddItemToCommonModes 函数把 rlt 添加到所有被标记为 common 的 mode 的 _timer 中,同时也会把 modeName 添加到 rlt 的 _rlModes 中,记录 rlt 都能在那种 run loop mode 下执行。

void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName);

 上面添加完成后,会调用 __CFRepositionTimerInMode 函数,然后调用 __CFArmNextTimerInMode,再调用 mk_timer_arm 函数把 CFRunLoopModeRef 的 _timerPort 和一个时间点注册到系统中,等待着 mach_msg 发消息唤醒休眠中的 run loop 起来执行到达时间的计时器。

 同一个 run loop mode 下的多个 timer 共享同一个 _timerPort,这是一个循环的流程:注册 timer(mk_timer_arm)—接收 timer(mach_msg)—根据多个 timer 计算离当前最近的下次回调的触发时间点—注册 timer(mk_timer_arm)。

 在使用 CFRunLoopAddTimer 添加 timer 时的调用堆栈如下:

CFRunLoopAddTimer
__CFRepositionTimerInMode
    __CFArmNextTimerInMode
        mk_timer_arm

 然后 mach_msg 收到 timer 事件时的调用堆栈如下:

__CFRunLoopRun
__CFRunLoopDoTimers
    __CFRunLoopDoTimer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFArmNextTimerInMode
    mk_timer_arm 

 每次计时器都会调用 __CFArmNextTimerInMode 函数,注册计时器的下次回调。休眠中的 run loop 通过当前的 run loop mode 的 _timerPort 端口唤醒后,在本次 run loop 循环中在 __CFRunLoopDoTimers 函数中循环调用 __CFRunLoopDoTimer 函数,执行达到触发时间的 timer 的 _callout 函数。__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(rlt->_callout, rlt, context_info); 是执行计时器的 _callout 函数。


36. NSTimer 的不准时问题。

 通过上面的 NSTimer 执行流程可看到计时器的触发回调完全依赖 run loop 的运行(macOS 和 iOS 下都是使用 mk_timer 来唤醒 run loop),使用 NSTimer 之前必须注册到 run loop,但是 run loop 为了节省资源并不会在非常准确的时间点调用计时器,如果一个任务执行时间较长(例如本次 run loop 循环中 source0 事件执行时间过长或者计时器自身回调执行时间过长,都会导致计时器下次正常时间点的回调被延后或者延后时间过长的话则直接忽略这次回调(计时器回调执行之前会判断当前的执行状态 !__CFRunLoopTimerIsFiring(rlt),如果是计时器自身回调执行时间过长导致下次回调被忽略的情况大概与此标识有关 )),那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer 提供了一个 tolerance 属性用于设置宽容度,即当前时间点已经过了计时器的本次触发点,但是超过的时间长度小于 tolerance 的话,那么本次计时器回调还可以正常执行,不过是不准时的延后执行。 tolerance 的值默认是 0,最大值的话是计时器间隔时间 _interval 的一半,可以根据自身的情况酌情设置 tolerance 的值,(其实还是觉得如果自己的计时器不准时了还是应该从自己写的代码中找原因,自己去找该优化的点,或者是主线实在优化不动的话就把计时器放到子线程中去))。

 (NSTimer 不是一种实时机制,以 main run loop 来说它负责了所有的主线程事件,例如 UI 界面的操作,负责的运算使当前 run loop 持续的时间超过了计时器的间隔时间,那么计时器下一次回调就被延后,这样就造成 timer 的不准时,计时器有个属性叫做 tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。如果延后时间过长的话会直接导致计时器本次回调被忽略。)

 在苹果的 Timer 文档中可看到关于计时精度的描述:Timer Programming Topics

 Timing Accuracy  A timer is not a real-time mechanism; it fires only when one of the run loop modes to which the timer has been added is running and able to check if the timer’s firing time has passed. Because of the various input sources a typical run loop manages, the effective resolution of the time interval for a timer is limited to on the order of 50-100 milliseconds. If a timer’s firing time occurs while the run loop is in a mode that is not monitoring the timer or during a long callout, the timer does not fire until the next time the run loop checks the timer. Therefore, the actual time at which the timer fires potentially can be a significant period of time after the scheduled firing time.    A repeating timer reschedules itself based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.

 计时器不是一种实时机制;仅当已添加计时器的 run loop mode 之一正在运行并且能够检查计时器的触发时间是否经过时,它才会触发。由于典型的 run loop 管理着各种输入源,因此计时器时间间隔的有效分辨率被限制在 50-100 毫秒的数量级。如果在运行循环处于不监视计时器的模式下或长时间调用期间,计时器的触发时间发生,则直到下一次运行循环检查计时器时,计时器才会启动。因此,计时器可能实际触发的时间可能是在计划的触发时间之后的相当长的一段时间。

 重复计时器会根据计划的触发时间而不是实际的触发时间重新安排自身的时间。例如,如果计划将计时器在特定时间触发,然后每5秒触发一次,则即使实际触发时间被延迟,计划的触发时间也将始终落在原始的5秒时间间隔上。如果触发时间延迟得太远,以至于超过了计划的触发时间中的一个或多个,则计时器在该时间段仅触发一次;计时器会在触发后重新安排为将来的下一个计划的触发时间。

 如下代码申请一条子线程然后启动它的 run loop,可观察 timer 回调的时间点。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    //  sleep(1);
        NSLog(@"⏰⏰⏰ timer 回调...");
    }];
    
    // 2 秒后在 thread 线程中执行 caculate 函数
    [self performSelector:@selector(caculate) withObject:nil afterDelay:2];
    
    [[NSRunLoop currentRunLoop] run];
    }];
    [thread start];
}

- (void)caculate {
    NSLog(@"👘👘 %@", [NSThread currentThread]);
    sleep(2);
}

 运行代码根据打印时间可看到前两秒计时器正常执行,然后 caculate 的执行导致定时器执行被延后两秒,两秒以后计时器继续正常的每秒执行一次。如果把计时器的回调中的 sleep(1) 注释打开,会发现计时器是每两秒执行一次。


37. performSelector 系列函数

 当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 run loop 中。所以如果当前线程没有 run loop,则这个方法会失效。

 在 NSObject 的 NSDelayedPerforming 分类下声明了如下函数。

@interface NSObject (NSDelayedPerforming)
// 指定 NSRunLoopMode
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
// 默认在 NSDefaultRunLoopMode
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
@end

 performSelector:withObject:afterDelay:inModes: 在延迟之后使用指定的模式在当前线程上调用接收方(NSObject 及其子类对象)的方法。

aSelector:一个选择器,用于标识要调用的方法。该方法应该没有明显的返回值(void),并且应该采用 id 类型的单个参数,或者不带参数。

anArgument:调用时传递给方法的参数。如果该方法不接受参数,则传递 nil。

delay:发送消息之前的最短时间。指定延迟 0 不一定会导致选择器立即执行。选择器仍在线程的 run loop 中排队并尽快执行。

modes:一个字符串数组,用于标识与执行选择器的 timer 关联的模式。此数组必须至少包含一个字符串。如果为此参数指定 nil 或空数组,则此方法将返回而不执行指定的选择器。

 此方法设置一个 timer,以便在当前线程的 run loop 上执行 aSelector 消息。timer 配置在 modes 参数指定的模式下运行。当 timer 触发时,线程尝试从 run loop 中取出消息并执行选择器。如果 run loop 正在运行并且处于指定的模式之一,则它成功;否则, timer 将等待直到 run loop 处于这些模式之一。关于它会在当前 run loop 的 run loop mode 下添加一个 timer 可通过如下代码验证:

    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"🧗‍♀️🧗‍♀️ ....");

        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
            NSLog(@"⏰⏰⏰ timer 回调...");
        }];

        [self performSelector:@selector(caculate) withObject:nil afterDelay:2]; // ⬅️ 断点 1
        
        NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // ⬅️ 断点 2
        [runloop run];
    }];
    [thread start];

 分别在执行到以上两个断点时,在控制台通过 po [NSRunLoop currentRunLoop] 打印:

// 断点 1 处:po [NSRunLoop currentRunLoop]
...
    timers = <CFArray 0x28314e9a0 [0x20e729430]>{type = mutable-small, count = 1, values = (
    0 : <CFRunLoopTimer 0x28204df80 [0x20e729430]>{valid = Yes, firing = No, interval = 1, tolerance = 0, next fire date = 631096717 (-14.273319 @ 16571855540445), callout = (NSTimer) [_NSTimerBlockTarget fire:] (0x1df20764c / 0x1df163018) (/System/Library/Frameworks/Foundation.framework/Foundation), context = <CFRunLoopTimer context 0x28154b900>}
)
...
// 断点 2 处:po [NSRunLoop currentRunLoop]
...
    timers = <CFArray 0x28314e9a0 [0x20e729430]>{type = mutable-small, count = 2, values = (
    0 : <CFRunLoopTimer 0x28204df80 [0x20e729430]>{valid = Yes, firing = No, interval = 1, tolerance = 0, next fire date = 631096717 (-32.979197 @ 16571855540445), callout = (NSTimer) [_NSTimerBlockTarget fire:] (0x1df20764c / 0x1df163018) (/System/Library/Frameworks/Foundation.framework/Foundation), context = <CFRunLoopTimer context 0x28154b900>}
    1 : <CFRunLoopTimer 0x28204db00 [0x20e729430]>{valid = Yes, firing = No, interval = 0, tolerance = 0, next fire date = 631096747 (-2.84795797 @ 16572578697099), callout = (Delayed Perform) ViewController caculate (0x1df1f4094 / 0x10093ab88) (/var/containers/Bundle/Application/C2E33DEA-1FB0-48A0-AEDD-2D13AF564389/Simple_iOS.app/Simple_iOS), context = <CFRunLoopTimer context 0x28003d4c0>}
)
...

 可看到 performSelector:withObject:afterDelay: 添加了一个 timer。

 如果希望在 run loop 处于默认模式以外的模式时使消息出列,请使用 performSelector:withObject:afterDelay:inModes: 方法。如果不确定当前线程是否为主线程,可以使用 performSelectorOnMainThread:withObject:waitUntilDone:performSelectorOnMainThread:withObject:waitUntilDone:modes: 方法来确保选择器在主线程上执行。要取消排队的消息,请使用 cancelPreviousPerformRequestsWithTarget:cancelPreviousPerformRequestsWithTarget:selector:object: 方法。

 此方法向其当前上下文的 runloop 注册,并依赖于定期运行的 runloop 才能正确执行。一个常见的上下文是当调度队列调用时,你可能调用此方法并最终注册到一个不自动定期运行的 runloop。如果在调度队列上运行时需要此类功能,则应使用 dispatch_after 和相关方法来获得所需的行为。(类似的还有 NSTimer 不准时时,也可以使用 dispatch_source 来替代)

🎉🎉🎉 未完待续...