在CFRunloop 中已经说明了一个线程及其runloop的对应关系 ,现在以iOS中NSThread的实际使用来说明runloop在线程中的意义。
在iOS中直接使用NSThread有一下几种方式,但是归根到底,当一个线程需要长时间的去跟踪一个任务的时候,这几种方式做的事情是一样的,只不过接口名称和参数不一样,感觉是为了使用起来更加方便。因为这些接口内部都需要依赖runloop去实现事件的监听,这个可以通过调用堆栈证实。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
- (void)performSelector:(SEL)aSelector onThread:(NSThread*)thr withObject:(id)arg waitUntilDone:(BOOL)wait
以上两个方法都是NSObject的方法,可以直接通过一个对象来创建一个线程。第二个方法具有更多的灵活性,它可以让你自己指定线程,第一个方法是自己默认创建一个线程。第二个方法的最后一个参数是指定是否等待aSelector执行完毕。
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
该方法是NSThread的类方法,跟第一个方法是类似的功能。
下面通过在子线程发起一个网络请求,去发现一些问题,然后通过runloop去解释原因,并推测API背后的实现方式。
代码1
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 - (void )viewDidLoad { [super viewDidLoad]; [self performSelectorInBackground:@selector (multiThread) withObject:nil ]; } - (void )multiThread { if (![NSThread isMainThread]) { self .request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@" http://www.baidu.com" ] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10 ]; [self .request setHTTPMethod: @"GET" ]; self .connection =[[NSURLConnection alloc] initWithRequest:self .request delegate:self startImmediately:YES ]; } } - (void )connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response{ NSLog (@"network callback" ); }
运行之后,可以发现在子线程中发起的网络请求,回调没有被调用。根据CFRunloop 介绍的知识可以大致猜测可能跟runloop有关系,也就是子线程的runloop中没有注册网络回调的消息,所以该子线程自己相关的runloop没有收到回调。实际上
- (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)
这个方法的第三个参数的bool值表示是否在创建完NSURLConnection对象之后立刻发起请求,一般情况下是YES,什么时候会传NO呢。
事实上,对于以上这种方式创建的线程,默认是没有生成该线程对应的runloop的。也就是说这种情况下,需要自己去创建对应线程的runloop,并且让他run起来,去不断监听各种往runloop里注册的消息。但是对于主线程而言,其对应的runloop会由系统建立,并且自己run起来。由于平时工作在主线程下,这些工作大部分情况下不需要人为参与,所以一到子线程就会有各种问题。子线程中起timer没有生效也是相同的原因。所以以上函数第三个参数的意思就是,如果是当前线程已经runloop跑起来的情况下,传YES。除此之外,需要自己创建runloop去run,再将网络请求消息注册到runloop中。
现在根据以上分析修改代码:
代码2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 self .request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http:// www.baidu.com" ] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10 ]; [self .request setHTTPMethod: @"GET" ]; self .connection =[[NSURLConnection alloc] initWithRequest:self .request delegate:self startImmediately:NO ]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[runLoop run]; [self .connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode ]; [self .connection start];
代码3
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 self .request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:@"http:// www.baidu.com" ] cachePolicy:NSURLCacheStorageNotAllowed timeoutInterval:10 ]; [self .request setHTTPMethod: @"GET" ]; self .connection =[[NSURLConnection alloc] initWithRequest:self .request delegate:self startImmediately:NO ]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop];[self .connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode ]; [self .connection start]; [runLoop run];
然后就发现网络回调被调用了。
之后分析了一下调用堆栈:
第一个:在multiThread里面是这样的:
第二个:网络回调里面是这样的:
通过堆栈可以得知,这两个函数都是由线程6调用的,也就是创建的子线程,也就是创建的子线程,但是堆栈中的内容很不一样。很显然第二个是从runloop 调出的,并且是Sources0这个消息调出的。而第一个是线程运行时候的初始化方法。所以当调用runloop run的时候,其实是线程进入自己的runloop去监听时间了,从此以后,所有的代码都会从runloop CALLOUT出来。所以这种情况下,需要把先把消息注册到runloop中,让runloop跑起来是最后需要做的事情。
以下是开源库AFNetworking网络请求的实现:
AFNetworking
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 - (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]; } + (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; }
AFNetworking使用的是
- (void)performSelector:(SEL)aSelector onThread:(NSThread\*)thr withObject:(id)arg waitUntilDone:(BOOL)wait
这个方法,但是为什么它没有使用
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
这个方法呢?
通过断点,发现了AFNetwokring网络请求中一些函数的调用顺序:
1.networkRequestThread
2.networkRequestThreadEntryPoint
3.operationDidStart
为什么operationDidStart会在networkRequestThreadEntryPoint之后调用?
在networkRequestThreadEntryPoint里主要是生成网络线程的runloop并且让它跑起来,里面的
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]
这主要是为了在没有任何网络请求的时候让网络线程保持监听状态,否则网络线程的loop会直接返回,之后再调用网络线程请求就没有意义了。再结合调用堆栈,发现operationDidStart是在runloop callout出来的,而networkRequestThreadEntryPoint是网络线程的入口方法。这跟之前的例子是一样的。所以,我猜测
- (void)performSelector:(SEL)aSelector onThread:(NSThread\*)thr withObject:(id)arg waitUntilDone:(BOOL)wait
这个方法背后是由主线程将aSelector作为消息注册到runloop中时间发生在networkRequestThreadEntryPoint方法调用之前,所以在networkRequestThreadEntryPoint方法中调用 。 NSRunLoop currentRunLoop的时候其实runloop本身应该已经被创建了。原因是因为在这个地方断点 ,打印runloop对象可以发现里面已经注册了source0的消息,如下截图:
也就是说父线程在
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait**函数中将aSelector
注册成source0,这是该函数背后的大致实现。通过查阅apple官方文档,基本属实,如下所示:
通过上面的分析,可以得出使用performSelector 方法可以将子线程runloop的初始化实现在子线程的初始化方法里实现,如果使用performSelectorInBackground
方法,那么子线程runloop的初始化和业务逻辑就会混到一起,并且每一次都会重新初始化。AFNetworking通过一个静态全局的子线程去管理所有的网络请求,其对应的runloop也只需要初始化一次。
通过以上分析,可以知道如果需要让一个子线程去持续的监听时间,就需要启动它的runloop并且忘其中注册source,timer,oberserver三者之一的消息类型。在默认情况下子线程的runloop是不会自己创建和启动的。
线程之间的通讯:NSMachPort NSNotificationCenter是iOS中全局的观察者,可以用于不同页面之间消息传递解耦。
先看一段代码:
代码1
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 @implementation ViewController - (void )viewDidLoad { [super viewDidLoad]; NSLog (@"current thread = %@" , [NSThread currentThread]); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector ( handleNotification:) name:TEST_NOTIFICATION object:nil ]; dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil ]; }); } - (void )handleNotification:(NSNotification *)notification { NSLog (@"current thread = %@" , [NSThread currentThread]); NSLog (@"test notification" ); } @end
输出如下:
输出
1 2 3 current thread = <NSThread: 0x7fbb23412f30 >{number = 1 , name = main} current thread = <NSThread: 0x7fbb23552370 >{number = 2 , name = (null)} test[865 :45174 ] test notification
在主线程中注册了一个通知,在子线程中抛出事件,最后在子线程中处理事件。
但是有些时候,可能需要在同一个线程中处理事件,比如更新UI的操作只能放到主线程中进行。所以,需要做一次线程之间消息的转发。如果是子线程往主线程转发,通过GCD即可实现。但是如果是任意两个线程之间通讯,则需要依赖NSMachPort通过它往目标线程的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 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 @interface ViewController () <NSMachPortDelegate >@property (nonatomic ) NSMutableArray *notifications; @property (nonatomic ) NSThread *notificationThread; @property (nonatomic ) NSLock *notificationLock; @property (nonatomic ) NSMachPort *notificationPort; @end @implementation ViewController - (void )viewDidLoad { [super viewDidLoad]; NSLog (@"current thread = %@" , [NSThread currentThread]); self .notifications = [[NSMutableArray alloc] init]; self .notificationLock = [[NSLock alloc] init]; self .notificationThread = [NSThread currentThread]; self .notificationPort = [[NSMachPort alloc] init]; self .notificationPort.delegate = self ; [[NSRunLoop currentRunLoop] addPort:self .notificationPort forMode:(__bridge NSString *) kCFRunLoopCommonModes]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector ( processNotification:) name:@"TestNotification" object:nil ]; dispatch_async (dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0 ), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:TEST_NOTIFICATION object:nil userInfo:nil ]; }); } - (void )handleMachMessage:(void *)msg { [self .notificationLock lock]; while ([self .notifications count]) { NSNotification *notification = [self .notifications objectAtIndex:0 ]; [self .notifications removeObjectAtIndex:0 ]; [self .notificationLock unlock]; [self processNotification:notification]; [self .notificationLock lock]; }; [self .notificationLock unlock]; } - (void )processNotification:(NSNotification *)notification { if ([NSThread currentThread] != _notificationThread) { [self .notificationLock lock]; [self .notifications addObject:notification]; [self .notificationLock unlock]; [self .notificationPort sendBeforeDate:[NSDate date] components:nil from:nil reserved:0 ]; } else { NSLog (@"current thread = %@" , [NSThread currentThread]); NSLog (@"process notification" ); } } @end
输入如下:
1 2 3 test[1474 :92483 ] current thread = <NSThread: 0x7ffa4070ed50 >{number = 1 , name = main} test[1474 :92483 ] current thread = <NSThread: 0x7ffa4070ed50 >{number = 1 , name = main} test[1474 :92483 ] process notification