0%

runloop使用

runloop启动前必须先添加输入源或定时器源,否则runloop一启动就会退出。总之runloop需要有监听的事件,否则就会退出。

启动runloop

NSRunLoop提供了如下3种启动runloop的方法。当然NSRunLoop其实封装的是CFRunloop。

1
2
3
4
5
- (void)run NS_SWIFT_UNAVAILABLE_FROM_ASYNC("run cannot be used from async contexts.");

- (void)runUntilDate:(NSDate *)limitDate NS_SWIFT_UNAVAILABLE_FROM_ASYNC("run(until:) cannot be used from async contexts.");

- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate NS_SWIFT_UNAVAILABLE_FROM_ASYNC("run(_:before:) cannot be used from async contexts.");

相同点:如果没有输入源或定时器源附加到runloop上,runloop会马上退出,并且方法也会立即退出。

run
它会让runloop置身在一个永久的循环当中.即使runloop因为处理完了某个输入源事件而退出,该方法又会让它重新运行.因此如果你想处理完某个事件后能够退出runloop,那么你就不能使用该方法了。runloop运行在default模式。

runUntilDate:
和run方法差不多只是多了个截止时间,截止时间到了runloop就会退出。runloop同样运行在default模式。

runMode:beforeDate:
可以指定runloop运行的模式以及一个截止时间。处理定时器源事件后不会退出runloop,但处理输入源事件后会退出runloop,因此需要外部重新驱动进入runloop。

退出runloop

根据官方文档,手动移除输入源或定时器源不能确保runloop会退出。最好的办法是使用runMode:beforeDate:方法启动runloop,设立标志位,给子线程发送消息perform selector。

runloop使用场景

由于主线程的runloop默认是开启的,所以一般是子线程应用runloop。

  1. 在子线程上使用定时器
  2. 其他线程需要与该子线程通信,比如perform selector或其他source。

CocoaAsyncSocket

创建线程

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
#if TARGET_OS_IPHONE
static NSThread *cfstreamThread; // Used for CFStreams


static uint64_t cfstreamThreadRetainCount; // setup & teardown
static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown
#endif

+ (void)startCFStreamThreadIfNeeded
{
LogTrace();

static dispatch_once_t predicate;
dispatch_once(&predicate, ^{

cfstreamThreadRetainCount = 0;
cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL);
});

dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool {

if (++cfstreamThreadRetainCount == 1)
{
cfstreamThread = [[NSThread alloc] initWithTarget:self
selector:@selector(cfstreamThread:)
object:nil];
[cfstreamThread start];
}
}});
}

线程保活—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
+ (void)cfstreamThread:(id)unused { @autoreleasepool
{
[[NSThread currentThread] setName:GCDAsyncSocketThreadName];

LogInfo(@"CFStreamThread: Started");

// We can't run the run loop unless it has an associated input source or a timer.
// So we'll just create a timer that will never fire - unless the server runs for decades.
[NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
target:self
selector:@selector(ignore:)
userInfo:nil
repeats:YES];

NSThread *currentThread = [NSThread currentThread];
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];

BOOL isCancelled = [currentThread isCancelled];

while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]])
{
isCancelled = [currentThread isCancelled];
}

LogInfo(@"CFStreamThread: Stopped");
}}

将一个可重复的、间隔时间为永远的(也就是永远不会触发的定时器)定时器添加到runloop,这样就确保了runloop不至于一启动就退出。

调用- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; 来启动runloop,这样runloop在处理输入源后就会退出。

while循环判断线程是否被取消,如果不是被取消则重新启动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
32
33
34
35
36
37
38
+ (void)stopCFStreamThreadIfNeeded
{
LogTrace();

// The creation of the cfstreamThread is relatively expensive.
// So we'd like to keep it available for recycling.
// However, there's a tradeoff here, because it shouldn't remain alive forever.
// So what we're going to do is use a little delay before taking it down.
// This way it can be reused properly in situations where multiple sockets are continually in flux.

int delayInSeconds = 30;
dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool {
#pragma clang diagnostic push
#pragma clang diagnostic warning "-Wimplicit-retain-self"

if (cfstreamThreadRetainCount == 0)
{
LogWarn(@"Logic error concerning cfstreamThread start / stop");
return_from_block;
}

if (--cfstreamThreadRetainCount == 0)
{
[cfstreamThread cancel]; // set isCancelled flag

// wake up the thread
[[self class] performSelector:@selector(ignore:)
onThread:cfstreamThread
withObject:[NSNull null]
waitUntilDone:NO];

cfstreamThread = nil;
}

#pragma clang diagnostic pop
}});
}

结束线程这里调的是cancel方法,由于线程还启动了runloop,所以在子线程上调用performSelector,唤醒线程,runloop处理后会退出,由于已经cancel,所以while也不会再启动runloop,于是线程正常结束。

线程的创建和结束都是在一个串行队列里操作的,所以不会出现多线程操作的情况。

AFNetworking

创建线程

1
2
3
4
5
6
7
8
9
10
+ (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;
}

线程保活—runloop

1
2
3
4
5
6
7
8
9
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}

添加了一个基于端口的输入源(虽然并没有真正使用到),确保runloop不至于一启动就退出。调用run 来启动runloop,该方法在runloop退出后会重新启动runloop继续运行。AFNetworking创建的这个线程在整个APP内都不会销毁的。二者无一例外的使用全局变量来引用线程。

参考

CFRunloop的多线程隐患

觉得文章有帮助可以打赏一下哦!