iOS RunLoop

用实例理解 RunLoop

Posted by MAYBESHEWILL on September 5, 2016

首先来看看定义

什么是RunLoop?
提到RunLoop,我们一般都会提到线程,这是为什么呢?先来看下官方对RunLoop的定义:RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。
RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有’主人’的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,’撞坏’被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。
这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来),其实RunLoop是管理线程的一种机制,这种机制不仅在 iOS 上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observer,RunLoop Mode包含以下几种:
NSDefaultRunLoopMode,NSEventTrackingRunLoopMode, UIInitializationRunLoopMode,NSRunLoopCommonModes, NSConnectionReplyMode,NSModalPanelRunLoopMode

初步认识

主线程中测试下边的代码

1
2
3
4
5
6
7
8
9
@interface 
while (1) {
	NSLog(@"while begin");
	// the thread be blocked here
	NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
	[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
	// this will not be executed
	NSLog(@"while end");
}

执行完[runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];之后,NSLog(@"while end");没有被完整的执行.
用 GCD 放到后台线程中:

1
2
3
4
5
6
7
8
9
10
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

	while (1) {

    NSLog(@"while begin");
    NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
    [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    NSLog(@"while end");
	}
});

这时候会发现,会一直在执行.打印当前的 RunLoop 再来看子线程添加的 RunLoop ⤵️

其中

1
2
3
4
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),

我们看到虽然有Mode,但是我们没有给它soures,observer,timer,其实Mode中的这些source,observer,timer,统称为这个Mode的item,如果一个Mode中一个item都没有,则这个RunLoop会直接退出,不进入循环(其实线程之所以可以一直存在就是由于RunLoop将其带入了这个循环中)。下面我们为这个RunLoop添加个source:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

    while (1) {

    NSPort *macPort = [NSPort port];
    NSLog(@"while begin");
    NSRunLoop *subRunLoop = [NSRunLoop currentRunLoop];
    [subRunLoop addPort:macPort forMode:NSDefaultRunLoopMode];
    [subRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    NSLog(@"while end");
    NSLog(@"%@",subRunLoop);

}    

});

这样我们可以看到能够实现了和主线程中相同的效果,线程在这个地方暂停了,为什么呢?我们明明让RunLoop在distantFuture之前都一直run的啊?相信大家已经猜出出来了。这个时候线程被RunLoop带到‘坑’里去了,这个‘坑’就是一个循环,在循环中这个线程可以在没有任务的时候休眠,在有任务的时候被唤醒;当然我们只用一个while(1)也可以让这个线程一直存在,但是这个线程会一直在唤醒状态,及时它没有任务也一直处于运转状态,这对于CPU来说是非常不高效的。

小结: 我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是应为系统为其添加了很多Item

Perform Selector

在主线程测试:

1
2
3
4
5
6
7
8
9
10
11
- (void)tryPerformSelectorOnMianThread{

[self performSelector:@selector(mainThreadMethod) withObject:nil]; }

- (void)mainThreadMethod{

NSLog(@"execute %s",__func__);

// print: execute -[ViewController mainThreadMethod]
}
// 没毛病,会执行

在后台线程执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)tryPerformSelectorOnBackGroundThread{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];

});
}
- (void)backGroundThread{

NSLog(@"%u",[NSThread isMainThread]);

NSLog(@"execute %s",__FUNCTION__);

}
// 调用tryPerformSelectorOnBackGroundThread 之后, backGroundThread 不执行了! 

这是因为,在调用performSelector:onThread: withObject: waitUntilDone的时候,系统会给我们创建一个Timer的source,加到对应的RunLoop上去,但是这个时候我们还没有RunLoop,如果我们加上RunLoop:

1
2
3
4
5
6
7
8
9
10
11
- (void)tryPerformSelectorOnBackGroundThread{

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

[self performSelector:@selector(backGroundThread) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop run];

	});
}

这时候就可以正常调用了.那为啥主线程可以执行呢? 因为主线程的 RunLoop 是一直存在的,我们无需再添加 RunLoop ,但是 当perform selector在后台线程中执行的时候,这个线程必须有一个开启的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
28
29
30
31
@interface ViewController ()

@property(nonatomic,strong) NSThread *myThread;

@end

@implementation ViewController

 - 	(void)alwaysLiveBackGoundThread{

NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(myThreadRun) object:@"etund"];
self.myThread = thread;
[self.myThread start];

}
- (void)myThreadRun{

NSLog(@"my thread run");

}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

NSLog(@"%@",self.myThread);
[self performSelector:@selector(doBackGroundThreadWork) onThread:self.myThread withObject:nil waitUntilDone:NO];
}
- (void)doBackGroundThreadWork{

NSLog(@"do some work %s",__FUNCTION__);

}
@end

执行之后发现,这个self.myThread 是存在的,但是为什么就是他妈的不执行doBackGroundThreadWork 呢?过上面的论述,我们应该怎样处理呢?我们可以给这个线程的RunLoop添加一个source,那么这个线程就会检测这个source等待执行,而不至于死亡.嘿,哥们,我还在.

1
2
3
4
5
6
7
- (void)myThreadRun{

 [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode]; 
 [[NSRunLoop currentRunLoop] run]

 NSLog(@"my thread run");
} 这个时候再次点击屏幕,我们就会发现,后台线程中执行的任务可以正常进行了。

小结: 正常情况下,后台线程执行完任务之后就处于死亡状态,我们要避免这种情况的发生可以利用RunLoop,并且给它一个Source这样来保证线程依旧还在

NSTimer

主线程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)tryTimerOnMainThread{

NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self       
selector:@selector(timerAction) userInfo:nil repeats:YES];

[myTimer fire];

}

- (void)timerAction{

NSLog(@"timer action");
// 没毛病,因为主线程有一个 RunLoop 在
} 这样呢:

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];

[myTimer fire];

});
// 只走了一次 就停止了

NSTimer,只有注册到RunLoop之后才会生效,这个注册是由系统自动给我们完成的,既然需要注册到RunLoop,那么我们就需要有一个RunLoop,我们在后台线程中加入如下的代码:

1
2
3
4
5
6
7
8
9
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSTimer *myTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
    // 子线程需要开一个 Runloop
    NSRunLoop *runloop1 = [NSRunLoop currentRunLoop];
    [runloop1 run];
    
    [myTimer fire];
});
// 这样就没毛病了

让两个后台线程具有依赖性

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void)runLoopAddDependance{

self.runLoopThreadDidFinishFlag = NO;
NSLog(@"Start a New Run Loop Thread");
NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];
[runLoopThread start];

NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");
dispatch_async(dispatch_get_global_queue(0, 0), ^{

// 这个时候并不能执行线程完成之后的任务,因为Run Loop所在的线程并不知道runLoopThreadDidFinishFlag被重新赋值。Run Loop这个时候没有被任务事件源唤醒。

while (!_runLoopThreadDidFinishFlag) {

    self.myThread = [NSThread currentThread];
    NSLog(@"Begin RunLoop");
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    NSPort *myPort = [NSPort port];
    [runLoop addPort:myPort forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    NSLog(@"End RunLoop");
    [self.myThread cancel];
    self.myThread = nil;

		}
	});
}
- (void)handleRunLoopThreadTask
{
	NSLog(@"Enter Run Loop Thread");
	for (NSInteger i = 0; i < 5; i ++) {
NSLog(@"In Run Loop Thread, count = %ld", i);
sleep(1);
// 应该在这里加上:
	[self performSelector:@selector(tryOnMyThread) onThread:self.myThread withObject:nil waitUntilDone:NO];
}

其中:

- (void)tryOnMyThread{

	_runLoopThreadDidFinishFlag = YES;

}

NSURLConnection的执行过程: 在使用NSURLConnection时,我们会传入一个Delegate,当我们调用了[connection start]之后,这个Delegate会不停的收到事件的回调。实际上,start这个函数的内部会获取CurrentRunloop,然后在其中的DefaultMode中添加4个source。如下图所示,CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的。如下图所示:

从中可以看出,当开始网络传输是,我们可以看到NSURLConnection创建了两个新的线程:com.apple.NSURLConnectionLoader和com.apple.CFSocket.private。其中CFSocket是处理底层socket链接的。NSURLConnectionLoader这个线程内部会使用RunLoop来接收底层socket的事件,并通过之前添加的source,来通知(唤醒)上层的Delegate。这样我们就可以理解我们平时封装网络请求时候常见的下面逻辑了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
while (!_isEndRequest)
{
NSLog(@"entered run loop");
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

NSLog(@"main finished,task be removed");

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{

_isEndRequest = YES;

}
  1. 为什么这个While循环不停的执行,还需要使用一个RunLoop? 程序执行一个while循环是不会耗费很大性能的,我们这里的目的是想让子线程在有任务的时候处理任务,没有任务的时候休眠,来节约CPU的开支。

  2. 如果没有为RunLoop添加item,那么它就会立即退出,这里的item呢? 其实系统已经给我们默认添加了4个source了。
  3. 既然[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];让线程在这里停下来,那么为什么这个循环会持续的执行呢?因为这个一直在处理任务,并且接受系统对这个Delegate的回调,也就是这个回调唤醒了这个线程,让它在这里循环。

AFNetWorking中是如何使用RunLoop的

在AFN中AFURLConnectionOperation是基于NSURLConnection构建的,其希望能够在后台线程来接收Delegate的回调。 为此AFN创建了一个线程,然后在里面开启了一个RunLoop,然后添加item

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}

}

+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{ 
    _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
    [_networkRequestThread start];
});
return _networkRequestThread;
}

这里的NSMachPort的作用和上边中一样,就是让线程不至于在很快死亡,然后RunLoop不至于退出(如果要使用这个MachPort的话,调用者需要持有这个NSMachPort,然后在外部线程通过这个port发送信息到这个loop内部,它这里没有这么做)。然后和上面的做法相似,在需要后台执行这个任务的时候,会通过调用:[NSObject performSelector:onThread:..]来将这个任务扔给后台线程的RunLoop中来执行。

1
2
3
4
5
6
7
8
9
10
- (void)start {
	[self.lock lock];
if ([self isCancelled]) {
    [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
} else if ([self isReady]) {
	self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
}
	[self.lock unlock];
}

GCD定时器的实现

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
28
29
30
31
32
33
34
35
- (void)gcdTimer{

// get the queue
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

// creat timer
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// config the timer (starting time,interval)
// set begining time
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
// set the interval
uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);

dispatch_source_set_timer(self.timer, start, interver, 0.0);

dispatch_source_set_event_handler(self.timer, ^{

// the tarsk needed to be processed async
dispatch_async(dispatch_get_global_queue(0, 0), ^{

    for (int i = 0; i < 100000; i++) {

        NSLog(@"gcdTimer");


    }

});


});

	dispatch_resume(self.timer);

}

参考连接:

http://blog.csdn.net/enuola/article/details/9163051 https://blog.ibireme.com/2015/05/18/runloop/

文章使用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议