一、概念
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
问题1:什么是事件循环
解释:
-
没有消息需要处理时,休眠以避免资源占用。
-
有消息处理时,立刻被唤醒。
备注:
内核态:在一个进程中,如果有系统调用,此时进程处于内核态;系统操作包括:执行文件操作,网络数据发送等操作,此时特权级别比较高,0级。
用户态:当一个进程执行用户自己的代码时,处于用户态,此时特权级别比较低,3级。
小结:
所有用户程序都是运行在用户态的, 但是有时候程序确实需要做一些内核态的事情, 例如执行文件操作,网络数据发送等操作。
而唯一可以做这些事情的就是操作系统, 所以此时程序就需要先操作系统请求以程序的名义来执行这些操作。
以下是操作系统内存空间分布图:
问题2:为什么main函数能够保证不退出
解释:
1、在main函数当中,调用了UIApplicationMain函数。
2、UIApplicationMain函数会启动主线程的runloop。
3、runloop是一个通过事件循环处理事件/消息的对象,可以做到“有事做的时候去做事,没事做的时候从用户态切换到内核态,避免资源的占用,当前线程是处于一个休眠状态”。
//main函数代码
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
二、runloop数据结构相关
2.1、runloop相关框架
NSRunLoop是CFRunLoop的封装,提供了面向对象的API。
2.2、runloop主要结构
runloop相关的数据结构:
- CFRunLoop
- CFRunLoopMode
- Source/Timer/Observer
2.2.1、CFRunLoop
主要包含5个部分
pthread
和线程一一对应(RunLoop和线程关系)currentMode
CFRunLoopMode类型-
modes
NSMutableSet<CFRunLoopMode*>
mode有多个类型- NSDefaultRunLoopMode
默认mode模型、主线程就在这个模型下 - UITrackingRunLoopMode
界面跟踪mode、滑动列表界面 - NSRunLoopCommonModes
CommonMode不是实际存在的一种Mode。
是同步Source/Timer/Observer到多个Model中的一种技术方案。 - UIInitializationRunLoopMode
初始化mode(没用过) - GSEventReceiveRunLoopMode
系统内部mode(没用过)
- NSDefaultRunLoopMode
commonModes
NSMutableSet<NSString*>
被打上“common”标记的mode的名称集合 == NSDefaultRunLoopMode + UITrackingRunLoopModecmomonModelItems
Source/Timer/Observer
如果当前模式是commonModes,则会处理cmomonModelItems里面的 Source/Timer/Observer事件。
问题3:RunLoop和线程之间的关系
解释:
1、线程和RunLoop之间的关系是一一对应的。
2、主线程的RunLoop是默认开启的,子线程的RunLoop是默认不开启的。
3、子线程的RunLoop在你主动获取的情况下,才会创建。
子线程的RunLoop在线程结束时,才会销毁
主线程除外。
问题4:commonModes的作用
解释:
- CommonMode不是实际存在的一种Mode。
- 是同步Source/Timer/Observer到多个Model中的一种技术方案。
2.2.2、CFRunLoopMode
主要包含以下部分:
-
name
名称、默认是NSDefaultRunLoopMode。 -
source0
NSMutableSet -
source1
NSMutableSet -
observers
NSMutableArray -
timers
NSMutableArray
2.2.3、CFRunLoopSource
-
source0
需要手动唤醒线程 -
source1
具备唤醒线程的能力
问题5:source0和source1有什么样的区别
解释
source0需要手动唤醒线程。
source1具备唤醒线程的能力。
2.2.3、CFRunLoopTimer
基于事件的定时器
和NSTimer是toll-free bridged (免费桥转换)。
2.2.4、CFRunLoopObserver
观测时间点(共6中状态)
- kCFRunLoopEntry
即将进入Loop - kCFRunLoopBeforeTimers
即将处理 Timer - kCFRunLoopBeforeSources
即将处理 Source - kCFRunLoopBeforeWaiting
即将进入休眠 - kCFRunLoopAfterWaiting
刚从休眠中唤醒 - kCFRunLoopExit
即将退出Loop
2.2.5、各个数据结构之间的关系
问题6 RunLoop、Model、Source/Timer/Observer关系
解释
RunLoop和Model是一对多的关系(从CFRunLoop的数据结构modes)。
Model和Source/Timer/Observer是一对多的关系。
问题7 RunLoop为什么有多个Model
解释
这样设计的原因是为了起到事件屏蔽的效果。
当RunLoop运行在Mode1上时,只能接收处理Mode1上的source1、observers、timers事件回调,不能接收其它Mode上的source/observer/timer事件回调;起到了事件屏蔽的效果。
2.2.6、CommonMode的特殊性
在ios上对应的是NSRunLoopCommonModes。
- CommonMode不是实际存在的一种Mode。
- 是同步Source/Timer/Observer到多个Model中的一种技术方案。
三、事件循环机制(内部逻辑)
3.1、整体流程
1、在RunLoop启动后,会发送一个通知,告知Observer观察者。
2、将要处理Timer/Source0事件,会发送一个通知,告知Observer观察者。
3、处理Source0事件。
4、如果有Source1要处理,会跳过当前流程,到第8步。
5、没有Source1要处理,线程将要休眠,发送通知。
6、休眠,等待唤醒。
7、线程刚被唤醒。
8、处理唤醒时收到的消息。
问题8 当一个处于休眠状态RunLoop通过哪些事件唤醒它
解释
- Source1回调
- Timer事件
- 外部手动唤醒
小结:
点击App图标,从程序启动、运行、退出这个过程讲解,系统都发生了什么?
解释
1、程序启动后,调用main函数后,会调用UIApplicationmain,这UIApplicationmain函数内部会启动主线程的RunLoop。经过一系列处理,最终主线程RunLoop处于休眠状态。
2、此时,点击一个屏幕,会产生一个mach_port,基于mach_port最终会转换成一个Source1,然后可以唤醒主线程,运行处理事件。
3、当把程序杀死时候,就会发生退出RunLoop,发送通知即将退出RunLoop,RunLoop退出后,线程也就销毁掉了。
3.2、RunLoop核心
1、在main函数中,经过一系列处理,会调用系统函数mach_msg(),就发生了系统调用,这样会从用户态到核心态
2、在核心态下面,在一定条件下(Source1/Timer/外部手动唤醒),mach_msg(),会返回给调用方,也就是程序从核心态到用户态。
四、RunLoop与NSTimer
问题9 滑动TableView的时候我们的定时器还会生效吗?
不会生效
原因:
1、当我们滑动scrollView时,主线程的RunLoop 会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的Source/Timer/Observer)。
2、而timer 是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer。
解决问题:
我们只需要在添加timer 时,将mode 设置为NSRunLoopCommonModes即可。
五、RunLoop与多线程
线程和RunLoop是一一对应的
自己创建的线程默认没有RunLoop的
问题10 怎么实现一个常驻线程
- 为当前线程开启一个RunLoop
- 向RunLoop中添加一个Port/Source等维持RunLoop的事件循环
- 启动该RunLoop
//创建线程
HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];
[subThread setName:@"HLThread"];
[subThread start];
//开启RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//如果注释了下面这一行,子线程中的任务并不能正常执行
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
六、面试问题总结
问题11、什么是RunLoop,它是怎样做到有事做事,没事休息的?
1、RunLoop是一个通过内部循环对事件/消息进行管理的一个对象。
2、程序运行会调用main函数,在main函数里面调用UIApplicationMain,UIApplicationMain函数会启动主线程的runloop。
3、runloop运行后,会调用系统方法mach_msg(),会使得程序从用户态变成核心态,此时线程处于休眠状态。
4、当有外界条件变化(Source/Timer/Observer),mach_msg会使得程序从核心态变成用户态,此时线程处于活跃状态。
问题12、RunLoop与线程是怎么样的关系
1、RunLoop与线程是一一对应的关系。
2、一个线程默认是没有runloop的(主线程除外)。
问题13、如何实现一个常驻线程
1、为当前线程开启一个RunLoop
2、向RunLoop中添加一个Port/Source等维持RunLoop的事件循环
3、启动该RunLoop
问题14、怎样保证子线程数据回来更新UI的时候,不打断用户的滑动操作?
1、把子线程抛给主线程进行UI更新的逻辑,可以包装起来,提交到主线程的NSDefaultRunLoopMode模式下面。
2、因为用户滑动操作是在UITrackingRunLoopMode模式下进行的。
//参考代码事件
[self.tableView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];