RunLoop

RunLoop 的概念

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

1
2
3
4
5
6
7
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}

这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

RunLoop在iOS中的初体验

UIApplicationMain函数内部启动了RunLoop,所以UIApplicationMain函数一直没有返回,保持程序持续运行。这个默认启动的RunLoop与主线程相关联

1
2
3
4
5
int main(int argc, char * argv[]){
@autoreleasepool{
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}

RunLoop对象

两套API

  • CGRunLoop(Core Foundation)

    它提供了纯 C 函数的 API,所有这些 API 都是线程安全的

  • NSRunLoop(基于CGRunLoop的封装)

    提供了面向对象的 API,但是这些 API 不是线程安全的

获取RunLoop对象

  • NSRunLoop
1
2
3
4
// 当前线程RunLoop对象
[NSRunLoop currentRunLoop];
// 主线程RunLoop对象
[NSRunLoop mainRunLoop];
  • CGRunLoop
1
2
3
4
// 当前线程RunLoop对象
CFRunLoopGetCurrent();
// 主线程RunLoop对象
CFRunLoopGetMain();

RunLoop与线程

  • 每条线程都对应一个唯一的RunLoop对象
  • 主线程的RunLoop自动创建,子线程的RunLoop需主动创建
  • RunLoop在第一次获取时创建,在线程结束时销毁

RunLoop相关类

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式

一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer

每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode

如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响

  • kCFRunLoopDefaultMode
    App的默认Mode,通常主线程是在这个Mode下运行

  • UITrackingRunLoopMode
    界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

  • UIInitializationRunLoopMode
    在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用

  • GSEventReceiveRunLoopMode
    接受系统事件的内部 Mode,通常用不到

  • kCFRunLoopCommonModes
    这是一个占位用的Mode,不是一种真正的Mode

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(输入源)

  • Source0:非基于Port的
  • Source1:基于Port的

CFRunloopTimerRef

CFRunLoopTimerRef是基于时间的触发器

其实就是NSTimer

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变

RunLoop处理逻辑

RunLoop处理逻辑

RunLoop处理逻辑

RunLoop应用

RunLoopTimer,RunLoopMode的使用

RunLoopTimer

1
2
3
4
5
6
7
8
9
10
// schedule方法创建的NSTimer,默认添加到RunLoop的加入default模式
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// timer方法创建的方法,需手动添加RunLoop中
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 定时器只能运行在NSDefaultRunLoopMode下,一旦RunLoop进入其他模式,将停止工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 在主线程中,不需要run,已经默认启动了一个runloop,非子线程,需要手动run
[[NSRunLoop currentRunLoop] run];

RunLoopMode

1
2
3
4
5
6
7
8
9
// 定时器只能运行在NSDefaultRunLoopMode下,一旦RunLoop进入其他模式,将停止工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

// 定时器只能运行在forMode:UITrackingRunLoopMode下,一旦RunLoop进入其他模式,将停止工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// 定时器会跑在标记为common modes的模式下
// 标记为common modes的模式:UITrackingRunLoopMode和kCFRunLoopDefaultMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

应用场景:如果在你拖拽scrollView时,希望某个操作不要执行,那么就不要再scrollView的代理方法中判断,而是直接将操作添加到defaultMode中。

显示图片(PerformSelector)

  • 场景:当一个线程在下载图片后,想显示在imageview上,但是用户在拖动view,如果此时依然要显示,会造成卡顿的现象

  • 解决:将显示放在default模式下,也就是说在拖动view时,控制器是处于UITraking模式的,那么显示就将无效,直到停止拖动。

1
2
// 在显示imageView图片时设置只在NSDefaultRunLoopMode模式下显示图片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

常驻线程

提示:

  • 线程执行完任务之后处于消亡状态,不能再次start,即使有一个强指针引用着。runloop一直跑圈的前提是里面有东西 (source(port),timer)
    自动释放池
  • RunLoop退出条件:
    • 线程被强制结束
    • 里面的Timer,source都清空了,observer有无是没影响的。因为runloop每次跑圈都会检测里面是否有东西

下面代码想实现的效果是:创建一个线程,为了不让线程执行完任务马上销毁,于是使用RunLoop让线程常驻,最后通过让线程再次执行任务来测试它是否被销毁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
- (void)viewDidLoad {
[super viewDidLoad];

// 创建线程
self.thread = [[SMThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}

- (void)run
{
// 在线程调用方法中,创建runloop,来保住子线程的命
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 先会检测runloop是否为空,空的话,直接退出,非空才开始跑圈
[[NSRunLoop currentRunLoop] run];
}

- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 线程通信
// 因为子线程一直保留住,所以可以在某个时刻执行其他操作,通过performSelector这个源发送给runloop,然后runloop在通知。。。。执行test方法
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

自动释放池

  • 在runloop休眠之前释放
  • 在唤醒后,做事前,又重新创建一个释放池

所以我们在启动runloop前,要创建一个自动释放池

1
2
3
4
5
6
7
8
- (void)execute
{
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}