iOS 底层 runloop内部的数据结构 - AlvinSunny/OC-TheUnderlying GitHub Wiki

RunLoop 相关的C语言类: CFRunLoopRef CFRunLoopModeRef --> 运行模式 CFRunLoopSourceRef -- >事件处理: 点击 、PerformSelector(触摸事件) CFRunLoopTimeRef --> 定时器 CFRunLoopObserverRef --> 监听器

typedef struct   __CFRunLoop  *CFRunLoopRef;
struct   __CFRunLoop  {
            pthread_t _pthread;
            CFMutableSetRef   _commonModes;  //通用Mode集合
            CFMutableSetRef   _commonModeItems; //存放着在通用模式下工作的timer或其他
            CFRunLoopModeRef   _currentMode;  //当前运行模式
            CFMutableSetRef        _modes;  //内部存储着若干个CFRunLoopModeRef
}

CFRunLoopMode

RunLoop在同一时段内只能并且必须在一种特定的Moderun,这个Mode被称为currentMode 如果想更换currentMode,需要暂停当前的loop,然后重启新的loop,这样可以分离开不同的Source/Timer/Observer,互不影响 可以定义自己的mode,但是几乎没有这么做的;

关于CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式

  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer

  • RunLoop启动时只能选择其中一个Mode作为currentMode

  • 如果需要切换Mode,只能退出当前的loop(但并不会退出程序,不要混淆),再重新选择一个Mode进入; 不同组的Source0/Source1/Timer/Observer能分割开来,互不影响

  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立刻退出

  • 常见的几种Mode:

    • kCFRunLoopDefaultMode == NSDefaultRunLoopMode : App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode: 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响 - kCFRunLoopCommonModes :默认包括kCFRunLoopDefaultMode、UITrackingRunLoopMode kCFRunLoopCommonModes并不是一个真的模式,它只是一个标记,如:被标记的 Timer 可以在kCFRunLoopDefaultMode模式和UITrackingRunLoopMode模式下运行。

基本用不到的Mode:

  • UIInitializationRunLoopMode :私有的mode,App启动的时候的状态,加载出第一个页面后,就转成了Default
  • GSEventReceiveRunLoopMode接受系统事件的内部 Mode,通常用不到

CFRunLoopMode中各成员的作用


typedef struct   __CFRunLoopMode  *CFRunLoopModeRef;
struct   __CFRunLoopMode {
    CFStringRef   _name;            //模式名
    CFMutableSetref   _sources0;    //CFRunLoopSourceRef,事件处理: 点击 、PerformSelector(触摸事件)
    CFMutableSetref   _sources1;    //基于Prot(端口)的线程间通信,系统事件捕捉
    CFMutableArrayRef   _observers; //CFRunLoopObserverRef   监听器
    CFMutableArrayRef   _timers;    //CFRunLoopTimeRef  定时器
}

CFRunLoopSource

CFRunLoopSource是RunLoop的数据源抽象类,类似Objective-C中的协议protocol,实现了这个protocol就可以充当RunLoop的数据源(几乎没有这么做的),RunLoop自己定义了两个Source:Source0Source1

  • Source0: 处理App内部事件; 比如: - 屏幕响应UIEvent, CFSocket(套接字),我们点击屏幕(touchesBegan:withEvent:)就是Source0事件 - 开发者主动实现的线程间通信:performSelector:onThread:
aSelector: 方法名
onThread: 需要访问的线程
withObject: 实现SEL方法时传递的参数
waitUntilDone:   
- 设置为YES: 表示需要等当前方法选择器中的方法执行完再执行下面的代码,防止还没停止当前实例对象就先释放掉了。
- 设置为NO:表示不需要等当前方法选择器中的方法执行完,可以继续执行下面的代码
modes: 一个字符串数组,它标识允许在其中执行指定选择器的模式。此数组必须包含至少一个字符串。如果为该参数指定nil或空数组,则此方法将在不执行指定的选择器的情况下返回。

//给某个线程发送消息
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array

//给主线程发消息
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array
  • Source1: 由内核管理,比如: 基于mach_port端口的线程或进程间通信、系统事件捕捉 事件是通过Source1来捕捉,然后再分发到Source0来处理的

注意:mach_port是iOS系统中进程间通信的一种方式,如果进程1往一个port中发送一个消息,此时进程2监听了这个port,就会拿到这个消息 NSPort是对CoreFoundation中的CFMachPortCFMessagePort的封装 来看一下对source事件的定义

RunLoop中对于source事件的定义.png

CFRunLoopSource中用union确保这个source要么是source0,要么是source1 看一下source0和source1的具体定义:

source0的定义.png

source1的定义.png

souce0中定义的都是函数指针 source1中除了函数指针,还有一个mach_port

Timers

NSTimer performSelector:withObject:afterDelay:

  • 该方法是拿到RunLoop并且把Timer添加到RunLoop里面,但不会启动RunLoop;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

CFRunLoopObserver

CFRunLoopObserver相当于观察者模式的观察者,用来向观察者报告RunLoop当前的状态

  • 用于监听RunLoop的状态
  • UI刷新(BeforeWaiting
  • Autorelease pool(BeforeWaiting)自动释放池 AutoreleasePool在RunLoop的两次sleep之间对AutoreleasePool进行pop和push,将这次loop中产生的Autorelease对象进行释放

下面是RunLoop中定义的状态:

/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU         // 所有状态
};

看以下代码

self.view.backgroundColor = [UIColor redColor];

view的背景颜色什么时候被修改为红色 ?

执行到到这行代码时,会先把代码保存下来;Observers(监听器)监听到将要休眠之前会刷新UI,在这时候改变了背景颜色为红色。

RunLoop、Mode、[Source、Timer、Observer]之间的关系@2x.png

疑问:以下输出结果相同吗?

    NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
    NSLog(@"%p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());

肯定不相同,因为NSRunLoop是对CFRunLoop的封装,会把CFRunLoopGetCurrent()的地址封装在NSRunLoop的地址中;可以通过输出对象NSLog(@"%@", [NSRunLoop mainRunLoop]);看出

    NSLog(@"NSRunLoop地址 %p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
    NSLog(@"CFRunLoop地址  %p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
    NSLog(@"NSRunLoop 对象 %@", [NSRunLoop mainRunLoop]);

输出结果:

NSRunLoop地址 0x6000018e1fe0 0x6000018e1fe0 CFRunLoop地址 0x6000000e4200 0x6000000e4200 NSRunLoop 对象 <CFRunLoop 0x6000000e4200 [0x110d44ae8]> 很明显可以看出:NSRunLoop的结构中存储着0x6000000e4200,0x6000000e4200就是CFRunLoop的地址;

  • 0x6000000e4200是真正的runloop
  • 0x6000018e1fe0代表的是OC对象NSRunLoop的地址

RunLoop监听状态监听逻辑

// 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//监听到状态的block回调
        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);

⚠️ **GitHub.com Fallback** ⚠️