RunLoop , 运行循环, App 可以在程序运行过程中做一些事情.
RunLoop 是什么?
为了说明, 我们分别用 Xcode 创建两个项目, 一个是 Command Tool, 一个是Single View App, 众所周知, 运行 Command Tool 程序, 只会在控制台输出结果, 并且只是一次性的, 运行 App, 程序会借助 模拟器/真机 运行.
这两者最大的区别在于, 在 main.m 文件中
Command Tool
int main(int argc, char * argv[]) {
return 0
}
App
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
App 之所以能在模拟器/真机中长期保持运行 状态, 而不会终止, 在于
UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))
原因:
- UIApplicationMain() 内部会创建一个 runloop. 使得程序不会马上退出, 而是保持保持运行状态.
- 这里面会处理App的各种事件(定时器事件, 用户交互事件等)
RunLoop对象
iOS中有两套API来访问和使用Runloop.
- Foundation: NSRunLoop
- Core Foundation: CFRunLoopRef
// viewDidLoad 这个方法是在主线程中调用的, 当前线程就是主线程
// 所以 mainRunLoop, currentRunLoop获得的 runloop 对象的地址是一样的.
NSLog(@"%p, %p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);
// 0x600003748600, 0x600003748600
NSLog(@"%p, %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());
// 0x600002f4c900, 0x600002f4c900
NSRunLoop
是基于 CFRunLoopRef
的一层OC包装, 官方开源了Core Foundation 的源码实现.
在源码中, 我们查看一下 CFRunLoopGetCurrent() 到底做了什么?
![](https://img.haomeiwen.com/i1208639/aa56ad6e5b6fc58f.png)
过程:
- 调用
_CFRunLoopGet0()
, 并传入参数 当前线程. - 其中
__CFRunLoops
, 是存放以pthread
为key,RunLoop
为 value 的字典. - 如果从字典中未找到 Runloop对象, 则 调用
__CFRunLoopCreate
为这条线程创建新的RunLoop , 并存储到字典中.
由此我们知道了Runloop 和 线程 的关系
- 每条线程都有与之对应的 RunLoop 对象.
- 线程刚创建的时候是没有 Runloop 的, 程序在运行的过程中, 会为这条线程创建对应的 RunLoop 对象, RunLoop 随着线程结束而销毁
- 线程 和 runloop 分别以键值对的形式存储在字典中, 方便程序管理.
Core Foundation中关于RunLoop的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
这是 CFRunLoopRef 的实现, 图中摘取了几个比较在意的成员变量.
![](https://img.haomeiwen.com/i1208639/f1a06a9a5c5facf1.png)
CFRunLoopModeRef 代表 RunLoop 的运行模式
常用到的有两种
-
kCFRunLoopDefaultMode (Mode的名字)
App的默认Mode, 通常主线程是在这个Mode下运行的 -
UITrackingRunLoopMode (Mode的名字)
界面追踪Mode, 用于ScrollView 追踪触摸滑动, 保证界面滑动时不受其他Mode影响
-
RunLoop
启动时只能选择其中的一个Mode
, 作为currentMode
. - 如果需要切换
Mode
, 只能退出当前Loop
, 再重新选择一个Mode
进入. - 不同
Model
的 Source0/Source1/Timer/Observer 分隔开来, 互不影响. - 如果
Mode
中没有任何 Source0/Source1/Timer/Observer,RunLoop
会立马退出.
- Source0: 触摸事件处理, performSelector: OnThread 等.
- Source1: 基于 Port 的线程间通信, 处理系统事件捕捉等.
- Timers: NSTimer操作, performSelector:withObject:afterDelay:等
- Observers: 监听RunLoop的状态, UI刷新(BeforeWaiting), Autorelease pool(BeforeWaiting).
当设置完view的背景色时, 这段代码不会立即生效, 而是等待 RunLoop 即将休眠的时候, 刷新界面
Mode | Name | Description |
---|---|---|
Default |
NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) |
默认模式是用于大多数操作的模式. 大多数情况下,您应该使用此模式启动运行循环并配置输入源. |
Connection |
NSConnectionReplyMode (Cocoa) |
Cocoa将此模式与NSConnection对象结合使用以监视回复. 很少使用此模式. |
Modal |
NSModalPanelRunLoopMode (Cocoa) |
Cocoa使用此模式来识别用于模态面板的事件. |
Event tracking |
NSEventTrackingRunLoopMode (Cocoa) |
Cocoa使用此模式在鼠标拖动循环和其他种类的用户界面跟踪循环期间限制传入事件. (拖动scrollView) |
Common modes |
NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) |
这是一组可配置的常用模式. 将输入源与此模式相关联也会将其与组中的每个模式相关联. 对于Cocoa应用程序, 此集合默认包括默认, 模态和事件跟踪模式. Core Foundation最初只包含默认模式. 您可以使用CFRunLoopAddCommonMode函数将自定义模式添加到集合中. |
详解RunLoop
前面我们从源码层面了解RunLoop, 现在我们从整体再来看.
![](https://img.haomeiwen.com/i1208639/2b10f7fe2b12d544.png)
有几点我们需要注意:
-
- RunLoop 从两种不同类型的源接收事件
-
Input sources
提供异步事件, 通常来自另外一个线程的消息, -
Timer sources
提供同步事件, 发生在预定时间, 或重复间隔.
-
- RunLoop Modes 是要监视的 Input sources 和 Timer sources 的集合,以及要通知的RunLoop observer的集合.
- 每次运行 RunLoop 时, 都显示/隐式 指定特定的运行模式.
- 模式是根据事件的来源而不是事件的类型进行区分的, 比如不会使用模式仅匹配鼠标按下事件或仅匹配键盘事件.
-
- Input sources 中通常有两类.
- 基于端口的 Input source 监视应用程序的 Mach 端口, 它是由内核自动发出信号.
- 自定义 Input source 处理自定义事件源, 它必须由另一个线程手动发信号给自定义源.
- Cocoa 还定义了一个自定义输入源, Cocoa Perform Selector Sources, 它允许我们在任何线程上执行选择器, 并且执行其选择器后将其自身从 RunLoop 中移除.
![]()
- runUtilDate: 是 NSRunLoop 类的对象方法, 用来运行 RunLoop.
- handlePort:, customSrc:, mySelector:, timeFired 是来自不同的源的事件(消息).
- Timer sources 在将来的预设时间将事件同步传递给你的线程。定时器是线程通知自己做某事的一种方式.
补充说明: Loop Observer
与在发生适当的异步或同步事件时触发的源不同,RunLoop observer 在执行 RunLoop 期间, 在特殊位置触发.
![](https://img.haomeiwen.com/i1208639/ae4073b891cef106.png)
RunLoop的多种状态:
- kCFRunLoopEntry: 即将进入 RunLoop
- kCFRunLoopBeforeTimers: 即将处理 Timer
- kCFRunLoopBeforeSources: 即将处理 Sources
- kCFRunLoopBeforeWaiting: RunLoop 即将休眠
- kCFRunLoopAfterWaiting: RunLoop 即将唤醒
- kCFRunLoopExit: 即将退出RunLoop
RunLoop的事件处理
每次运行 RunLoop 时, 线程的RunLoop都会处理挂起的事件, 并且为任何附加的观察者生成通知. (App一启动, 会自动在主线程设置并运行RunLoop, 称之为 主循环)
- Notify observers: 进入运行循环.
- Notify observers: 即将处理 Timer.
- Notify observers: 即将处理Sources
- 处理Source0: 触发任何准备触发的基于非端口的输入源, 跳到第 9 步:
- 处理Source1: (如果基于端口的输入源准备就绪并等待触发), 就跳到第 9 步:
- Notify observers: 线程即将休眠(等待消息唤醒)
-
Notify observers: 线程结束休眠(被下面的消息唤醒)
- 处理Timer
- 处理Source1: 事件到达基于端口的输入源
- RunLoop 被明确唤醒
- 为 RunLoop 设置的超时值到期
- Notify observers: 线程刚刚醒来.
-
处理 Blocks:
- 如果输入源被触发,则传递事件.
- 如果触发了用户定义的计时器,则处理计时器事件并重新RunLoop。转到第2步.
- 如果运行循环被明确唤醒但尚未超时,请重新RunLoop, 转到第2步
- Notify observers: RunLoop 已退出
使用 RunLoop
我们需要显示运行 RunLoop 的唯一时机是为应用程序创建辅助线程, 对于辅助线程, 如果确定需要运行循环, 那么需要配置并运行它.
- 在线程上使用 Timer.
- 保持线程以执行定期任务(线程保活).
- 使用端口或自定义输入源与其他线程通信.
1. 解决NSTimer在滑动时停止工作的问题
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"==>%d",_count++);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
原因:
RunLoop 处理事件的默认 Mode 是 Default, 当 app 同时有计时器事件和scrollView滚动事件时, 优先处理 scrollView 滚动事件(Event tracking Mode), 处理完才会再来处理计时器事件.
解决办法:
将 计时器事件 与 Common Mode 绑定, RunLoop 内部会自动切换 Tracking Mode 和 Default Mode, 来处理计时器事件 和 scrollView 滚动事件, 使得两者看似同时在工作.
2. 线程保活
LCThread 类是一个继承自 NSThread 的类, 在里面我们实现了 dealloc 方法, 为了监测线程是否被销毁的情况.
self.thread = [[LCThread alloc] initWithBlock:^{
// 一直在运行. 线程保活
NSLog(@"----begin----%s", __func__);
// 当前runloop开始睡眠, 当前线程被阻塞了
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
NSLog(@"----end----%s", __func__);
}];
// 启动此线程
[self.thread start];
保证线程不立刻被销毁, 我们在此期间制定任务
比如: 点击屏幕. 打印此线程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
手动释放线程
- (void)stopThread{
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)stop
{
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
3. 监控界面卡顿
通过 RunLoop observer 来监控目标 RunLoop 的状态, 如果频繁出现 kCFRunLoopBeforeSources, kCFRunLoopAfterWaiting, 检测出现次数, timeCount, 超过指定次数可认为App卡顿 .
因为这两个状态是要去处理事件的状态.
网友评论