阿里、字节:一套高效的iOS面试题(五 - Runloop)

·  阅读 964

runloop

撸面试题中,文中内容基本上都是搬运自大佬博客及自我理解,可能有点乱,不喜勿喷!!!

原文题目来自:阿里、字节:一套高效的iOS面试题

runloop对于一个标准的iOS开发来说都不陌生,应该说熟悉runloop是标配,下面就随便列几个典型问题吧

1、app如何接收到触摸事件的?

硬件事件(触摸、按键、加速等) >>> IOKit(生成 IOHIDEvent 事件) >>> SpringBoard.app(查找前台 App,big通过 Mach Port 转发) >>> App(Source1 触发回调) >>> App 内部分发(调用 _UIApplicationHandleEventQueue(),将 IOHIDEvent 处理包装为 UIEvent) >>> 查找最佳响应者 >>> 开发者接手事件

详细了解请往后看

2、为什么只有主线程的runloop是开启的

因为主线的 Runloop 是在 App 启动时系统自动为我们创建好的。 而子线程的 Runloop 则需要我们自己创建和管理。

3、为什么只在主线程刷新UI?

我们先来看一下 UIKit | Apple Developer Documentation,里边有这样一段话:

翻译一下:除非另有说明,只能在 主线程(main thread) 或者 主调度队列(main dispatch queue) 中使用 UIKit 的类。这个限制适用于所有 UIResponder 的派生类或以任何方式涉及到用户交互界面的操作。

  1. UIKit 并不是线程安全的。加入在两个线程中对同一个控件设置了同一张背景图片,很有可能由于背景图片被释放两次,从而导致程序崩溃。

  2. 根据事件响应机制(UIResponder),所有事件接收都是在主线程的 UIApplication 中,子线程无法快速有效地接收并处理事件。同时,根据之前的 Hit-Testing 需要遍历所有 subviews,如果一个线程移除掉了某一个 subview,此时也很可能导致程序奔溃。

  3. 子线程如果要对其他 UI 进行更新,必须等到子线程结束运行。按钮点击的 UI 更新效果必须是实时展示,如果放到子线程中做,必须等到子线程结束运行在更新,那此时更新还有什么意义呢?

4、PerformSelector和runloop的关系!

PerformSelector 只是 Runloop 诸多事件源的一种,其依赖于 Runloop 才能正常执行。如果没有 Runloop ,那么 PerformSelector 将失效。

5、何使线程保活?

其实就是常驻线程,给线程创建一个 Runloop :

[NSRunloop currentRunloop];

搞事情~~~

苹果 CFRunloopRef 开源代码

苹果跨平台 CoreFoundation 开源代码

1000 Runloop到底是个什么东西?

我们先看一下 苹果官网 在 Runloop - Foundation | Apple Developer Documentation 中关于 Runloop 的一些描述:

The programmatic interface to objects that manage input sources. 讲人话:管理输入源的编程接口对象。

Warning
讲人话:Runloop 的类一般都没有设计成线程安全的,且它的方法应该只在其所在线程的上下文被调用。永远不要试图在其他线程调用 Runloop 对象的方法,这样做很可能导致无法预期的结果。

苹果官方 Runloop模型图

这里我们可以看到:Runloop 接收两种事件:输入源(Input sources)、(Timer sources)。其中输入源又有什么呢,我们看这里:

Overview

讲人话:Runloop 对象处理诸如鼠标键盘等来自桌面系统的输入源事件、Port 事件、NSConnection 事件。当然还有 Timer 事件。

从字面意思看,runloop 就是 运行 + 循环

我们先看一下这两个方法:

int main1(int argc, char *argv[]) {
    NSLog(@"Hello, world!!!");
    return 0;
}


int main2(int argc, char *argv[]) {
    NSLog(@"Hello, world!!!");
    BOOL isAppRunning = YES;
    do {
        // TODO: --
    } while (isAppRunning)
    return 0;
}

我们可以看一下上边两个 main 函数, main1 打印出 “Hello, world!!!” 结束了,而 main2 可以一直运行。其原因不言而喻。

现在,我们再看一下这个 main 方法(取自 Xcode 创建工程自动生成的 main.m)

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

简而言之,main 函数里的 UIApplicationMain 函数内部启动了一个 Runloop,所以 UIApplicationMain 一直没有返回,保持了程序的持续运行。

总结

  • Runloop 实际上是一个对象,这个对象在一个永不结束的循环中处理程序运行期间出现的各类事件(触摸、加速、远程、计时器、Selector、UI刷新等等),从而保持程序的持续运行。
  • Runloop 在空闲的时候(没有需要处理的事件),会使线程进入睡眠状态,从而节省 CPU 资源,以提高程序性能。

1001 Runloop 的作用

  1. 保证线程的持续运行(毕竟都在一个循环里了);
  2. 处理 App 运行期间的各种事件( 如触摸事件、计时器事件、Selector事件、加速事件、远程事件等);
  3. 节省 CPU 资源,提高程序性能。在没有事件需要处理时,Runlopp 就会进入睡眠状态,直到收到事件被唤醒。

1002 Runloop 与线程

我们知道线程是用来执行特定的一个任务或一系列任务。默认情况下,线程执行完既定的任务之后就退出了,不能再继续执行了。此时,如果我们想要线程不退出且能持续执行任务,苹果采用了一种 Runloop 的方式来解决这个问题。

  1. 一条线程对应了一个 Runloop。每条线程都有与之对应的唯一的 Runloop 对象;
  2. Runloop 并不能保证线程安全。我们只能在线程内部操作当前线程的 Runloop 对象,而不能在当前线程内操作别的线程的 Runloop 对象;
  3. Runloop 对象在第一次获取 Runloop 时创建,在线程结束时销毁;
  4. 主线程的 Runloop 对象系统自动在程序启动的时候帮我们创建好了,而子线程的 Runloop 对象则需要我们自己创建与维护。

Note:所有 Runloop 保存在一个全局的字典里,线程作为 key,Runloop 作为 value。

1003 如何获取 Runloop

Overview

上边这个图片是来自苹果官网 Runloop - Foundation | Apple Developer Documentation 的一张截图。

苹果并不允许我们随意通过 alloc 与 init 来创建 Runloop,而是在我们第一次获取 Runloop 时为我们创建。

  • Foundation
[NSRunloop currentRunloop];  // 获取当前线程的 Runloop 对象
[NSRunloop mainRunloop];  // 获取主线程的 Runloop 对象
  • Core Foundation
CFRunloopGetCurrent();  // 获取当前线程的 Runloop 对象
CFRunloopGetMain();  // 获取主线程的 Runloop 对象

1004 Runloop 相关类

参考链接:iOS 多线程:『RunLoop』详尽总结 参考链接:深入理解RunLoop

要搞清楚 Runloop 的运行机制,我们先来看一下 Core Foundation 框架里关于 Runloop 的五个类:

  1. CFRunloopRef :Runloop 的对象
  2. CFRunloopModeRef : Runloop 的运行模式
  3. CFRunloopSourceRef : Runloop 的输入源 / 事件源(上边提到过)
  4. CFRunloopTimerRef : Runloop 模型图中的计时源
  5. CFRunloopObserverRef : 观察者,可以监听到 Runloop 的状态改变

Runloop - 五个类的关系

从上图可以看出上边五个类的关系:

  • 一个 Runloop 对象(CFRunloopRef) 中包含多个 运行模式(CFRunloopModeRef)。
  • 每个运行模式(CFRunloopModeRef)中包含多个输入源(CFRunloopSourceRef)、观察者(CFRunloopObserverRef)、计时器(CFRunloopTimerRef)。
  1. 每次 Runloop 启动时,只能指定一个运行模式(CFRunloopModeRef),这个运行模式被称为当前模式(CurrentMode)。
  2. 如果需要切换运行模式(CFRunloopModeRef),只能先退出当前 loop,再重新指定一个运行模式(CFRunloopModeRef)进入新的 loop。
  3. 这样做的目的将不同运行模式(CFRunloopModeRef)中的输入源(CFRunloopSourceRef)、观察者(CFRunloopObserverRef)、计时器(CFRunloopTimerRef)完全分隔开,互不影响。

1004.1 CFRunloopSourceRef

源,事件源,事件产生的地方 。

Source 其实有两种分类方法。

一类是按照苹果官方文档理论来分类:

  • Input Sources
    1. Port-Based Sources (基于端口)
    2. Custom Input Sources (自定义)
    3. Cocoa Perform Selector Sources (Cocoa 框架选择器)
  • Timer Sources

另一类是则是按照实际应用中通过调用函数来分类:

  • Source1 : 基于 Port。通过内核与其他其他线程通信,接收、分发系统事件。
  • Source0 : 非基于Port。用于用户主动触发的事件(点击 Button 或点击屏幕)。

实际操作

这里我们主要做 Source0 的测试工作:

  • touchesBegan

    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
        NSLog(@"点击了屏幕");
    }
    

    touchesBegan 堆栈

  • UIControlEventTouchUpInSide

    - (void)didTapButton:(UIButton *)sender {
        NSlog(@"按钮被点击了");
    }
    

    UIControlEventTouchUpInSide

  • PerformSelector

    [self performSelectorOnMainThrea:@selector(action_performSelector) object:nil waitUtilDone:YES];
    

    PerformSelector

可以看到,以上三次测试都能看出我们的事件都被分发到了Source0

1004.2 CFRunloopTimerRef

计时源,基于时间的触发器。基本上可以看做是 NSTimer(它与 NSTimer 是 toll-free bridged,可以混用)。其包含一个时间和一个回调(函数指针)。当 CFRunloopTimerRef 被加入到 Runloop 中,Runloop 会注册对应的时间点。当到相应的时间点时,Runloop 会被唤醒以执行回调。

小知识点1

[NSTimer scheduledTimerWithTimeInterval:5 target:self selector:@selector(action_timer) userInfo:nil repeats:YES];

等同于下边两句代码。

NSTimer *timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(action_timer) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

小知识点2

小知识点1 其实是有点问题的。因为如果当前界面上有 UIScrollView(UITableView / UICollectionView / UITextView),并且你正在滑动这个 UIScrollView 的话,那么 timer 是不会被触发。

原理:我们将 timer 加入到 NSDefaultRunloopMode 中了,正常情况下 Runloop 都是处理这个模式下的。但当滑动 UIScrollView 的时候,Runloop 就切换到 NSTrackingRunloopMode 了。而这个模式下我们并没有加入 timer。所以,timer 不会触发。 相同的,如果我们将 tiemr 加入到 NSTrackingRunloopMode 中,那么只有我们在滑动 UIScrollView 时 timer 才会被触发。 解决:将 timer 加入到 伪模式(NSRunLoopCommonModes) 中。别着急,等下就说到这个了。这样 timer 始终始终都能触发了。

1005.3 CFRunloopObserverRef

观察者。每个 Observer 都包含了一个回调(函数指针)。当 Runloop 状态发生改变时,调用每个 Observer 的回调函数。可以被 Observer 监测到的时间点:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           // 1: 即将进入 loop
    kCFRunLoopBeforeTimers = (1UL << 1),    // 2: 即将触发 timer
    kCFRunLoopBeforeSources = (1UL << 2),   // 4: 即将触发 source
    kCFRunLoopBeforeWaiting = (1UL << 5),   // 32: 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    // 64: 刚从休眠被唤醒
    kCFRunLoopExit = (1UL << 7),            // 128: 即将退出 loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   // 所有状态改变
};

测试一下

// 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(
                                                                       CFAllocatorGetDefault(),
                                                                       kCFRunLoopAllActivities,
                                                                       YES,
                                                                       0,
                                                                       ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"Runloop 状态改变了:%zd", activity);
    });
    
    // 将 observer 添加到当前 Runloop 中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
    
    // CF 对象,需手动管理内存
    CFRelease(observer);

Runloop 的状态最终变为 32,也就是kCFRunLoopBeforeWaiting 了。这说明 Runloop 在没有事件需要处理的时候回自动 进入休眠状态。

1004.4 CFRunloopModeRef

简化一下 CFRunloopModeRef的内存模型:

struct __CFRunLoopMode {
    CFStringRef _name;              // 模型名
    CFMutableSetRef _sources0;      // Set: source0 非 port 事件源
    CFMutableSetRef _sources1;      // Set: source1 port 事件源
    CFMutableArrayRef _observers;   // Array: 观察者
    CFMutableArrayRef _timers;      // Array:计时源
};

苹果默认定义了多种运行模式(CFRunloopModeRef):

  1. kCFRunloopDefaultMode : App 的默认运行模式,通常主线程处于这种模式;
  2. UITrackingRunloopMode : 跟踪用户交互事件(用追踪于 UIScrollView 触摸滑动,保证界面滑动式不受其他模式影响);
  3. UIInitializationRunloopMode : 刚启动 App 时进入的第一个模式,启动完成后不再使用。
  4. GSEventReceiveRunloopMode : 接收系统内部事件,通常用不到的;
  5. kCFRunloopCommonMode : 伪模式,并不是真正的运行模式,其实只是被标记为 “Common” 属性的模式。预设的模式中 kCFRunloopDefaultMode 与 UITrackingRunloopMode 被标记为 “Common” 属性了。应用:将 Timer 加入到 kCFRunloopCommonMode 中,这个 Timer 就可以在滑动 UIScrollView 和不滑动两种情况都正常触发了。

1004.5 CFRunloopRef

简单一点,CFRunloopRef 就是 Runloop 的实现, 这涉及到两个我们会使用到的类。

  • NSRunloop : Foundation 框架下的 Runloop 类。它时对 CFRunloopRef 的封装。
  • CFRunloopRef : 是 Core Foundation 框架下的 Runloop 的类。

我们打开 opensource.apple.com/tarballs/CF… 中的 CFRunloop.c

简化一下 __CFRunloop 的内存模型:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;    // Set : 被标记为 Common 的 Mode
    CFMutableSetRef _commonModeItems;    // Set< Source / Observer / Timer > : 当 Runloop 内容发生变化时,将自动同步到 _commonModes 的所有 Mode 中
    CFRunLoopModeRef _currentMode;    // Mode : Runloop 的当前 Mode
    CFMutableSetRef _modes;    // Set : Runloop 的所有 Mode
};

最后两条属性很清晰明了,那前两条属性是什么意思呢?

这里有一个叫做 CommonModes 的概念。一个 Mode 可以将自己标记为 “Common” 属性(通过将自身的 ModeName 添加到 Runloop 的 _commonMode 中)。每当 Runloop 的内容发生改变时, Runloop 都会自动将 _commonModeItems 里的内容(Source / Observer / Timer)同步到具有 “Common” 标记的所有 Mode 中。

那么,_commonModes 就是被标记为 “Common” 的模式(默认也就是 kCFRunloopDefaultModeUITrackingRunloopMode)。 _commonModeItems 也就是在 Runloop 内容发生改变时,自动同步到具有 “Common” 标记的模式的内容(CFRunloopSourceRef / CFRunloopObserverRef / CFRunloopTimerRef)了。

CFRunloopRef 对外暴露的 CFRunloopModeRef 管理接口只有 2 个(管理:增删改):

/** 为指定 runloop 添加指定名称的 mode */
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode);
/** 以指定名称的 mode 运行 runloop */
CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

CFRunloopModeRef 对外暴露的管理 mode item 的接口:

// 为指定 runloop 的指定 mode 添加或删除事件源
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode);

// 为指定 runloop 的指定 mode 添加或删除观察者
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);

// 为指定 runloop 的指定 mode 添加或删除 计时源
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

从上边开放的接口我们可以看出:

  • 只能通过 mode name 来操作 mode;
  • 传入一个新的 mode name 但是 runloop 内部没有相应的 mode 时,runloop 会自定创建一个对应的 CFRunloopModeRef
  • runloop 的 mode 只能添加,不能删除。

1005 Runloop 内部逻辑

先看一段源码(只看流程相关)

  • CFRunLoopRun
///Junes 用 DefaultMode 启动
void CFRunLoopRun(void) {
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}


///Junes 用指定 Mode 启动,允许设置 Runloop 超时时间
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {    
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}


///Junes Runloop 的实现
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
    
    ///Junes 根据 modeName 找到对应的 mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    ///Junes 如果 currentMode 不存在,或者 为空(没有 Source/timer/observer),直接返回
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        if (currentMode) __CFRunLoopModeUnlock(currentMode);
        
        __CFRunLoopUnlock(rl);
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    ///Junes 1、通知 Observers : Runloop 即将进入 loop。
    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    ///Junes 内部函数: 进入 __CFRunLoopRun() 开始 loop。
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    
    
    static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
        
        Boolean didDispatchPortLastTime = true;
        int32_t retVal = 0;
        do {
            ///Junes 2、通知 Observers:即将触发 Timer 回调
            if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
            
            ///Junes 3、通知 Observers:即将触发 Source0(非 port) 回调
            if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

            ///Junes  执行被加入的 block
            __CFRunLoopDoBlocks(rl, rlm);

            ///Junes 4、Runloop 触发 Source0 回调
            Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
            
            ///Junes 执行被加入的 block
            if (sourceHandledThisLoop) {
                __CFRunLoopDoBlocks(rl, rlm);
            }

            
            ///Junes 5、如果有 Source1(基于 port)处于 ready 状态,直接处理这个 Source1 然后跳转到消息处理
            if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
                msg = (mach_msg_header_t *)msg_buffer;
                if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                    goto handle_msg;
                }
            }

            didDispatchPortLastTime = false;

            ///Junes 6、通知 Observers:即将进入休眠
            if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
            __CFRunLoopSetSleeping(rl);

            
            ///Junes 7、调用 mach msg 等待 mach port 消息。runloop 进入休眠,直到被下面的某一个事件唤醒
            /**
             · 一个基于 port 的 Source1 事件
             · 一个 Timer 到触发事件了
             · Runloop 自身超时时间到了
             · 被其他调用者手动唤醒
             */
            if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
                msg = (mach_msg_header_t *)msg_buffer;
                if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                    goto handle_msg;
                }
            }

            ///Junes 8、通知 Observers : Runloop 刚刚被唤醒了
            __CFRunLoopUnsetSleeping(rl);
            if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
            
            ///Junes 收到消息,处理消息
            handle_msg:;

            ///Junes 9.1、如果 Timer 到时间了,触发这个 Timer 的回调
            if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
                CFRUNLOOP_WAKEUP_FOR_TIMER();
                
                if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                    // Re-arm the next timer
                    __CFArmNextTimerInMode(rlm, rl);
                }
            }
            ///Junes 9.2、如果有 dispatch 到 main queue 的 block,执行这个 block
            else if (livePort == dispatchPort) {
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                sourceHandledThisLoop = true;
                didDispatchPortLastTime = true;
            }
            ///Junes 9.3、如果一个 Source1(基于 port)发出事件了,处理这个事件
            else {
                CFRUNLOOP_WAKEUP_FOR_SOURCE();

                CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
                if (rls) {
                    mach_msg_header_t *reply = NULL;
                    sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                    if (NULL != reply) {
                        (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                        CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                    }
                }
            }

            
            ///Junes 执行被加入的 block
            __CFRunLoopDoBlocks(rl, rlm);
            

            if (sourceHandledThisLoop && stopAfterHandle) {
                ///Junes 进入 loop 时参数指定处理完事件就退出
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                ///Junes 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(rl)) {
                ///Junes 被外部调用者强制停止了
                __CFRunLoopUnsetStopped(rl);
                retVal = kCFRunLoopRunStopped;
            } else if (rlm->_stopped) {
                ///Junes 被外部调用者强制停止了(Mode 已经被强制停止了)
                rlm->_stopped = false;
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                ///Junes 当前 Mode 为空(没有任何 Source / Observer / Timer)
                retVal = kCFRunLoopRunFinished;
            }

        } while (0 == retVal);  // 继续 loop

        return retVal;
    }
    
    
    
    ///Junes 10、通知 Observers : Runloop 即将推出。
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}l

这里继续借用大佬的图:

Runloop 其实就是一个函数,内部是一个 do-while 循环。当调用 CFRunloopRun() 时,线程就会一直停在循环里,直到超时或者被手动停止。

1006 Runloop 的底层实现

在上边的 Runloop 实现的代码块中,有这样一句: __CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)。 这句代码的作用是 在 Runloop 休眠的同时等待 mach port 的消息

我们点进去看一下

Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
    for (;;) {
        mach_msg(msg, XXXXXXXXXXXXXXXXXXXXXXXXXXX);
    }
}

其实很简单,无限循环的 mach_msg 。而 mach_msg 是苹果操作系统核心 Darwin 用与端口之间的通信的。也就是说,Runloop 的核心是基于 mach port 的,

才疏学浅,照着大佬的话念一下(了解一下没坏处):

苹果官方将整个系统大致分为以上四层:

  • 应用层:用户能切身看到的图形应用,如 Spotlight、SpringBoard 等。
  • 应用框架层:开发者解除的 cocoa 框架。
  • 核心框架层:各种核心框架,Metal等。
  • Darwin:操作系统的核心,包括系统内核、驱动、shell 脚本等。这一层是开源的

依然是大佬的图

看图。硬件层之上的 IOKit、BSD、Mach(还有别的乱七八糟的)共同组成了 XNU 内核。

  • Mach : XNU 内核的内环。其作为一个微内核,只提供了处理器调度、进程间通信等非常少量的基础服务;
  • BSD : 环绕 Mach 的外环。提供了进程管理、文件系统、网络通讯等功能;
  • IOKit : 设备驱动。一个面向对象(C++)的框架。

Mach 提供的 API 非常少,且苹果不鼓励开发者使用。但这是房子的地基,一切的基础。在 Mach 中,所有的东西都通过自己的对象实现(进程、线程、虚拟内存等都被成为“对象”)。Mach 的各个对象间无法直接调用,只能通过消息传递的方式来实现通信。“消息”是 Mach 中最基础的概念,消息在不同的端口(port)之间传递,这就是 Mach 的 IPC(进程间通信)的核心。

Mach 的消息定义在 <mach/message.h> 中:

typedef struct {
    mach_msg_header_t header;
    mach_msg_body_t body;
} mach_msg_base_t;

typedef struct {
    mach_msg_bits_t msgh_bits;
    mach_msg_size_t msgh_size;
    mach_port_t msgh_remote_port;
    mach_port_t msgh_local_port;
    mach_port_name_t msgh_voucher_port;
    mach_msg_id_t msgh_id;
} mach_msg_header_t;

Mach 消息其实就是一个 header + body 的二进制数据包,其 header 定义了当前端口 local_port 和 目标端口 remote_port

Mach 发送消息与接收消息是同一个 API。其 option 标记了消息的流向:

mach_msg_return_t mach_msg(
			mach_msg_header_t *msg,
			mach_msg_option_t option,
			mach_msg_size_t send_size,
			mach_msg_size_t rcv_size,
			mach_port_name_t rcv_name,
			mach_msg_timeout_t timeout,
			mach_port_name_t notify);

感兴趣的去看看 Inter-Process Communicationsegmentfault.com/a/119000000…

1007 苹果用 Runloop 干了什么

先复制一下大佬的代码(懒得重新整理一次,而且我并看不到 _wrapRunLoopWithAutoreleasePoolHandler 等函数,只是一个地址而已。。。)

CFRunLoop {
    current mode = kCFRunLoopDefaultMode
    common modes = {
        UITrackingRunLoopMode
        kCFRunLoopDefaultMode
    }
 
    common mode items = {
 
        // source0 (manual)
        CFRunLoopSource {order =-1, {
            callout = _UIApplicationHandleEventQueue}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventSignalCallback }}
        CFRunLoopSource {order = 0, {
            callout = FBSSerialQueueRunLoopSourceHandler}}
 
        // source1 (mach port)
        CFRunLoopSource {order = 0,  {port = 17923}}
        CFRunLoopSource {order = 0,  {port = 12039}}
        CFRunLoopSource {order = 0,  {port = 16647}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventCallback}}
        CFRunLoopSource {order = 0, {port = 2407,
            callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
        CFRunLoopSource {order = 0, {port = 1c03,
            callout = __IOHIDEventSystemClientAvailabilityCallback}}
        CFRunLoopSource {order = 0, {port = 1b03,
            callout = __IOHIDEventSystemClientQueueCallback}}
        CFRunLoopSource {order = 1, {port = 1903,
            callout = __IOMIGMachPortPortCallback}}
 
        // Ovserver
        CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
        CFRunLoopObserver {order = 0, activities = 0x20,          // BeforeWaiting
            callout = _UIGestureRecognizerUpdateObserver}
        CFRunLoopObserver {order = 1999000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _afterCACommitHandler}
        CFRunLoopObserver {order = 2000000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
        CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
 
        // Timer
        CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
            next fire date = 453098071 (-4421.76019 @ 96223387169499),
            callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
    },
 
    modes = {
        CFRunLoopMode  {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },
 
        CFRunLoopMode  {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },
 
        CFRunLoopMode  {
            sources0 = {
                CFRunLoopSource {order = 0, {
                    callout = FBSSerialQueueRunLoopSourceHandler}}
            },
            sources1 = (null),
            observers = {
                CFRunLoopObserver >{activities = 0xa0, order = 2000000,
                    callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
            )},
            timers = (null),
        },
 
        CFRunLoopMode  {
            sources0 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventSignalCallback}}
            },
            sources1 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventCallback}}
            },
            observers = (null),
            timers = (null),
        },
        
        CFRunLoopMode  {
            sources0 = (null),
            sources1 = (null),
            observers = (null),
            timers = (null),
        }
    }
}

改写一下上边的 CFRunloopRun 的实现:

{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
 
        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
 
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
 
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
        
 
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
 
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
 
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
 
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
 
 
    } while (...);
 
    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

1007.1 AutoreleasePool

摘抄自深入理解RunLoop

App 启动后,苹果在主线程的 Runloop 里注册了两个 Observer,其回调都是 _wrapRunloopWithAutoreleasePoolHandler()

第一个 Observer 监视 kCFRunLoopEntry (即将进入 loop),其回调内会调用 _objc_autoreleasePoolPush() 来创建自动释放池。它的 order 为 -2147483647 ,具有最高优先级,保证自动释放池的创建在其他回调之前。

第二个 Observer 监视了两个事件: kCFRunLoopBeforeWaiting (准备进入休眠)时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 来释放旧的自动释放池并创建一个新的自动释放池;kCFRunLoopExit (Runloop 即将退出)时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,确保自动释放池的释放操作在其他所有回调之后。

在主线程执行的代码,通常都是写在事件回调、Timer 回调内,这些回调都会被 Runloop 创建的 AutoreleasePool 围绕,所以不会出现内存泄漏。

1007.2 事件响应

参考链接: iOS事件 - 响应者链和事件分发

参考链接:iOS触摸事件 hitTest touches nextResponder

iOS概念攻坚之路(六):事件传递与响应

image.png

起始阶段

  1. CPU 处于睡眠状态,等待事件;
  2. 手指触摸屏幕。

系统响应阶段

1、屏幕硬件感应到输入,将触摸事件传递给 输入输出驱动 IOKit; 2、IOKit.framework 将触摸事件封装为 IOHIDEvent 对象; 3、IOKit.framework 通过 进程间通信 Mach Port 将事件转发给 SpringBoard.app

SpringBoard.app 就是 iOS 的系统桌面。当触摸事件发生时,由负责管理桌面的 SpringBoard.app 统一管理和分发。(ps:毕竟触摸事件发生时,用户可能在刷微博,也可能在找 App)。

Mach Port 是进程端口,各个进程通过它进行通信。

桌面响应阶段

SpringBoard.app 的主线程 Runloop 收到 IOKit.framework 转发来的消息并苏醒,通过触发对应 Mach PortSource1 回调 __IOHIDEventSystemClientQueueCallback()

如果 SpringBoard.app 检测到有 App 在前台(假定为 xxx.app),SpringBoard.app 通过 Mach Port 将消息 转发给 xxx.app;如果没有 App 在前台运行,SpringBoard.app 进入 App 内部响应阶段的第二阶段,即触发 Source1 回调。

App 内部响应阶段

Source1 事件回调

苹果注册了一个 Source1 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

当发生一个硬件事件(如触摸、按键、摇晃、屏幕旋转等)后,首先由 IOKit.framework 将这个硬件事件封装为一个 IOHIDEvent 事件并转发给 SpringBoard.appSpringBoard.app 仅接收按键(音量/锁屏/静音)、触摸、加速、距离传感器等几种 Event,随后利用 Mach Port 转发给合适的 App。再然后苹果注册的 Source1 就会触发,并调用 _UIApplicationHandleEventQueue() 进行应用内的事件分发。

Source0 事件回调

Source1 中的 _UIApplicationHandleEventQeue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 检测 BeforeWaiting(Runloop 即将进入休眠) 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。

当有 GestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调也都会相应处理。

正式响应

简单了解过 Source0Source1 之后,我们看看触摸事件接下来是如何处理的:

  1. 前台 App 主线程 Runloop 收到 SpringBoard.app 转发来的消息苏醒,并触发对应 Mach PortSource1 回调 __IOHIDEventSystemClientQueueCallback()
  2. Source1 回调内部触发 Source0 回调 __UIApplicationHandleEventQueue()
  3. Source0 回调内部,将 IOHIDEvent 封装为 UIEvent
  4. 触摸事件给响应链开始...
  5. 通过递归调用 UIViewhitTest(_:with:),结合 point(inside:with:) 找到 UIEvent 中每一个 UITouch 所属的 UIView(其实是想找离触摸点最近的那个 UIView);
  6. 第五步的过程是从 UIView 的最顶层王最底层递归查询,但这并不是 UIReponder 响应链,事件响应是在 UIEvent 中每一个 UITouch 所属的 UIView 全都确定之后才开始的。

BUT

以下三种状态的 UIViewhitTest(_:with:) 不会被调用,通知也导致其子 UIViewhitTest(_:width:) 不会被调用。且之后响应事件是下向上传递的,这直接导致了这三种状态的 UIView 及其子 UIView 不会接收到任何事件:

  1. userInterfaceEnable = NO
  2. hideen = YES
  3. alpha = 0.0~0.01

把断点打在 UIView.hitTest(_:with:) 中时,对应调用堆栈如些:

UIView.hitTest()

响应者链

一个 App 中,每一个页面上都有多个 UI 控件,也就是 UIView。当一个触摸事件发生时,如何确定到底是哪个 UIView 来响应这个事件。

寻找 hit-test view

hit-test view 就是触摸事件所在的 view,也就是响应这个触摸事件的 view。而寻找这个 view 的过程就称为 Hit-Testing

发生触摸事件后,系统会将该事件加入到一个由 UIApplication 管理的事件队列中(FIFO,大家排队嘛)。

UIApplication 以先进先出的顺序依次处理分发事件。首先,取出第一个事件,通常将该事件发送给应用程序的主窗口 keyWindow(iOS13 之后改变为 UIWindow 的数组 windows : Array<UIWindow> 了)。主窗口在视图结构层次中找到一个最合适的视图来响应该触摸事件,当然这还没结束。找到这个最合适的视图之后,就会调用该视图的 touches 方法来做具体处理:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;
- (void)touchesCancelled:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event;

那么问题来了,响应链是如何找到这个最合适的视图呢?或者说,这个找到的视图怎么确定自己才是最适合的视图呢? 这里有以下几点:

  1. 自己是否能接收触摸事件;
  2. 触摸点是否在自己身上(也就是范围内);
  3. 从后往前遍历子视图数组,重复之前的第一步与第二步;
  4. 如何没有符合条件的子视图,那么自己就是最适合的。

这里有一份 hitTest 的伪代码:

/**
 * 调用时机:只要触摸事件传递到此 UIView,就会执行此方法
 * 作用:寻找最合适的事件响应者
 * UIApplication -> [UIWindow hitTest:withEvent:] 寻找最合适处理该事件的 UIView
 * param point: 当前手指触摸的点,以此 UIView 为坐标系
 * param event: 当前触摸事件
 */
- (UIView *)ly_hitTest:(CGPoint)point withEvent:(UIEvent *)event {
    // 2. 保证用户交互开启。若未开启,此 UIView 内没有合适的响应者
    if (!self.isUserInteractionEnabled) {
        return nil;
    }
    
    // 2. 保证未隐藏。若已隐藏,此 UIView 内没有合适的响应者
    if (!self.isHidden) {
        return nil;
    }
    
    // 3. 保证透明度大于 0.01, 若 小于 0.01,此 UIView 内没有合适的响应者
    if (self.alpha <= 0.01) {
        return nil;
    }
    
    // 4. 确保点击触摸点在自身范围内,若不在,,此 UIView 内没有合适的响应者
    if (![self pointInside:point withEvent:event]) {
        return nil;
    }
    
    // 5. 遍历所有的子 UIView,若存在更合适的 UIView,则返回它
    for (UIView *subview in [self.subviews reverseObjectEnumerator]) {
        CGPoint subPoint = [self convertPoint:point toView:subview];
        UIView *possibleView = [subview hitTest:subPoint withEvent:event];
        if (possibleView) {
            return possibleView;
        }
    }
    
    // 6. 没有更合适的响应者,返回 self
    return self;
}

在查找 hit-test view 的过程中 ,使用了两个最重要的方法:

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;   
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

hitTest:withEvent:

只要事件传递给一个控件, 这个控件就会调用自己的 hitTest:withEvent: 方法,用以寻找并返回最合适的响应者,(此操作并不关心这个控件是否能处理事件,也不关心点是否在这个控件上。事件都会先传递给这个控件再调用这个控件的 hitTest:withEvent:方法。不管点击在哪里,hitTest:withEvent: 返回的 UIView 都是最合适的响应者)

那么利用这个特性,我们就可以拦截事件的处理。 事件传递给谁就会调用谁的 hitTest:withEvent: 方法。如果返回 nil,那么该控件就不是最合适的响应者,最合适的响应者就是该控件的父控件了。 如 扩大UIButton的点击范围、超出父 View 的子 View 的事件响应、UIScrollView 的 page 滑动: iOS事件响应链中Hit-Test View的应用

如果想让 A 成为最合适的响应者就重写 A 的父控件 B 的hitTest:withEvent: 方法,或者自己在 hitTest:withEvent: 返回 self,建议采取第一种方案。

特殊情况
  • 谁都不能处理事件,窗口也不能处理:

    重写 windowhitTest:withEvent:, 返回 nil

  • 只能由窗口处理事件:

    控制器的 view 的hitTest:withEvent: 返回 nil, 或 window 的 hitTest:withEvent: 返回 self

  • 返回 nil 的含义:

    调用当前控件的 hitTest:withEvent: 返回 nil 的意思是这个控件不是最合适的响应者,且该控件的子控件也不是最合适的响应者。若相同层级的兄弟控件也不是最合适的响应者,那么最合适的响应者就是父控件。

pointInside:withEvent:

pointInside:withEvent: 方法用来判断点在不在当前控件上(调用 pointInside:withEvent: 方法的控件的坐标系),如果返回 YES,代表点在方法调用者的坐标系中。

返回 NO 就代表点不在方法调用者的坐标系中了,也就是方法调用者无法处理该事件,我们可以重写该方法来主动拦截事件的传递:

/**
  * 判断传入的点是否在当前控件的 坐标系内
  */
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
  return NO;
}

响应者对象

响应者对象是既能相应同时也能处理事件的对象。UIResponder 是所有响应者的父类,包括 UIApplicationUIViewUIViewController 都是 UIResponder 的子类。

第一响应者

第一响应者就是第一个接收事件的对象。我们在使用 Xcode 中的 storyboard 画视图的时候,可以看到视图结构中存在 First Responder:

这里的 First Responder 就是 UIApplication了。此外,我们也可以通过实现 canBecomeFirstResponder 方法并返回 YES 来使控制一个控件成为 First Responder,同时调用 becomFirstResponder 方法也可以。例如,UITextField 调用该方法时会弹出键盘并输入,此时 UITextField 就是 First Responder。

事件传递机制

两种情况

  1. 接收事件的 initial view 如果不能处理该事件且它不是最顶层的 view,则事件会往它的父 view 进行传递。 initial view 的父 view 获取事件后仍不能处理,则继续往上传递。循环这个过程,如果顶层的 view 还不能处理,则事件将传递给顶层 view 的 ViewController ,如果 ViewController 也不能处理,则传递给 UIWindow。如果此时 Window 也不能处理该事件,则传递给 UIApplication。最后如果连 UIApplication 都不能处理该事件,则该事件被丢弃。
  2. 与情况1相同,但是 ViewController 有层级关系。那么当子 ViewController 不能处理事件,事件继续往上传递,知道 Root ViewController,后边流程就与情况1相同了。

事件传递的完整流程

  1. 先将事件由上向下传递(父控件传递给子控件),找到最合适的响应者;
  2. 调用最合适响应者的 touches 方法;
  3. 如果调用了 super.touches ,则事件会顺着响应者链往上传递给上一个响应者;
  4. 接着调用上一个响应者的 touches 方法;

判断上一个响应者

  1. 如果这个 view 是控制器的 view,则上一个响应者为控制器;
  2. 如果这个 view 不是控制器的 view,则上一个响应者是父控件。

1007.3 手势识别

事件响应 中其实大概也包括手势识别了。不过这里在粘贴一下:

_UIApplicationHandledEventQueue() 识别到一个手势(UIGestureRecognizer)时,首先调用其 Cancel 将当前手势的 touchesBegin / touchesMoved / touchesEnded 打断。随后系统将对应的手势标记为待处理。

接下来,苹果注册的其中一个 Observer 监视 kCFRunLoopBeforeWaiting(loop 即将进入休眠)事件,这个 Observer 的回调 _UIGestureRecognizerUpdateObserver() 内部获取所有被标记为待处理的手势,并执行手势的回调。

当手势发生改变时,这个回调也会做出相应处理。

1007.4 UI 刷新

继续搬运,大佬的代码 当我们操作 UI (比如更改 farme,添加 subview ,或者手动调用了 UIView / CALayer 的 setNeedsLayout() / setNeedsDisplay())时,这个 UI 就会被标记为待处理,并被提交到一个全局容器里。

苹果注册了一个 Observer 监视 kCFRunLoopBeforeWaiting(loop 即将进入休眠)事件和 kCFRunLoopExit (即将推出 loop),其回调会执行一个函数: _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv() 。这个函数会遍历所有待处理的 UIView / CALayer 来执行绘制和调整,并刷新 UI。

1007.5 计时器

NSTimer 其实就是 CFRunloopTimerRef。他俩之间是 tool-free-bridged 的。一个 NSTimer 被添加到 Runloop 中时,Runloop 会为其重复的时间点注册号事件。Runloop 为了节省资源,并不会在非常精确的时间点回调这个 NSTimer。NSTimer 有一个属性 - 宽容度( @property NSTimeInterval tolerance;),允许了最大误差。

如果某个时间点错过了(错过,已经超过了宽容度),比如执行了一个很长的任务,那么这个时间点就会被跳过去,不会延迟执行(错过,不是迟到)。

这里有个特例,CADisplayLink。他是一个与屏幕刷新率保持一致的计时器,但他更复杂,因为每一帧都不能跳过。

1007.6 PerformSelector

PerformSelector 其实分三类:

performSelector:withObject:afterDelay:inModes:

延迟执行某个方法。其实是给 Runloop 添加了一个 CFRunloopTimerRef。

performSelector:onThread:withObject:waitUntilDone:modes:

利用 mach port 给指定线程发送了一个 Source0 消息。

performSelector:withObject:withObject:

跟直接调用没啥区别

1007.7 GCD

dispatch_async(dispatch_get_main_queue(), block) 。当我们使用 GCD 让主线程执行 block 时,libDispatch 会向主线程的 Runloop 发送消息。Runloop 被唤醒,从消息中拿到 block,然后在 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 中执行这个 block。

BUT,这仅限于 dispatch 到主线程,如果是 dispatch 到子线程,那么还是直接由 libDispatch 处理的。

1007.8 网络请求

搬运停不下来...大佬的代码

在 iOS 中,网络请求的接口自下而上有以下几层:

  • CFSocket

    最底层的接口,负责 socket 通信。

  • CFNetwork

    基于 CFSocket 的封装。ASIHttpRequest 工作在这一层

  • NSURLConnection

    基于 CFNetwork 的封装,提供面向对象的接口。AFNetworking 工作在这一层

  • NSURLSession

    iOS 7 新增,看似与 NSURLConnection 并列。然后其底层用到了 NSURLConnection 的部分功能(如 com.apple.NSURLConnectionLoader 线程)。AFNetworking2 与 Alamofire 工作在在这一层。

通常初始化 NSURLConnection 时,我们会传入一个 delegate。当调用了 [NSURLConnection start] 之后,这个 delegate 会多次收到事件回调。实际上,start 这个函数内部会获取 currentRunloop,然后在其中的 DefaultMode 中添加 4 个 Source0(需要手动调用的 Source)。CFMultiplexerSource 负责回调 delegate,CFHTTPCookieStorage 负责处理 Cookie。

当网络开始传输时,可以看到 NSURLConnection 创建了两个新的线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。跟我们上边看的相同,CFSocket 线程处理底层 socket 连接, NSURLConnectionLoader 线程内部会使用 Runloop 来接收底层 socket 的 Source1 事件,然后通过之前添加的 4 个 Source0 来通知上层我们船进入的 delegate。

顺下来。NSURLConnectionLoader 线程的 Runloop 基于 mach port 接收来自底层 CFSocket 的 Source1 事件。接收到事件后,其在恰当的时机向 CFMultiplexerSource 等 Source0 发送消息,同时唤醒 delegate 线程的 Runloop 来处理这些消息。CFMultiplexerSource 会在 delegate 线程的 Runloop 对 delegate 执行真正的回调操作。

分类:
iOS
标签:
分类:
iOS
标签:
收藏成功!
已添加到「」, 点击更改