1、runloop对象是怎么存储的
runloop对象和线程是一一对应的关系runloop对象是储存在一个全局hashmap表中的,这个全局字段的key是线程对象,value是runloop对象
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)、定时器、如果有事件,处理事件,没有就休眠。
/* 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指定stopAfterHandle为NO,说明使用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 内的。
1.一个runloop对应一个线程,多个mode,
一个mode下对应多个source、observer、timer
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函数,其中判断了当mode为null或者modeItem为空,直接return
如果将开启runloop的代码,写到perform前,那么会开启不成功,因为开启runloop需要有输入源或者定时器的情况才可以开启
实现了一个常驻线程 原理
原理就是往当前线程的runloop中添加一个端口,让其监听这个端口(理解为监听某个端口的输入源,比如系统内核端口,监听一些系统事件),因为可以一直监听这个端口,那么runloop就不会退出
其实就是保持runloop不退出,就达到常驻线程的效果了,那么要让runloop不退出,就得有输入源或者重复的定时器让其监听
当开启一个线程,就会对应创建一个runloop对象吗?
不是的,调用获取当前runloop的方法,内部实现:如果当前runloop不存在就创建一个,存在就返回当前runloop
所以走这句代码self.myRunloop = [NSRunLoop currentRunLoop];就生成当前线程对应的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];,移除端口,就可以销毁了。
(其实这时候还不一定能成功销毁,因为可能系统加入了一些其他源的监听)
如果NSTimer的repeats是NO,那么执行一次timer的事件后,就会退出runloop
以上,如果通过移除端口,结束timer,反正以移除已知的输入源或者定时器来退出runloop都是不太靠谱的,因为系统内部有可能会在当前线程的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,优先级最低,保证其释放池子发生在其他所有回调之后。