Runloop
Runloop 是什么
Runloop 实际是一个对象,这个对象一直在循环中来处理 App 运行过程中各种事件(触摸事件,UI 刷新事件,定时器事件,Selector 事件),从而保持程序的持续运行。Runloop 在没有事件需要处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。
Runloop 与线程的关系
线程的作用是用来执行一个或多个任务。在默认情况下,线程执行完之后就会退出。如果需要线程能一直不断的处理任务,且并不会退出,这时就需要用到 Runloop。
- 一个线程对应一个 Runloop 对象,每条线程都有唯一一个对应的 Runloop 对象。
- Runloop 并不保证线程安全,只能在当前线程内部操作当前线程的 Runloop 对象,而不能在当前线程内部去操作其他线程的 Runloop 对象方法。
- Runloop 对象在第一次获取 Runloop 时创建,在线程结束时,销毁。
- 主线程的 Runloop 对象系统自动创建,其余线程的 Runloop 对象需要主动创建和维护。
主线程的 Runloop
主线程的 Runloop 是默认启动的。iOS App 在启动时,会执行一个 main 函数。1
2
3
4
5int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在 UIApplicationMain 内部拥有一个无限循环的代码,只要 App 不退出不崩溃,就一直在循环。1
2
3
4
5
6
7
8
9int main(int argc, char * argv[]) {
BOOL running = YES;
do {
// 执行各种任务,处理各种事件
// ......
} while (running); // 判断是否需要退出
return 0;
}
Runloop 会在循环中会不断检测,通过 Input sources (输入源)和 Timer sources (定时源)两种来源等待接受事件,然后对接受到的事件通知线程进行处理,并且在没有事件时,让线程进行休息。
Runloop 的相关类
- CFRunLoopRef: Runloop 对象
- CFRunLoopModeRef:Runloop 的运行模式
- CFRunLoopSourceRef:Runloop 中的输入源
- CFRunLoopTimerRef:Runloop 中的定时源
- CFRunLoopObserverRef:观察者,能监听 Runloop 的状态改变
之间的关系
一个 Runloop 对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每个运行模式下包含若干个输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef)。
- 每次 Runloop 启动时,只能指定一个运行模式(CFRunLoopModeRef),这个运行模式被称作当前运行模式(CurrentMode)。
- 如果需要切换运行模式,只能退出当前的循环(Loop),再重新指定一个运行模式进入。
- 这样做的目的是为了分隔开不同组的输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef),让其互不影响。
CFRunLoopRef
CFRunLoopGetCurrent();
获得当前线程的 RunLoop 对象CFRunLoopGetMain();
获得主线程的 RunLoop 对象[NSRunLoop currentRunLoop];
获得当前线程的 RunLoop 对象[NSRunLoop mainRunLoop];
获得主线程的 RunLoop 对象
CFRunLoopModeRef
- kCFRunLoopDefaultMode: App 的默认 Mode,通常主线程是在这个 Mode 下运行的。
- UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes: 这是一个占位的 Mode,是一个伪模式。
其中 kCFRunLoopDefaultMode。UITrackingRunLoopMode,kCFRunLoopCommonModes 是我们开发时需要用到的运行模式。
CFRunLoopTimerRef
例子:
- 新建项目,在 Main.storyboard 中拖入一个 TextView
在 ViewController 中加入以下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14- (void)viewDidLoad {
[super viewDidLoad];
// 定义一个定时器,约定两秒之后调用self的run方法
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)run
{
NSLog(@"---run");
}运行,这时我们不对模拟器进行任何操作,定时器会稳定的每隔 2 秒调用 run 方法打印
- 当我们滚动 TextView 时,run 方法不打印了。松开鼠标时,run 方法有开始正常工作
原因:
- 当我们不做任何操作时,Runloop 处于 NSDefaultRunLoopMode 下的运行模式
- 当我们拖动 TextView 时,Runloop 就结束 NSDefaultRunLoopMode,切换到了 UITrackingRunLoopMode 模式,这个 Mode 下没有添加 NSTImer,所有 run 方法不执行了
- 当我们松开鼠标时,Runloop 就结束 UITrackingRunLoopMode 模式,又切换回 NSDefaultRunLoopMode 模式,所以 NSTimer 又开始正常工作
可以将[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
替换为[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
,则情况相反。
使用[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
可以保证在 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 下都能够正常的执行 run 方法。这里用到的就是 kCFRunLoopCommonModes。
CFRunLoopSourceRef
有两种分类方式
根据官方文档:
- Port-Based Sources(基于端口)
- Custom Input Sources(自定义)
- Cocoa Perform Selector Sources
根据函数调用栈
- Source0 :非基于 Port
- Source1:基于 Port,通过内核和其他线程通信,接收、分发系统事件
CFRunLoopObserverRef
监听的状态有下面几种:1
2
3
4
5
6
7
8
9typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop:1
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer:2
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source:4
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠:32
kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒:64
kCFRunLoopExit = (1UL << 7), // 即将从Loop中退出:128
kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变
};
例子:1
2
3
4
5
6
7
8
9
10
11
12
13
14- (void)viewDidLoad {
[super viewDidLoad];
// 创建观察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"监听到RunLoop发生改变---%zd",activity);
});
// 添加观察者到当前RunLoop中
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放observer,最后添加完需要释放掉
CFRelease(observer);
}
原理
- 通知观察者 RunLoop 已经启动
- 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源
- 启动任何准备好的非基于端口的源
- 如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤 9
- 通知观察者线程进入休眠状态
- 将线程置于休眠知道任一下面的事件发生:
- 某一事件到达基于端口的源
- 定时器启动
- RunLoop 设置的时间已经超时
- RunLoop 被显示唤醒
- 通知观察者线程将被唤醒
- 处理未处理的事件
- 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop。进入步骤 2
- 如果输入源启动,传递相应的消息
- 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop。进入步骤 2
- 通知观察者 RunLoop 结束
应用
NSTimer 的使用
参考 CFRunLoopTimerRef 中的例子
延迟显示 ImageView
当列表中有很多 ImageView 需要显示图片时,就可能会出现卡顿现象。
利用 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupian"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];
实现在列表滚动时,不显示图片保证流畅性,只在列表静止时,显示图片。
创建后台常驻的线程
1 | - (void)viewDidLoad { |