什么是runloop
- 运行循环
- 内部其实就是do-while循环,在这个循环内部不断地处理各种任务(比如Source、Timer、Observer)
高级回答: Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象
这里有两个重点
-
事件循环
-
事件/消息进行管理
什么是事件循环呢?
事件循环(状态切换)
没有消息需要处理时,休眠以避免资源占用
用户态——>内核态
有消息需要处理时,立刻被唤醒
用户态<—— 内核态
什么是事件/消息进行管理呢?
RunLoop 通过 mach_msg()函数接收、发送消息来进行管理。
它的本质是调用函数 mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。
可以做到在有事做的时候做事,没事做的时候,会由用户态切换到内核态,避免资源浪费。
如何实现事件、消息的管理
mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap), 即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。
当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;
内核态中内核实现的 mach_msg() 函数会完成实际的工作,
所以说 Runloop的核心就是一个 mach_msg(),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。
RunLoop 的基本作用
- 保持程序的持续运行:
如果没有RunLoop,main()函数一执行完,程序就会立刻退出。
而我们的 iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop; - 处理 App 中的的各种事件(比如触摸事件、定时器事件等);
- 节省 CPU 资源,提高程序性能:该做事时做事,该休息时休息。
runloop的底层数据结构
RunLoop 的本质是什么?
答:本质是一个OC对象,内部也有isa指针。
RunLoop 对象
-
iOS 中有 2 套 API 来访问和使用
RunLoop:
① Foundation:NSRunLoop(是CFRunLoopRef的封装,提供了面向对象的 API)
② Core Foundation:CFRunLoopRef -
NSRunLoop和CFRunLoopRef都代表着RunLoop对象 -
NSRunLoop不开源,而CFRunLoopRef是开源的:Core Foundation 源码
CFRunLoopRef
RunLoop对象的底层就是一个CFRunLoopRef结构体,它里面存储着:
- _pthread:
RunLoop与线程是一一对应关系 - _commonModes:存储着 NSString 对象的集合(Mode 的名称)
- _commonModeItems:存储着被标记为通用模式的
Source0/Source1/Timer/Observer - _currentMode:
RunLoop当前的运行模式 - _modes:存储着
RunLoop所有的 Mode(CFRunLoopModeRef)模式
// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
pthread_t _pthread; // 与线程一一对应
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式;- 一个
RunLoop包含若干个 Mode,每个 Mode 又包含若干个Source0/Source1/Timer/Observer; RunLoop启动时只能选择其中一个 Mode,作为 currentMode;- 如果需要切换 Mode,只能退出当前 Loop,再重新选择一个 Mode 进入,切换模式不会导致程序退出;
- 不同 Mode 中的
Source0/Source1/Timer/Observer能分隔开来,互不影响; - 如果 Mode 里没有任何
Source0/Source1/Timer/Observer,RunLoop会立马退出。
// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
CFStringRef _name; // mode 类型,如:NSDefaultRunLoopMode
CFMutableSetRef _sources0; // CFRunLoopSourceRef
CFMutableSetRef _sources1; // CFRunLoopSourceRef
CFMutableArrayRef _observers; // CFRunLoopObserverRef
CFMutableArrayRef _timers; // CFRunLoopTimerRef
...
};
CFRunLoopSourceRef
- 在
RunLoop中有两个很重要的概念,一个是上面提到的模式,还有一个就是事件源。事件源分为输入源(Input Sources)和定时器源(Timer Sources)两种; 输入源(Input Sources)又分为Source0和Source1两种,以下__CFRunLoopSource中的共用体union中的version0和version1就分别对应Source0和Source1。
Source0 和 Source1 的区别:
| Input Sources | 区别 |
|---|---|
| Source0 | 需要手动唤醒线程:添加Source0到RunLoop并不会主动唤醒线程,需要手动唤醒) ① 触摸事件处理 ② performSelector:onThread: |
| Source1 | 具备唤醒线程的能力 ① 基于 Port 的线程间通信 ② 系统事件捕捉:系统事件捕捉是由Source1来处理,然后再交给Source0处理 |
CFRunLoopObserverRef
CFRunLoopObserverRef用来监听RunLoop的 6 种活动状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 表示以上所有状态
};
runloop 是怎么响应用户操作的, 具体流程是什么样的?
- source1 捕捉用户触摸事件
- source0去处理触摸时间
常见的几种Mode:
-
Default : App的默认Mode,通常主线程是在这个Mode下运行
-
UITracking: 界面跟踪Mode,用于ScrollView`追踪触摸滑动,保证界面滑动时不受其他Mode影响
-
Common :并不是一个真的模式,它只是一个标记,如:被标记的 Timer可以在Default模式和UITracking下运行。
基本用不到的Mode:
-
UIInitialization :私有的mode,App启动的时候的状态,加载出第一个页面后,就转成了Default
-
GSEventReceive系统的内部 Mode,通常用不到
runloop和线程的关系?
-
线程和RunLoop是一一对应的关系.
-
一个线程对应一个RunLoop,主线程的RunLoop默认在底层启动通过UIApplicationMain,子线程的RunLoop必须得创建,还得调用run方法来进行启动
-
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
-
RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建; -
RunLoop销毁时机:RunLoop会在线程结束时销毁; -
RunLoop一直循环不退出,必须得有运行循环模式,并且在这个模式中得存在Source(Source0,Source1)、Timer中的任意一个
runloop和自动释放池
- 1)runloop启动的时候,会创建一个自动释放池
- 2)runloop退出和即将休眠的时候,会销毁自动释放池
Event Loop
一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。
如果没有事件,也没有timer,则runloop就会睡眠,如果有,则runloop就会被唤醒,然后跑一圈。
主线程的 RunLoop 的启动过程
iOS 程序能保持持续运行的原因就是在main()函数中调用了UIApplicationMain函数,这个函数内部会启动主线程的RunLoop。
RunLoop 与 NSTimer
NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef- 如果我们在子线程上使用
NSTimer,就必须开启子线程的RunLoop,否则定时器无法生效
runloop应用场景
在开发中,RunLoop 是一个让线程能随时处理事件但并不退出的机制,在不同的开发平台有着广泛的使用场景,以下为你详细介绍:
iOS/macOS 开发
- 定时器(NSTimer) :
NSTimer依赖于 RunLoop 来触发,只有当 RunLoop 处于运行状态,且注册了定时器的模式被激活时,定时器才会触发。例如,你可以使用NSTimer实现一个每隔一段时间更新 UI 的功能。
objc
// 创建一个定时器,每隔 1 秒触发一次
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self
selector:@selector(updateUI)
userInfo:nil
repeats:YES];
// 将定时器添加到当前 RunLoop 的默认模式中
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- 事件响应:当用户点击屏幕、滑动页面等操作产生的事件,会先被系统捕获,然后通过 RunLoop 分发给相应的处理函数。RunLoop 会不断地从事件队列中取出事件并处理,确保界面的流畅响应。
- 手势识别:手势识别器(UIGestureRecognizer)也是依赖 RunLoop 来工作的。当用户进行手势操作时,RunLoop 会不断地检测触摸事件,并将其传递给手势识别器进行识别和处理。
- 网络请求:在进行网络请求时,RunLoop 可以用于处理异步回调。例如,使用
NSURLSession进行网络请求时,当请求完成后,会通过回调函数将结果返回。这些回调函数会在 RunLoop 的特定模式下被调用,确保在合适的时机处理网络响应。
其他开发场景
- 线程保活:在某些情况下,你可能需要让一个线程一直保持运行状态,随时处理任务。这时可以使用 RunLoop 来实现线程的保活。例如,创建一个自定义线程,并在该线程中启动 RunLoop,使其不断循环处理事件。
objc
- (void)startCustomThread {
self.customThread = [[NSThread alloc] initWithTarget:self selector:@selector(threadEntryPoint) object:nil];
[self.customThread start];
}
- (void)threadEntryPoint {
@autoreleasepool {
// 创建并启动 RunLoop
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}
- 异步任务处理:可以利用 RunLoop 在后台线程中处理一些耗时的异步任务,避免阻塞主线程,保证界面的流畅性。例如,在后台线程中进行数据的加载和处理,处理完成后通过回调通知主线程更新 UI。
- 控制线程生命周期(线程保活)
self.thread = [[NSThread alloc]initWithBlock:^{
// 在线程里面开启RunLoop,self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
// 开启线程
[self.thread start];
-
解决NSTimer在滑动时停止工作的问题
-
监控应用卡顿
-
性能优化
-
定时器(Timer)、PerformSelector
-
GCD:dispatch_async(dispatch_get_main_queue(), ^{ });
-
事件响应、手势识别、界面刷新
-
网络请求
NSTimer 和 CADisplayLink 存在的问题
不准时:NSTime和CADisplayLink底层都是基于RunLoop的CFRunLoopTimerRef的实现的,也就是说它们都依赖于RunLoop。如果RunLoop的任务过于繁重,会导致它们不准时。
比如NSTimer每1.0秒就会执行一次任务,Runloop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于Runloop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能Runloop的任务过于繁重,第四次花了0.5s,那加起来时间就是 1.3s,导致NSTimer不准时。
解决方法:使用 GCD 的定时器。GCD 的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLoop,所以它非常的准时。示例如下:
dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
//创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//设置时间(start:几s后开始执行; interval:时间间隔)
uint64_t start = 2.0; //2s后开始执行
uint64_t interval = 1.0; //每隔1s执行
dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
//设置回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"%@",[NSThread currentThread]);
});
//启动定时器
dispatch_resume(timer);
NSLog(@"%@",[NSThread currentThread]);
self.timer = timer;
/*
2020-02-01 21:34:23.036474+0800 多线程[7309:1327653] <NSThread: 0x600001a5cfc0>{number = 1, name = main}
2020-02-01 21:34:25.036832+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:26.036977+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:27.036609+0800 多线程[7309:1327707] <NSThread: 0x600001a1e5c0>{number = 4, name = (null)}
*/