再谈runloop

219 阅读7分钟

1、runloop对象是怎么存储的

  • runloop对象和线程是一一对应的关系
  • runloop对象是储存在一个全局hashmap表中的,这个全局字段的key是线程对象,valuerunloop对象

2、runloop怎么跑起来的,又是怎么退出的

首先runloop有六个状态变化

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

所以,当启动runloop的时候,就是监听输入源(端口port、source0、source1)、定时器、如果有事件,处理事件,没有就休眠。

void CFRunLoopRun(void) {
/* DOES CALLOUT */
 int32_t result;
 do {
      result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode,                                                  1.0e10, false);
          CHECK_FOR_FORK();
      } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

使用run方法启动runloop的情况,一直在重复的进入runloop

CFRunLoopRunSpecific中做了一些前置判断,比如判断当前Mode为空,直接return,这个也可以说明一点***启动runloop之前,runloop中一定要有输入源或者定时器***



退出runloop有四个条件

  • 入参stopAfterHandle为YES的时候,那么处理完source就会退出runloop
  • 自身超时时间到了
  • 被外部调用CFRunloop停止
  • _CFRunLoopStopMode 停止

CFRunLoopRun指定stopAfterHandleNO,说明使用run方法开启runloop,处理完source后不会退出runloop

如果是使用CFRunLoopRunInMode则可以指定是否需要处理完source后就退出runloop



do-while的过程中,做了以下操作

  • 监听source(source1是基于port的线程通信(触摸/锁屏/摇晃等),source0是不基于port的,是App事件 包括:UIEvent、performSelector),监听到就处理
  • 监听timer的事件,监听到就处理
  • 没有source和timer的时候,就休眠,休眠不是不监听,还是保持监听的,只是当有事件的时候,才唤醒,继续处理


当我们触发了事件(触摸/锁屏/摇晃等)后,由IOKit.framework生成一个 IOHIDEvent事件,而IOKit是苹果的硬件驱动框架,由它进行底层接口的抽象封装与系统进行交互传递硬件感应的事件,并专门处理用户交互设备,由IOHIDServices和IOHIDDisplays两部分组成,其中IOHIDServices是专门处理用户交互的,它会将事件封装成IOHIDEvents对象,接着用mach port转发给需要的App进程,随后 Source1就会接收IOHIDEvent,之后再回调__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()内触发Source0
Source0 再触发 _UIApplicationHandleEventQueue()。
所以触摸事件看到是在 Source0 内的。

总结:触摸事件先通过 mach port 发送,封装为 source1,之后又转换为 source0



1.一个runloop对应一个线程,多个mode
        一个mode下对应多个sourceobservertimer


struct __CFRunLoop {
    pthread_t _pthread;                // 线程对象
    CFMutableSetRef _commonModes;       //    mode
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
    // 简化
};
  • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
  • kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

除了以上5个mode,还有其他mode,但是很少遇见这里


4.子线程不自动开启runloop,手动开启runloop前,必须得有输入源和定时器(输入源就是通过监听端口,可以获取不同的事件),通过CFRunloop源码中的CFRunLoopRunSpecific函数,其中判断了当modenull或者modeItem为空,直接return




如果将开启runloop的代码,写到perform前,那么会开启不成功,因为开启runloop需要有输入源或者定时器的情况才可以开启


实现了一个常驻线程 原理

原理就是往当前线程的runloop中添加一个端口,让其监听这个端口(理解为监听某个端口的输入源,比如系统内核端口,监听一些系统事件),因为可以一直监听这个端口,那么runloop就不会退出

其实就是保持runloop不退出,就达到常驻线程的效果了,那么要让runloop不退出,就得有输入源或者重复的定时器让其监听


当开启一个线程,就会对应创建一个runloop对象吗?

不是的,调用获取当前runloop的方法,内部实现:如果当前runloop不存在就创建一个,存在就返回当前runloop

所以走这句代码self.myRunloop = [NSRunLoop currentRunLoop];就生成当前线程对应的runloop


怎么销毁常驻线程

1.要销毁常驻线程,首先得先把runloop退出?

当没有输入源或者定时器可以监听的时候,退出runloop

- (void)test2
{
    [self.myThread setName:@"TestThread"];
    self.myRunloop = [NSRunLoop currentRunLoop];
    self.myPort = [NSMachPort port];
    
    // 添加监听NSMachPort的端口(这个端口可以理解为输入源,因为可以一直监听这个,所以这时候的runloop不会退出,会一直在做do-while)
    [self.myRunloop addPort:self.myPort forMode:NSDefaultRunLoopMode];
    
    [self.myRunloop run];
    
    // [self.myRunloop run]; 会导致以下代码没法走,
//因为runloop就是一个do-while的循环,do-while监听源,处理源
    [self.testPtr release];
}
[Runloop run];后面就不要有 代码了


如何退出

如果常驻线程是通过监听端口实现的,那么就调用[self.myRunloop removePort:self.myPort forMode:NSDefaultRunLoopMode];,移除端口,就可以销毁了。
其实这时候还不一定能成功销毁,因为可能系统加入了一些其他源的监听

如果NSTimerrepeatsNO,那么执行一次timer的事件后,就会退出runloop

以上,如果通过移除端口,结束timer,反正以移除已知的输入源或者定时器来退出runloop都是不太靠谱的,因为系统内部有可能会在当前线程的runloop中添加一些输入源,也就是还有未知的输入源,我们没有移除。


使用CFRunLoopStop退出Runloop

确实是退出了runloop,但是又马上进入了

所以刚才stop之后,确实是退出runloop了,但是因为我们是用run启动的,所以会重复的调用runMode:beforeDate:又启动了

三种Run方法

// 不会退出runloop
- (void)run; 

// 超时时候到退出runloop
- (void)runUntilDate:(NSDate *)limitDate; 

// 处理完source会退出或者时间到也会退出
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

// 上三个方法分别对应CFRunloop


runMode:beforeDate:启动runloop,再用CFRunLoopStop退出runloop试试

将上一段代码[self.myRunloop run];
替换成[self.myRunloop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

成功退出runloop并且线程run后的代码也走了,这时候通过打个暂停断点,看堆栈,发现我们的线程不在了,说明已经被销毁了(runloop退出后,线程没有任务,自然就销毁了)


虽然 可以成功退出runloop,但是还是有问题,当runloop处理完source后,就退出runloop了,而且这时候,也不会像调用run方法那样,重新进入runloop

所以这种方式还是不行



最后一个最佳方式,既能手动退出runloop, 又不会处理完source就退出runloop,不再进来

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning) {
	// runMode是有返回值的,当启动runloop后,是不会返回的,
         //所以不会一直在调这个方法,runloop退出了,才会再调
	[theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]
       // 有返回值 但是一直不返回的方法
}

当想退出runloop的时候,将shouldKeepRunning置为NO就可以了




runloop和autoreleasepool

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

第一个 Observer 监视的事件是
 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件:
 BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;

Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。