iOS底层原理 - 探寻RunLoop本质(一)

面试题引发的思考:

Q: RunLoop内部实现逻辑?

RunLoop运行逻辑

Q: RunLoop和线程的关系?

  • 每条线程都有唯一的一个与之对应的RunLoop对象;
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 主线程的RunLoop自动创建,子线程的RunLoop需要手动创建;
  • RunLoop在第一次获取线程时创建,在线程结束时销毁。

Q: timer与RunLoop的关系?

  • timer运行在RunLoop里,相当于RunLoop来控制timer何时执行。

Q: RunLoop 是怎么响应用户操作的, 具体流程是什么样的?

  • 先由Source1捕捉触摸事件,然后由Source0去处理触摸事件。

Q: RunLoop有哪几种状态?

RunLoop状态

Q: RunLoop的mode作用是什么?

  • 把不同模式下的Source0/Source1/Timer/Observer隔离开来,互不影响,会使程序在当前模式下流畅运行。

1. RunLoop简介

(1) RunLoop概念

RunLoop顾名思义,运行循环,在程序运行过程中循环做一些事情:

  • 如果没有RunLoop程序执行完毕就会立即退出;
  • 如果有RunLoop程序并不会马上退出,而是保持运行状态,等待处理程序的各种事件;
  • RunLoop可以保持程序的持续运行,在没有事件处理的时候使程序进入休眠模式,从而节省CPU资源,提高程序性能。
没有RunLoop

代码如上:没有RunLoop,执行完第14行代码后,会立即退出程序。

有RunLoop

代码如上:有RunLoop,程序并不会马上退出,而是保持运行状态。

其伪代码如下:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        int result = 0;
        do {
            // 睡眠中等待消息
            int message = sleep_and_wait();
            // 处理消息
            result = process_message(message);
        } while (result == 0);
        return 0;
    }
}

RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。

(2) RunLoop应用

RunLoop应用范畴:

  • 定时器(Timer)、PerformSelector
  • GCD Async Main Queue
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • AutoreleasePool

2. RunLoop相关概念

(1) RunLoop对象

  • iOS中有2套API来访问和使用RunLoop
  • FoundationNSRunLoop
  • Core FoundationCFRunLoopRef
  • NSRunLoopCFRunLoopRef都代表着RunLoop对象
  • NSRunLoop是基于CFRunLoopRef的一层OC包装
  • CFRunLoopRef 源码是开源的
- (void)viewDidLoad {
    [super viewDidLoad];

    // Foundation
    [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

    // Core Foundation
    CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
    CFRunLoopGetMain(); // 获得主线程的RunLoop对象
}

(2) RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象;
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 主线程的RunLoop自动创建,子线程的Runloop需要手动创建;
  • RunLoop在第一次获取线程时创建,在线程结束时销毁。

以上结论都可以由以下相关源码总结得出:

CFRunLoopGetCurrent函数

CFRunLoopGetCurrent调用_CFRunLoopGet0

_CFRunLoopGet0函数

(3) RunLoop相关的类

Core Foundation中关于RunLoop的5个类:

  • CFRunLoopRef:获得当前RunLoop和主RunLoop
  • CFRunLoopModeRef:运行模式
  • CFRunLoopSourceRef:事件源,输入源
  • CFRunLoopTimerRef:定时器时间
  • CFRunLoopObserverRef:观察者

由相关源码可知:

CFRunLoop结构体
CFRunLoopMode结构体

由以上代码可以得出RunLoop五个类直接的对应关系:

对应关系
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,作为_currentMode
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入;
    不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响;
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。

  • Source0 触摸事件处理、performSelector:onThread:
  • Source1 基于Port的线程间通信、系统事件捕捉
  • Timers NSTimerperformSelector:withObject:afterDelay:
  • Observers 用于监听RunLoop的状态、UI刷新(BeforeWaiting)、Autorelease poolBeforeWaiting
1> CFRunLoopModeRef

常见的2种Mode

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):
    App的默认Mode,通常主线程是在这个Mode下运行;
  • UITrackingRunLoopMode
    界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
2> CFRunLoopObserverRef

RunLoop的Observer状态类型如下:

RunLoop状态

监听RunLoop状态:

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 创建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopExit: {
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }
            default:
                break;
        }
    });
    // 添加observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}

ViewController放一个UITextView控件,运行程序之后,滑动控件,查看打印结果:

打印结果

由打印结果可知:

  • 控件开始滚动时,先退出kCFRunLoopDefaultMode,然后进入UITrackingRunLoopMode
  • 控件停止滚动时,先退出UITrackingRunLoopMode,然后进入kCFRunLoopDefaultMode

如此便产生了模式的切换。


3. RunLoop运行逻辑

(1) 官方文档解读

由苹果官方文档可知RunLoop的运行逻辑如下:

RunLoop运行逻辑

由以上图例可知:
RunLoop在运行循环过程中,接收到Input sources或者Timer sources时会通过对应处理方式处理;没有事件消息传入的时候,就会使程序处于休眠状态。

(2) 源码解读

下面通过源码解读RunLoop底层实现。

RunLoop入口函数

首先通过运行程序断点可以知道RunLoop入口函数为CFRunLoopRunSpecific函数

对相关源码进行简化,以便解读RunLoop底层实现:

RunLoop底层实现

RunLoop底层实现代码流程图如下:

RunLoop运行逻辑

(3) 特殊要点 GCD相关

一般GCD是有自己的处理逻辑,不依赖RunLoop实现;
GCD有一种特殊情况,需要交给RunLoop进行处理:

GCD特殊情况依赖RunLoop

(4) RunLoop休眠的实现原理

由RunLoop底层实现可知:

RunLoop休眠

RunLoop是通过__CFRunLoopServiceMachPort函数来休眠的:

__CFRunLoopServiceMachPort函数

__CFRunLoopServiceMachPort函数主要方法为mach_msg函数,mach_msg是内核层面的API,这是程序真正达到休眠的方式。

那么RunLoop休眠的实现原理如下:

RunLoop休眠的实现原理

RunLoop在用户态调用mach_msg()时,会自动转到内核态,调用内核态的mach_msg(),达到真正休眠的目的:
没有消息就让线程休眠;有消息就唤醒线程,回到用户态处理消息。

你可能感兴趣的:(iOS底层原理 - 探寻RunLoop本质(一))