Runloop和多線程

CFRunloop中已經(jīng)說明了一個(gè)線程及其runloop的對(duì)應(yīng)關(guān)系 ,現(xiàn)在以iOS中NSThread的實(shí)際使用來說明runloop在線程中的意義。

在iOS中直接使用NSThread有一下幾種方式,但是歸根到底,當(dāng)一個(gè)線程需要長時(shí)間的去跟蹤一個(gè)任務(wù)的時(shí)候,這幾種方式做的事情是一樣的,只不過接口名稱和參數(shù)不一樣,感覺是為了使用起來更加方便。因?yàn)檫@些接口內(nèi)部都需要依賴runloop去實(shí)現(xiàn)事件的監(jiān)聽,這個(gè)可以通過調(diào)用堆棧證實(shí)。

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

- (void)performSelector:(SEL)aSelector onThread:(NSThread*)thr withObject:(id)arg waitUntilDone:(BOOL)wait

以上兩個(gè)方法都是NSObject的方法,可以直接通過一個(gè)對(duì)象來創(chuàng)建一個(gè)線程。第二個(gè)方法具有更多的靈活性,它可以讓你自己指定線程,第一個(gè)方法是自己默認(rèn)創(chuàng)建一個(gè)線程。第二個(gè)方法的最后一個(gè)參數(shù)是指定是否等待aSelector執(zhí)行完畢。

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

該方法是NSThread的類方法,跟第一個(gè)方法是類似的功能。

下面通過在子線程發(fā)起一個(gè)網(wǎng)絡(luò)請(qǐng)求,去發(fā)現(xiàn)一些問題,然后通過runloop去解釋原因,并推測API背后的實(shí)現(xiàn)方式。

代碼1

- (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");

}

運(yùn)行之后,可以發(fā)現(xiàn)在子線程中發(fā)起的網(wǎng)絡(luò)請(qǐng)求,回調(diào)沒有被調(diào)用。根據(jù)CFRunloop介紹的知識(shí)可以大致猜測可能跟runloop有關(guān)系,也就是子線程的runloop中沒有注冊(cè)網(wǎng)絡(luò)回調(diào)的消息,所以該子線程自己相關(guān)的runloop沒有收到回調(diào)。實(shí)際上

- (instancetype)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL)

這個(gè)方法的第三個(gè)參數(shù)的bool值表示是否在創(chuàng)建完NSURLConnection對(duì)象之后立刻發(fā)起請(qǐng)求,一般情況下是YES,什么時(shí)候會(huì)傳NO呢。

事實(shí)上,對(duì)于以上這種方式創(chuàng)建的線程,默認(rèn)是沒有生成該線程對(duì)應(yīng)的runloop的。也就是說這種情況下,需要自己去創(chuàng)建對(duì)應(yīng)線程的runloop,并且讓他run起來,去不斷監(jiān)聽各種往runloop里注冊(cè)的消息。但是對(duì)于主線程而言,其對(duì)應(yīng)的runloop會(huì)由系統(tǒng)建立,并且自己run起來。由于平時(shí)工作在主線程下,這些工作大部分情況下不需要人為參與,所以一到子線程就會(huì)有各種問題。子線程中起timer沒有生效也是相同的原因。所以以上函數(shù)第三個(gè)參數(shù)的意思就是,如果是當(dāng)前線程已經(jīng)runloop跑起來的情況下,傳YES。除此之外,需要自己創(chuàng)建runloop去run,再將網(wǎng)絡(luò)請(qǐng)求消息注冊(cè)到runloop中。

現(xiàn)在根據(jù)以上分析修改代碼:

代碼2

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

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];

然后就發(fā)現(xiàn)網(wǎng)絡(luò)回調(diào)被調(diào)用了。

之后分析了一下調(diào)用堆棧:

第一個(gè):在multiThread里面是這樣的:

multiThread.png

第二個(gè):網(wǎng)絡(luò)回調(diào)里面是這樣的:

網(wǎng)絡(luò)回調(diào).png

通過堆??梢缘弥@兩個(gè)函數(shù)都是由線程6調(diào)用的,也就是創(chuàng)建的子線程,也就是創(chuàng)建的子線程,但是堆棧中的內(nèi)容很不一樣。很顯然第二個(gè)是從runloop 調(diào)出的,并且是Sources0這個(gè)消息調(diào)出的。而第一個(gè)是線程運(yùn)行時(shí)候的初始化方法。所以當(dāng)調(diào)用runloop run的時(shí)候,其實(shí)是線程進(jìn)入自己的runloop去監(jiān)聽時(shí)間了,從此以后,所有的代碼都會(huì)從runloop CALLOUT出來。所以這種情況下,需要把先把消息注冊(cè)到runloop中,讓runloop跑起來是最后需要做的事情。

以下是開源庫AFNetworking網(wǎng)絡(luò)請(qǐng)求的實(shí)現(xiàn):

AFNetworking

- (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

這個(gè)方法,但是為什么它沒有使用

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

這個(gè)方法呢?

通過斷點(diǎn),發(fā)現(xiàn)了AFNetwokring網(wǎng)絡(luò)請(qǐng)求中一些函數(shù)的調(diào)用順序:

1.networkRequestThread

2.networkRequestThreadEntryPoint

3.operationDidStart

為什么operationDidStart會(huì)在networkRequestThreadEntryPoint之后調(diào)用?

在networkRequestThreadEntryPoint里主要是生成網(wǎng)絡(luò)線程的runloop并且讓它跑起來,里面的

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]

這主要是為了在沒有任何網(wǎng)絡(luò)請(qǐng)求的時(shí)候讓網(wǎng)絡(luò)線程保持監(jiān)聽狀態(tài),否則網(wǎng)絡(luò)線程的loop會(huì)直接返回,之后再調(diào)用網(wǎng)絡(luò)線程請(qǐng)求就沒有意義了。再結(jié)合調(diào)用堆棧,發(fā)現(xiàn)operationDidStart是在runloop callout出來的,而networkRequestThreadEntryPoint是網(wǎng)絡(luò)線程的入口方法。這跟之前的例子是一樣的。所以,我猜測

- (void)performSelector:(SEL)aSelector onThread:(NSThread\*)thr withObject:(id)arg waitUntilDone:(BOOL)wait

這個(gè)方法背后是由主線程將aSelector作為消息注冊(cè)到runloop中時(shí)間發(fā)生在networkRequestThreadEntryPoint方法調(diào)用之前,所以在networkRequestThreadEntryPoint方法中調(diào)用 。 NSRunLoop currentRunLoop的時(shí)候其實(shí)runloop本身應(yīng)該已經(jīng)被創(chuàng)建了。原因是因?yàn)樵谶@個(gè)地方斷點(diǎn) ,打印runloop對(duì)象可以發(fā)現(xiàn)里面已經(jīng)注冊(cè)了source0的消息,如下截圖:

currentRunloop.png

也就是說父線程在

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait**函數(shù)中將aSelector

注冊(cè)成source0,這是該函數(shù)背后的大致實(shí)現(xiàn)。通過查閱apple官方文檔,基本屬實(shí),如下所示:

官方文檔.png

通過上面的分析,可以得出使用performSelector方法可以將子線程runloop的初始化實(shí)現(xiàn)在子線程的初始化方法里實(shí)現(xiàn),如果使用performSelectorInBackground

方法,那么子線程runloop的初始化和業(yè)務(wù)邏輯就會(huì)混到一起,并且每一次都會(huì)重新初始化。AFNetworking通過一個(gè)靜態(tài)全局的子線程去管理所有的網(wǎng)絡(luò)請(qǐng)求,其對(duì)應(yīng)的runloop也只需要初始化一次。

通過以上分析,可以知道如果需要讓一個(gè)子線程去持續(xù)的監(jiān)聽時(shí)間,就需要啟動(dòng)它的runloop并且忘其中注冊(cè)source,timer,oberserver三者之一的消息類型。在默認(rèn)情況下子線程的runloop是不會(huì)自己創(chuàng)建和啟動(dòng)的。

線程之間的通訊:NSMachPort

NSNotificationCenter是iOS中全局的觀察者,可以用于不同頁面之間消息傳遞解耦。

先看一段代碼:

代碼1

@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

輸出如下:

輸出

current thread = <NSThread: 0x7fbb23412f30>{number = 1, name = main}
current thread = <NSThread: 0x7fbb23552370>{number = 2, name = (null)}
test[865:45174] test notification

在主線程中注冊(cè)了一個(gè)通知,在子線程中拋出事件,最后在子線程中處理事件。

但是有些時(shí)候,可能需要在同一個(gè)線程中處理事件,比如更新UI的操作只能放到主線程中進(jìn)行。所以,需要做一次線程之間消息的轉(zhuǎn)發(fā)。如果是子線程往主線程轉(zhuǎn)發(fā),通過GCD即可實(shí)現(xiàn)。但是如果是任意兩個(gè)線程之間通訊,則需要依賴NSMachPort通過它往目標(biāo)線程的runloop中注冊(cè)事件來完成。

@interface ViewController () <NSMachPortDelegate>

@property (nonatomic) NSMutableArray    *notifications;         // 通知隊(duì)列
@property (nonatomic) NSThread          *notificationThread;    // 期望線程
@property (nonatomic) NSLock            *notificationLock;      // 用于對(duì)通知隊(duì)列加鎖的鎖對(duì)象,避免線程沖突
@property (nonatomic) NSMachPort        *notificationPort;      // 用于向期望線程發(fā)送信號(hào)的通信端口

@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;

    // 往當(dāng)前線程的run loop添加端口源
    // 當(dāng)Mach消息到達(dá)而接收線程的run loop沒有運(yùn)行時(shí),則內(nèi)核會(huì)保存這條消息,直到下一次進(jìn)入run loop
    [[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];

    });
}

//NSMacPort回調(diào)方法
- (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) {
        // Forward the notification to the correct thread.
        [self.notificationLock lock];
        [self.notifications addObject:notification];
        [self.notificationLock unlock];
        [self.notificationPort sendBeforeDate:[NSDate date]
                                   components:nil
                                         from:nil
                                     reserved:0];
    }
    else {
        // Process the notification here;
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSLog(@"process notification");
    }
}

@end

輸入如下:

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
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
禁止轉(zhuǎn)載,如需轉(zhuǎn)載請(qǐng)通過簡信或評(píng)論聯(lián)系作者。

相關(guān)閱讀更多精彩內(nèi)容

  • 深入淺出iOS多線程(一)——線程的概念深入淺出iOS多線程(二)——pthraed和NSThread的使用深入淺...
    struggle3g閱讀 661評(píng)論 1 2
  • iOS 開發(fā)高級(jí)進(jìn)階 第三周 多線程 Runloop iOS 多線程以及 RunLoop 學(xué)習(xí)總結(jié) 基礎(chǔ)知識(shí) 什么...
    varlarzh閱讀 648評(píng)論 0 1
  • 開啟線程 分離主線程創(chuàng)建:創(chuàng)建線程后會(huì)自動(dòng)執(zhí)行,但是線程外部不可獲取到該線程對(duì)象detachNewThreadWi...
    Mr_Pt閱讀 1,133評(píng)論 0 1
  • runtime 和 runloop 作為一個(gè)程序員進(jìn)階是必須的,也是非常重要的, 在面試過程中是經(jīng)常會(huì)被問到的, ...
    made_China閱讀 1,276評(píng)論 0 7
  • Runloop 和 線程 在CFRunloop中已經(jīng)說明了一個(gè)線程及其runloop的對(duì)應(yīng)關(guān)系,現(xiàn)在以iOS中NS...
    鬧鬼的金礦閱讀 863評(píng)論 0 51

友情鏈接更多精彩內(nèi)容