Runloop和多線程

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

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

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

然后就發現網絡回調被調用了。

之后分析了一下調用堆棧:

第一個:在multiThread里面是這樣的:

multiThread.png

第二個:網絡回調里面是這樣的:

網絡回調.png

通過堆棧可以得知,這兩個函數都是由線程6調用的,也就是創建的子線程,也就是創建的子線程,但是堆棧中的內容很不一樣。很顯然第二個是從runloop 調出的,并且是Sources0這個消息調出的。而第一個是線程運行時候的初始化方法。所以當調用runloop run的時候,其實是線程進入自己的runloop去監聽時間了,從此以后,所有的代碼都會從runloop CALLOUT出來。所以這種情況下,需要把先把消息注冊到runloop中,讓runloop跑起來是最后需要做的事情。

以下是開源庫AFNetworking網絡請求的實現:

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

這個方法,但是為什么它沒有使用

- (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的消息,如下截圖:

currentRunloop.png

也就是說父線程在

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

注冊成source0,這是該函數背后的大致實現。通過查閱apple官方文檔,基本屬實,如下所示:

官方文檔.png

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

方法,那么子線程runloop的初始化和業務邏輯就會混到一起,并且每一次都會重新初始化。AFNetworking通過一個靜態全局的子線程去管理所有的網絡請求,其對應的runloop也只需要初始化一次。

通過以上分析,可以知道如果需要讓一個子線程去持續的監聽時間,就需要啟動它的runloop并且忘其中注冊source,timer,oberserver三者之一的消息類型。在默認情況下子線程的runloop是不會自己創建和啟動的。

線程之間的通訊: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

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

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

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

    // 往當前線程的run loop添加端口源
    // 當Mach消息到達而接收線程的run loop沒有運行時,則內核會保存這條消息,直到下一次進入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回調方法
- (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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
禁止轉載,如需轉載請通過簡信或評論聯系作者。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容