Cocoa深刻學習:NSOperationQueue、NSRunLoop和線程安全

http://blog.cnbluebox.com/blog/2014/07/01/cocoashen-ru-xue-xi-nsoperationqueuehe-nsoperationyuan-li-he-shi-yong/html

 

目前在 iOS 和 OS X 中有兩套先進的同步 API 可供咱們使用:NSOperation 和 GCD 。其中 GCD 是基於 C 的底層的 API ,而 NSOperation 則是 GCD 實現的 Objective-C API。 雖然 NSOperation 是基於 GCD 實現的, 可是並不意味着它是一個 GCD 的 「dumbed-down」 版本, 相反,咱們能夠用NSOperation 輕易的實現一些 GCD 要寫大量代碼的事情。 所以, NSOperationQueue 是被推薦使用的, 除非你遇到了 NSOperationQueue 不能實現的問題。ios

1. 爲何優先使用NSOperationQueue而不是GCD

曾經我有一段時間我很是喜歡使用GCD來進行併發編程,由於雖然它是C的api,可是使用起來卻很是簡單和方便, 不過這樣也就容易使開發者忘記併發編程中的許多注意事項和陷阱。c++

好比你可能寫過相似這樣的代碼(這樣來請求網絡數據):程序員

1
2 3 4 5 6 7 8 9 10 
dispatch_async(_Queue, ^{   //請求數據  NSData *data = [NSData dataWithContentURL:[NSURL URLWithString:@"http://domain.com/a.png"]];   dispatch_async(dispatch_get_main_queue(), ^{   [self refreshViews:data];  }); }); 

沒錯,它是能夠正常的工做,可是有個致命的問題:這個任務是沒法取消的 dataWithContentURL:是同步的拉取數據,它會一直阻塞線程直到完成請求,若是是遇到了超時的狀況,它在這個時間內會一直佔有這個線程;在這個期間併發隊列就須要爲其餘任務新建線程,這樣可能致使性能降低等問題。web

所以咱們不推薦這種寫法來從網絡拉取數據。編程

操做隊列(operation queue)是由 GCD 提供的一個隊列模型的 Cocoa 抽象。GCD 提供了更加底層的控制,而操做隊列則在 GCD 之上實現了一些方便的功能,這些功能對於 app 的開發者來講一般是最好最安全的選擇。NSOperationQueue相對於GCD來講有如下優勢:windows

  • 提供了在 GCD 中不那麼容易複製的有用特性。
  • 能夠很方便的取消一個NSOperation的執行
  • 能夠更容易的添加任務的依賴關係
  • 提供了任務的狀態:isExecuteing, isFinished.

名詞: 本文中提到的 「任務」, 「操做」 即表明要再NSOperation中執行的事情。 api

2. Operation Queues的使用

2.1 NSOperationQueue

NSOperationQueue 有兩種不一樣類型的隊列:主隊列和自定義隊列。主隊列運行在主線程之上,而自定義隊列在後臺執行。在兩種類型中,這些隊列所處理的任務都使用 NSOperation 的子類來表述。安全

1
2 3 4 5 6 
NSOperationQueue *mainQueue = [NSOperationQueue mainQueue]; //主隊列 NSOperationQueue *queue = [[NSOperationQueue alloc] init]; //自定義隊列 NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{  //任務執行  }]; [queue addOperation:operation]; 

咱們能夠經過設置 maxConcurrentOperationCount 屬性來控制併發任務的數量,當設置爲 1 時, 那麼它就是一個串行隊列。主對列默認是串行隊列,這一點和 dispatch_queue_t 是類似的。網絡

2.2 NSOperation

你可使用系統提供的一些現成的 NSOperation 的子類, 如 NSBlockOperationNSInvocationOperation 等(如上例子)。你也能夠實現本身的子類, 經過重寫 main 或者 start 方法 來定義本身的 operations 。

使用 main 方法很是簡單,開發者不須要管理一些狀態屬性(例如 isExecuting 和 isFinished),當 main 方法返回的時候,這個 operation 就結束了。這種方式使用起來很是簡單,可是靈活性相對重寫 start 來講要少一些, 由於main方法執行完就認爲operation結束了,因此通常能夠用來執行同步任務。

1
2 3 4 5 6 
@implementation YourOperation - (void)main {  // 任務代碼 ... } @end 

若是你但願擁有更多的控制權,或者想在一個操做中能夠執行異步任務,那麼就重寫 start 方法, 可是注意:這種狀況下,你必須手動管理操做的狀態, 只有當發送 isFinished 的 KVO 消息時,才認爲是 operation 結束

1
2 3 4 5 6 7 8 9 10 11 12 
@implementation YourOperation - (void)start {  self.isExecuting = YES;  // 任務代碼 ... } - (void)finish //異步回調 {  self.isExecuting = NO;  self.isFinished = YES; } @end 

當實現了start方法時,默認會執行start方法,而不執行main方法

爲了讓操做隊列可以捕獲到操做的改變,須要將狀態的屬性以配合 KVO 的方式進行實現。若是你不使用它們默認的 setter 來進行設置的話,你就須要在合適的時候發送合適的 KVO 消息。

須要手動管理的狀態有:

  • isExecuting 表明任務正在執行中
  • isFinished 表明任務已經執行完成
  • isCancelled 表明任務已經取消執行

手動的發送 KVO 消息, 通知狀態更改以下 :

1
2 3 
[self willChangeValueForKey:@"isCancelled"]; _isCancelled = YES; [self didChangeValueForKey:@"isCancelled"]; 

爲了能使用操做隊列所提供的取消功能,你須要在長時間操做中時不時地檢查 isCancelled 屬性, 好比在一個長的循環中:

1
2 3 4 5 6 7 8 9 
@implementation MyOperation  - (void)main {  while (notDone && !self.isCancelled) {  // 任務處理  } } @end 

3. RunLoop

在cocoa中講到多線程,那麼就不得不講到RunLoop。 在ios/mac的編碼中,咱們彷佛不須要過多關心代碼是如何執行的,一切彷彿那麼天然。好比咱們知道當滑動手勢時,tableView就會滾動,啓動一 個NSTimer以後,timer的方法就會定時執行, 可是爲何呢,實際上是RunLoop在幫咱們作這些事情:分發消息。

3.1 什麼是RunLoop

你應該看過這樣的僞代碼解釋ios的app中main函數作的事情:

1
2 3 4 5 6 
int main(int argc, char * argv[]) {  while (true) {  [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  } } 

也應該看過這樣的代碼用來阻塞一個線程:

1
2 3 
while (!complete) {  [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } 

或許你感受到他們有些神奇,但願個人解釋能讓你明白一些.

咱們先思考一個問題: 當咱們打開一個IOS應用以後,什麼也不作,這時候看起來是沒有代碼在執行的,爲何應用沒有退出呢?

咱們在寫c的簡單的只有一個main函數的程序時就知道,當main的代碼執行完,沒有事情可作的時候,程序就執行完畢退出了。而咱們IOS的應用是如何作到在沒有事情作的時候維持應用的運行的呢? 那就是RunLoop。

RunLoop的字面意思就是「運行迴路」,聽起來像是一個循環。實際它就是一個循環,它在循環監聽着事件源,把消息分發給線程來執行。RunLoop並非線程,也不是併發機制,可是它在線程中的做用相當重要,它提供了一種異步執行代碼的機制。

3.2 事件源

runloop

由圖中能夠看出NSRunLoop只處理兩種源:輸入源、時間源。而輸入源又能夠分爲:NSPort、自定義源、performSelector:OnThread:delay:, 下面簡單介紹下這幾種源:

3.2.1 NSPort 基於端口的源

Cocoa和 Core Foundation 爲使用端口相關的對象和函數建立的基於端口的源提供了內在支持。Cocoa中你從不須要直接建立輸入源。你只須要簡單的建立端口對象,並使用NSPort 的方法將端口對象加入到run loop。端口對象會處理建立以及配置輸入源。

NSPort通常分三種: NSMessagePort(基本廢棄)、NSMachPortNSSocketPort。 系統中的NSURLConnection就是基於NSSocketPort進行通訊的,因此當在後臺線程中使用NSURLConnection 時,須要手動啓動RunLoop, 由於後臺線程中的RunLoop默認是沒有啓動的,後面會講到。

3.2.2 自定義輸入源

在Core Foundation程序中,必須使用CFRunLoopSourceRef類型相關的函數來建立自定義輸入源,接着使用回調函數來配置輸入源。Core Fundation會在恰當的時候調用回調函數,處理輸入事件以及清理源。常見的觸摸、滾動事件等就是該類源,由系統內部實現。

通常咱們不會使用該種源,第三種狀況已經知足咱們的需求

3.2.3 performSelector:OnThread

Cocoa提供了能夠在任一線程執行函數(perform selector)的輸入源。和基於端口的源同樣,perform selector請求會在目標線程上序列化,減緩許多在單個線程上容易引發的同步問題。而和基於端口的源不一樣的是,perform selector執行完後會自動清除出run loop。

此方法簡單實用,使用也更普遍。

3.2.4 定時源

定時源就是NSTimer了,定時源在預設的時間點同步地傳遞消息。由於Timer是基於RunLoop的,也就決定了它不是實時的。

3.3 RunLoop觀察者

咱們能夠經過建立CFRunLoopObserverRef對象來檢測RunLoop的工做狀態,它能夠檢測RunLoop的如下幾種事件:

  • Run loop入口
  • Run loop將要開始定時
  • Run loop將要處理輸入源
  • Run loop將要休眠
  • Run loop被喚醒但又在執行喚醒事件前
  • Run loop終止

3.4 Run Loop Modes

RunLoop對於上述四種事件源的監視,能夠經過設置模式來決定監視哪些源。 RunLoop只會處理與當前模式相關聯的源,未與當前模式關聯的源則處於暫停狀態。

cocoa和Core Foundation預先定義了一些模式(Apple文檔翻譯):

Mode Name Description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 缺省狀況下,將包含全部操做,而且大多數狀況下都會使用此模式
Connection NSConnectionReplyMode (Cocoa) 此模式用於處理NSConnection的回調事件
Modal NSModalPanelRunLoopMode (Cocoa) 模態模式,此模式下,RunLoop只對處理模態相關事件
Event Tracking NSEventTrackingRunLoopMode (Cocoa) 此模式下用於處理窗口事件,鼠標事件等
Common Modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 此模式用於配置」組模式」,一個輸入源與此模式關聯,則輸入源與組中的全部模式相關聯。

咱們也能夠自定義模式,能夠參考ASIHttpRequest在同步執行時,自定義了 runLoop 的模式叫 ASIHTTPRequestRunLoopMode。ASI的Timer源就關聯了此模式。

3.5 常見問題一:爲何TableView滑動時,Timer暫停了?

咱們作個測試: 在一個 viewController 的 scrollViewWillBeginDecelerating: 方法裏面打個斷點, 而後滑動 tableView。 待斷點處, 使用 lldb 打印一下 [NSRunLoop currentRunLoop] 。 在描述中能夠看到當前的RunLoop的運行模式:

current mode = UITrackingRunLoopMode
common modes = <CFBasicHash 0x14656e60 [0x3944dae0]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x398d54c0 [0x3944dae0]>{contents = "UITrackingRunLoopMode"}
1 : <CFString 0x39449d10 [0x3944dae0]>{contents = "kCFRunLoopDefaultMode"}
}

也就是說,當前主線程的 RunLoop 正在以 UITrackingRunLoopMode 的模式運行。 這個時候 RunLoop 只會處理與 UITrackingRunLoopMode 「綁定」的源, 好比觸摸、滾動等事件;而 NSTimer 是默認「綁定」到 NSRunLoopDefaultMode 上的, 因此 Timer 是事情是不會被 RunLoop 處理的,咱們的看到的時定時器被暫停了!

常見的解決方案是把Timer「綁定」到 NSRunLoopCommonModes 模式上, 那麼Timer就能夠與:

1
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 

這樣這個Timer就能夠和當前組中的兩種模式 UITrackingRunLoopModekCFRunLoopDefaultMode 相關聯了。 RunLoop在這兩種模式下,Timer均可以正常運行了。

注意: 由上面能夠發現 NSTimer 是不許確的。 由於RunLoop只負責分發源的消息。若是線程當前正在處理繁重的任務,好比循環,就有可能致使Timer本次延時,或者少執行一次。網上有人作過實驗:

runloop_timer

上面的Log是一個間隔爲 1 s 的計時器,咱們能夠發如今 12.836s ~ 15.835s 之間的時間段內, 明顯的 13s 的方法沒有執行。 14s 的方法有所延遲。

所以當咱們用NSTimer來完成一些計時任務時,若是須要比較精確的話,最好仍是要比較「時間戳」。

3.6 常見問題二:後臺的NSURLConnection不回調,Timer不運行

咱們知道每一個線程都有它的RunLoop, 咱們能夠經過 [NSRunLoop currentRunLoop]CFRunLoopGetCurrent() 來獲取。 可是主線程和後臺線程是不同的。主線程的RunLoop是一直在啓動的。然後臺線程的RunLoop是默認沒有啓動的。

後臺線程的RunLoop沒有啓動的狀況下的現象就是:「代碼執行完,線程就結束被回收了」。就像咱們簡單的程序執行完就退出了。 因此若是咱們但願在代碼執行完成後還要保留線程等待一些異步的事件時,好比NSURLConnection和NSTimer, 就須要手動啓動後臺線程的RunLoop。

啓動RunLoop,咱們須要設定RunLoop的模式,咱們能夠設置 NSDefaultRunLoopMode。 那默認就是監聽全部時間源:

1
2 3 4 5 
//Cocoa [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  //Core Foundation CFRunLoopRun(); 

咱們也能夠設置其餘模式運行,可是咱們就須要把「事件源」 「綁定」到該模式上:

1
2 3 4 5 6 7 8 
//NSURLConnection [_connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]];  //Timer [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate distantFuture]]; 

3.7 問題三:本節開頭的例子爲什麼能夠阻塞線程

1
2 3 
while (!complete) {  [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; } 

你應該知道這樣一段代碼能夠阻塞當前線程,你可能會奇怪:RunLoop就是不停循環來檢測源的事件,爲何還要加個 while 呢?

這是由於RunLoop的特性,RunLoop會在沒有「事件源」可監聽時休眠。也就是說若是當前沒有合適的「源」被RunLoop監聽,那麼這步就跳過了,不能起到阻塞線程的做用,因此仍是要加個while循環來維持。

同時注意:由於這段代碼能夠阻塞線程,因此請不要在主線程寫下這段代碼,由於它極可能會致使界面卡住。

4. 線程安全

講了這麼多,你是否已經對併發編程已經躍躍欲試了呢? 可是併發編程一直都不是一個輕鬆的事情,使用併發編程會帶來許多陷阱。哪怕你是一個很成熟的程序員和架構師,也很難避免線程安全的問題;使用的越多,出錯的可能就越大,所以能夠不用多線程就不要使用。

關於併發編程的不可預見性有一個很是有名的例子:在1995年, NASA (美國宇航局)發送了開拓者號火星探測器,可是當探測器成功着陸在咱們紅色的鄰居星球后不久,任務嘎然而止,火星探測器莫名其妙的不停重啓,在計算機領域 內,遇到的這種現象被定爲爲優先級反轉,也就是說低優先級的線程一直阻塞着高優先級的線程。在這裏咱們想說明的是,即便擁有豐富的資源和大量優秀工程師的 智慧,併發也仍是會在很多狀況下反咬你你一口。

4.1 資源共享和資源飢餓

併發編程中許多問題的根源就是在多線程中訪問共享資源。資源能夠是一個屬性、一個對象,通用的內存、網絡設備或者一個文件等等。在多線程中任何一個共享的資源均可能是一個潛在的衝突點,你必須精心設計以防止這種衝突的發生。

通常咱們經過鎖來解決資源共享的問題,也就是能夠經過對資源加鎖保證同時只有一個線程訪問資源

4.1.1 互斥鎖

互斥訪問的意思就是同一時刻,只容許一個線程訪問某個特定資源。爲了保證這一點,每一個但願訪問共享資源的線程,首先須要得到一個共享資源的互斥鎖。 對資源加鎖會引起必定的性能代價。

4.1.2 原子性

從語言層面來講,在 Objective-C 中將屬性以 atomic 的形式來聲明,就能支持互斥鎖了。事實上在默認狀況下,屬性就是 atomic 的。將一個屬性聲明爲 atomic 表示每次訪問該屬性都會進行隱式的加鎖和解鎖操做。雖然最把穩的作法就是將全部的屬性都聲明爲 atomic,可是加解鎖這也會付出必定的代價。

4.1.3 死鎖

互斥鎖解決了競態條件的問題,但很不幸同時這也引入了一些其餘問題,其中一個就是死鎖。當多個線程在相互等待着對方的結束時,就會發生死鎖,這時程序可能會被卡住。

好比下面的代碼:

1
2 3 4 5 
dispatch_sync(_queue, ^{  dispatch_sync(_queue, ^{  //do something  }); }) 

再好比:

1
2 3 4 5 
main() { dispatch_sync(dispatch_get_main_queue(), ^{  //do something }); } 

上面兩個例子也能夠說明 dispatch_sync 這個API是危險的,因此儘可能不要用。

當你的代碼有死鎖的可能時,它就會發生

4.1.4 資源飢餓

當你認爲已經足夠了解併發編程面臨的問題時,又出現了一個新的問題。鎖定的共享資源會引發讀寫問題。大多數狀況下,限制資源一次只能有一個線程進行 讀取訪問實際上是很是浪費的。所以,在資源上沒有寫入鎖的時候,持有一個讀取鎖是被容許的。這種狀況下,若是一個持有讀取鎖的線程在等待獲取寫入鎖的時候, 其餘但願讀取資源的線程則由於沒法得到這個讀取鎖而致使資源飢餓的發生。

4.2 優先級反轉

優先級反轉是指程序在運行時低優先級的任務阻塞了高優先級的任務,有效的反轉了任務的優先級。GCD提供了3種級別的優先級隊列,分別是 Default, High, Low。 高優先級和低優先級的任務之間共享資源時,就可能發生優先級反轉。當低優先級的任務得到了共享資源的鎖時,該任務應該迅速完成,並釋放掉鎖,這樣高優先級 的任務就能夠在沒有明顯延時的狀況下繼續執行。然而高優先級任務會在低優先級的任務持有鎖的期間被阻塞。若是這時候有一箇中優先級的任務(該任務不須要那 個共享資源),那麼它就有可能會搶佔低優先級任務而被執行,由於此時高優先級任務是被阻塞的,因此中優先級任務是目前全部可運行任務中優先級最高的。此 時,中優先級任務就會阻塞着低優先級任務,致使低優先級任務不能釋放掉鎖,這也就會引發高優先級任務一直在等待鎖的釋放。以下圖:

使用不一樣優先級的多個隊列聽起來雖然不錯,但畢竟是紙上談兵。它將讓原本就複雜的並行編程變得更加複雜和不可預見。所以咱們寫代碼的時候最好只用Default優先級的隊列,不要使用其餘隊列來讓問題複雜化。

關於dispatch_queue的底層線程安全設計可參考:底層併發 API

5. 總結

本文主要講了 NSOperationQueue、 NSRunLoop、 和線程安全等三大塊內容。 但願能夠幫助你理解 NSOperation的使用, NSRunLoop的做用, 還有併發編程帶來的複雜性和相關問題。

併發其實是一個很是棒的工具。它充分利用了現代多核 CPU 的強大計算能力。可是由於它的複雜性,因此咱們儘可能使用高級的API,儘可能寫簡單的代碼,讓併發模型保持簡單; 這樣能夠寫出高效、結構清晰、且安全的代碼。

 

 

NSRunLoop的進一步理解

http://www.devdiv.com/nsrunloop_-article-2360-1.html

iPhone應用開發中關於NSRunLoop的概述是本文要介紹的內容,NSRunLoop是一種更加高明的消息處理模式,他就高明在對消息處理過程進 行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理,在NSRunLoop中每個消息就被打包在input source或者是timer source中了,來看詳細內容。

1.什麼是NSRunLoop

咱們會常常看到這樣的代碼:

  1. - (IBAction)start:(id)sender  
  2. {  
  3. pageStillLoading = YES;  
  4. [NSThread detachNewThreadSelector:@selector(loadPageInBackground:)toTarget:self withObject:nil];  
  5. [progress setHidden:NO];  
  6. while (pageStillLoading) {  
  7. [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  
  8. }  
  9. [progress setHidden:YES];  
  10. }  
複製代碼

這 段代碼很神奇的,由於他會「暫停」代碼運行,並且程序運行不會由於這裏有一個while循環而受到影響。在[progress setHidden:NO]執行以後,整個函數想暫停了同樣停在循環裏面,等loadPageInBackground裏面的操做都完成了之後才讓 [progress setHidden:YES]運行。這樣作就顯得簡介,並且邏輯很清晰。若是你不這樣作,你就須要在loadPageInBackground裏面表示 load完成的地方調用[progress setHidden:YES],顯得代碼不緊湊並且容易出錯。
[iGoogle有話說:應用程序框架主線程已經封裝了對NSRunLoop runMode:beforeDate:的調用;它和while循環構成了一個消息泵,不斷獲取和處理消息;可能你們會比較奇怪,既然主線程中已經封裝好 了對NSRunLoop的調用,爲何這裏還能夠再次調用,這個就是它與Windows消息循環的區別,它能夠嵌套調用.當再次調用 while+NSRunLoop時候程序並無中止執行,它還在不停提取消息/處理消息.這一點與Symbian中Active Scheduler的嵌套調用達到同步做用原理是同樣的.]

那麼具體什麼是NSRunLoop呢?其實NSRunLoop的本質是一個消息機制的處理模式。若是你對vc++編程有必定了解,在windows中,有 一系列很重要的函數SendMessage,PostMessage,GetMessage,這些都是有關消息傳遞處理的API。

可是在你進入到Cocoa的編程世界裏面,我不知道你是否是走的太快太匆忙而忽視了這個很重要的問題,Cocoa裏面就沒有說起到任何關於消息處理的 API,開發者歷來也沒有本身去關心過消息的傳遞過程,好像一切都是那麼天然,像大天然同樣天然?在Cocoa裏面你不再用去本身定義 WM_COMMAD_XXX這樣的宏來標識某個消息,也不用在switch-case裏面去對特定的消息作特別的處理。難道是Cocoa裏面就沒有了消息 機制?答案是否認的,只是Apple在設計消息處理的時候採用了一個更加高明的模式,那就是RunLoop。

2. NSRunLoop工做原理

接下來看一下NSRunLoop具體的工做原理,首先是官方文檔提供的說法,看圖:
1008450.png
經過全部的「消息」都被添加到了NSRunLoop中去,而在這裏這些消息並分爲「input source」和「Timer source」 並在循環中檢查是否是有事件須要發生,若是須要那麼就調用相應的函數處理。爲了更清晰的解釋,咱們來對比VC++和iOS消息處理過程。

VC++中在一切初始化都完成以後程序就開始這樣一個循環了(代碼是從戶sir mfc程序設計課程的slides中截取):

  1. int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPSTR  lpCmdLine,int nCmdShow){  
  2. ...  
  3. while (GetMessage(&msg, NULL, 0, 0)){  
  4. if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg)){  
  5. TranslateMessage(&msg);  
  6. DispatchMessage(&msg);  
  7. }  
  8. }  
  9. }  
複製代碼

能夠看到在GetMessage以後就去分發處理消息了,而iOS中main函數中只是調用了UIApplicationMain,那麼咱們能夠介意猜出UIApplicationMain在初始化完成以後就會進入這樣一個情形:

  1. int UIApplicationMain(...){  
  2. ...  
  3. while(running){  
  4. [NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];  
  5. }  
  6. ...  
  7. }
複製代碼

所 以在UIApplicationMain中也是一樣在不斷處理runloop纔是的程序沒有退出。剛纔的我說了NSRunLoop是一種更加高明的消息處 理模式,他就高明在對消息處理過程進行了更好的抽象和封裝,這樣才能是的你不用處理一些很瑣碎很低層次的具體消息的處理,在NSRunLoop中每個消 息就被打包在input source或者是timer source中了,當須要處理的時候就直接調用其中包含的相應對象的處理函數了。

因此對外部的開發人員來說,你感覺到的就是,把source/timer加入到runloop中,而後在適當的時候相似於[receiver action]這樣的事情發生了。甚至不少時候,你都沒有感覺到整個過程前半部分,你只是感受到了你的某個對象的某個函數調用了。

好比在UIView被觸摸時會用touchesBegan/touchesMoved等等函數被調用,也許你會想,「該死的,我都不知道在那裏被告知有觸 摸消息,這些處理函數就被調用了!?」因此,消息是有的,只是runloop已經幫你作了!爲了證實個人觀點,我截取了一張debug touchesBegan的call stack,有圖有真相,如圖:
1008451.png 如今會過頭來看看剛纔的那個會「暫停」代碼的例子,有沒有更加深刻的認識了呢?

相關文章
相關標籤/搜索