RunLoop

1.RunLoop的概念

RunLoop其實就是一個大的do while循環(huán),它的關鍵點在于如何管理事件/消息泼返,如何讓線程在沒有處理消息時休眠以避免資源占用,在有消息到來時立刻被喚醒宝恶。所以RunLoop實際上是一個對象符隙,這個對象管理了其需要處理的時間和消息,并提供了一個函數(shù)來執(zhí)行上面的事件邏輯垫毙。因此runLoop可以說就是為了線程而生霹疫。

2.RunLoop 的作用:

1.使程序一直運行并接受用戶輸入
2.決定程序在何時應該處理哪些事件
3.調用解耦(事件隊列的分發(fā)與派放)
4.節(jié)省CPU時間
5.RunLoop也負責autorelease pool的創(chuàng)建與釋放(當一個運行循環(huán)結束或者RunLoop退出和休眠的時候,它都會釋放一次autorelease pool)

其中RunLoop運行一次的時間為1/60 S

與Runloop最密切相關的:NSTimer 综芥、UIEvent 丽蝎、Autorelease

CFRunLoop是基于pthread來管理的。蘋果不允許直接創(chuàng)建RunLoop,它只提供了兩個自動獲取的函數(shù)CFRunLoopGetMain() 和 CFRunLoopGetCurrent()膀藐,當線程中沒有RunLoop的時候屠阻,CFRunLoopGetCurrent()其實會創(chuàng)建一個RunLoop對象并返回。

3.RunLoop的結構

RunLoop接受事件來自兩種不同的來源:輸入源和定時源额各,輸入源傳遞異步事件国觉,通常消息來自其他線程和程序,定時源則傳遞同步事件虾啦,發(fā)生在特定時間或者重復的時間間隔麻诀。兩種源都使用程序的某一特定處理例程來處理到達的事件。輸入源包括兩種傲醉,分別是基于端口的輸入源和自定義輸入源蝇闭。基于端口的輸入源監(jiān)聽程序相應的輸入源硬毕,自定義輸入源則監(jiān)聽自定義的事件源呻引。基于端口的輸入源由內核發(fā)送吐咳,而自定義的輸入源需要人工從其他線程發(fā)送逻悠。

RunLoop由線程和Mode組成,線程和RunLoop之間是一一對應的關系韭脊,其關系是保存在一個字典里面蹂风,線程剛創(chuàng)建時并沒有RunLoop,如果你不主動獲取那么它一直不會有乾蓬。RunLoop的創(chuàng)建是發(fā)生在第一次獲取時惠啄,RunLoop的銷毀是發(fā)生在線程結束時。主線程的RunLoop默認是開啟的任内,當程序在運行的時候會產生大量的對象撵渡,這些對象存儲在RunLoop的釋放池里面,當RunLoop循環(huán)完一次之后會釋放自動釋放池同時創(chuàng)建新的自動釋放池死嗦。子線程沒有開啟RunLoop需要手動獲取趋距,因為子線程的RunLoop是手動獲取的,所以自動釋放池默認也沒有越除,當我們在子線程里面創(chuàng)建了大量的臨時對象的時候就需要創(chuàng)建自動釋放池节腐。

一個RunLoop包含若干個RunLoopMode,但是一個RunLoop每次只能加入一種Mode,每一個Mode里面包含若干個source/timer/observer外盯。每次調用RunLoop的主函數(shù)時只能指定其中一個Mode,這個Mode被稱為currentMode,如果需要切換Mode只能退出RunLoop然后再重新指定另外的Mode進入。這樣做主要是為了分開不同組的source/timer/observer讓其不受影響翼雀。

Run loop模式是所有要監(jiān)視的輸入源和定時源以及要通知的run loop注冊觀察者的集合饱苟。每次運行你的run loop,你都要指定(無論顯示還是隱式)其運行個模式狼渊。在run loop運行過程中箱熬,只有和模式相關的源才會被監(jiān)視并允許他們傳遞事件消息。(類似的狈邑,只有和模式相關的觀察者會通知run loop的進程)城须。和其他模式關聯(lián)的源只有在run loop運行在其模式下才會運行,否則處于暫停狀態(tài)米苹。

4.RunLoop的特點

RunLoop在同一段時間只能且必須在一種特定的Mode下run
更換Mode時糕伐,需要停止當前Loop,然后重啟Loop
Mode是iOS App滑動順暢的關鍵
當傳入一個新的mode name 但是RunLoop內部沒有對應的mode時,RunLoop會幫你創(chuàng)建對應的RunLoopMode

5. RunLoopSource

CFRunLoopSourceRef 是事件產生的地方蘸嘶。Souce是RunLoop的數(shù)據(jù)抽象類赤炒。Source有兩個版本:Source0 和 Source1。
· Source0 只包含了一個回調(函數(shù)指針)亏较,它并不能主動觸發(fā)事件莺褒。使用時,你需要先調用 CFRunLoopSourceSignal(source)雪情,將這個 Source 標記為待處理遵岩,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件巡通。(Souce0處理App內部事件尘执,App自己負責管理觸發(fā),如UIEvent,CFSocket)
· Source1 包含了一個 mach_port 和一個回調(函數(shù)指針)宴凉,被用于通過內核和其他線程相互發(fā)送消息誊锭。這種 Source 能主動喚醒 RunLoop 的線程。(Souce1由RunLoop和內核管理弥锄,如CFMach,CFMessage)

其實可以簡單的理解為RunLoop通常處理的事件源有兩大種類丧靡,分別是time souce和input source,input source是異步消息通常來自其他線程或者程序。time source是timer中的同步事件

6.RunLoopTimer

CFRunLoopTimerRef 是基于時間的觸發(fā)器籽暇,它和 NSTimer 是toll-free bridged 的温治,可以混用。其包含一個時間長度和一個回調(函數(shù)指針)戒悠。當其加入到 RunLoop 時熬荆,RunLoop會注冊對應的時間點,當時間點到時绸狐,RunLoop會被喚醒以執(zhí)行那個回調卤恳。(NSTimer受RunLoop的Mode影響累盗,GCD的定時器不受RunLoop的Mode影響)

7.RunLoopObserver

CFRunLoopObserverRef 是觀察者,(它向外部報告RunLoop當前狀態(tài)的更改)每個 Observer 都包含了一個回調(函數(shù)指針)突琳,當 RunLoop 的狀態(tài)發(fā)生變化時若债,觀察者就能通過回調接受到這個變化”窘瘢可以觀測的時間點有以下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item,一個 item 可以被同時加入多個 mode主巍。但一個 item 被重復加入同一個 mode 時是不會有效果的冠息。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出孕索,不進入循環(huán)逛艰。

UIKit通過RunLoopObserver在RunLoop兩次Sleep間對AutreleasePool進行push和pop,將這次Loop中產生的Autorelease對象釋放。

8.CFRunLoopMode 和 CFRunLoop 的結構

CFRunLoopMode 和 CFRunLoop 的結構大致如下:
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
從上面可以看出CFRunLoop里面有一個commonModes,它是一個set集合搞旭。系統(tǒng)中共有5個Mode,每一個Mode可以將自己標記為Common屬性(通過將其ModeName屬性添加到RunLoop的commonModes中)散怖。每當RunLoop的內部發(fā)生變化時,RunLoop都會將commonModeItems里面的Souce/Observer/Timer同步到具有Common標記的所有Mode里肄渗。

系統(tǒng)默認注冊了5個Mode(前兩個跟最后一個常用)
? kCFRunLoopDefaultMode:App的默認Mode镇眷,通常主線程是在這個Mode下運行(NSTimer scheduledTimerWithTime這個方法默認加入的就是KCFRunLoopDefaultMode)
? UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動翎嫡,保證界面滑動時不受其他 Mode 影響
? UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode欠动,啟動完成后就不再使用
? GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內部 Mode,通常用不到
? kCFRunLoopCommonModes:這個Mode其實包含了第一個和第二個Mode

GCD中的任務隊列被分配到main queue的block會被分發(fā)到main RunLoop中執(zhí)行惑申。

當RunLoop掛起的時候會指定用于喚醒mach_port的端口具伍。同時會調用mach_msg監(jiān)聽喚醒端口,被喚醒前圈驼,系統(tǒng)會將這個線程掛起人芽,停留在mach_msg_trap狀態(tài)

由另一個線程或者另一個進程中的某個線程向內核發(fā)送這個端口的msg后,trap狀態(tài)被喚醒绩脆,runLoop繼續(xù)開始干活

CFRunLoop的默認超時時間很長

AutoreleasePool
App啟動后萤厅,蘋果在主線程RunLoop里注冊了兩個Observer,其回調都是_wrapRunLoopWithAutoreleasePoolHandler()。

第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)靴迫,其回調內會調用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池祈坠。其 order 是-2147483647,優(yōu)先級最高矢劲,保證創(chuàng)建釋放池發(fā)生在其他所有回調之前赦拘。

第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入休眠) 時調用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調用 _objc_autoreleasePoolPop() 來釋放自動釋放池芬沉。這個 Observer 的 order 是 2147483647躺同,優(yōu)先級最低阁猜,保證其釋放池子發(fā)生在其他所有回調之后。

在主線程執(zhí)行的代碼蹋艺,通常是寫在諸如事件回調剃袍、Timer回調內的。這些回調會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著捎谨,所以不會出現(xiàn)內存泄漏民效,開發(fā)者也不必顯示創(chuàng)建 Pool 了。

9.定時器與RunLoop的關系:

NSTimer 其實就是 CFRunLoopTimerRef涛救,他們之間是 toll-free bridged 的畏邢。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件检吆。例如 10:00, 10:10, 10:20 這幾個時間點舒萎。RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調這個Timer蹭沛。Timer 有個屬性叫做 Tolerance (寬容度)臂寝,標示了當時間點到后,容許有多少最大誤差摊灭。

如果某個時間點被錯過了咆贬,例如執(zhí)行了一個很長的任務,則那個時間點的回調也會跳過去帚呼,不會延后執(zhí)行素征。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交萝挤,那我只能等 10:20 這一趟了御毅。

PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中怜珍。所以如果當前線程沒有 RunLoop端蛆,則這個方法會失效。

當調用 performSelector:onThread: 時酥泛,實際上其會創(chuàng)建一個 Timer 加到對應的線程去今豆,同樣的,如果對應線程沒有 RunLoop 該方法也會失效柔袁。

10.何時使用RunLoop

我們知道當我們的程序啟動的時候呆躲,主線程已經默認創(chuàng)建了一個runLoop,所以只有在二級線程中我們才有機會創(chuàng)建runLoop。RunLoop的主要作用是為了幫助線程常駐進程捶索,所以僅當在為你的程序創(chuàng)建輔助線程的時候插掂,你才需要顯式運行一個run loop。對于輔助線程,你需要判斷一個run loop是否是必須的辅甥。如果是必須的酝润,那么你要自己配置并啟動它。你不需要在任何情況下都去啟動一個線程的run loop璃弄。比如要销,你使用線程來處理一個預先定義的長時間運行的任務時,你應該避免啟動run loop夏块。Run loop在你要和線程有更多的交互時才需要疏咐,比如以下情況:
使用端口或自定義輸入源來和其他線程通信
在線程中執(zhí)行定時事件源的任務
Cocoa中使用任何performSelector...的方法
在線程中執(zhí)行較為頻繁的,周期性的任務
如果你決定在程序中使用run loop脐供,那么它的配置和啟動都很簡單浑塞。和所有線程編程一樣,你需要計劃好在輔助線程退出線程的情形患民。讓線程自然退出往往比強制關閉它更好缩举。

11.線程笨寻穑活

在AFN中匹颤,把網(wǎng)絡的請求和解析都放在了一個子線程中,就是下面這段代碼

+ (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;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

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

這段代碼用單例創(chuàng)建了一個線程同時將線程加入了RunLoop中托猩,這是AFN中用來線程庇”停活的方法。這里為什么要加入RunLoop是因為我們創(chuàng)建的線程是脫離線程京腥,默認在執(zhí)行完任務之后就會被系統(tǒng)回收赦肃,為了讓線程一直存活下去必須讓它加入RunLoop.至于RunLoop為什么要調用addport forMode方法是因為如果RunLoop里面沒有任何的modelItem的話,RunLoop會直接退出公浪。

我們可以試著來仿照AFN中的線程彼穑活來仿寫一段代碼:

-(void)threadTest
{
    for (int i = 0; i < 100000; i ++) {
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(addToRunLoop) object:nil];
        [thread start];
    }
}

-(void)addToRunLoop
{
    NSLog(@"test");
    [[NSThread currentThread]setName:@"test"];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}

當我們運行程序的時候我們會發(fā)現(xiàn),內存在不斷的上漲欠气,同時控制臺會輸出[NSThread start]: Thread creation failed with error 35這個錯誤厅各。我們嘗試把addToRunLoop這個方法里面的代碼封掉,發(fā)現(xiàn)程序運行正常并且內存并不會一直上漲预柒,那么可以猜測队塘,是因為線程加入了RunLoop導致了線程不能銷毀因此內存上漲。

那么我們取消RunLoop和線程宜鸯,那么看看會有什么變化呢:

-(void)threadTest
{
    for (int i = 0; i < 1000000; i ++) {
        NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(addToRunLoop) object:nil];
        [thread start];
        [self performSelector:@selector(stopRunLoopAndThread) onThread:thread withObject:nil waitUntilDone:YES];
    }
}
-(void)addToRunLoop
{
    NSLog(@"test");
    [[NSThread currentThread]setName:@"test"];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}
-(void)stopRunLoopAndThread
{
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSThread *thread = [NSThread currentThread];
    [thread cancel];
}

運行程序發(fā)現(xiàn)內存還是在增長憔古,并且控制臺也會輸出[NSThread start]: Thread creation failed with error 35這個錯誤,看來我們沒有正確的取消RunLoop淋袖。

RunLoop的啟動方式:
(1)run (直接進入鸿市,但會使線程進入死循環(huán)從而不利于控制RunLoop,結束RunLoop的唯一方式就是kill它)

(2)runUntilDate(RunLoop會在處理完事件或者超時時間后結束)

(3)runMode:beforeDate: (指定RunLoop的超時時間以及運行在何種模式下)

runMode:beforeDate:是單次調用,其他兩種是循環(huán)調用runMode:beforeDate:方法灸芳。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末涝桅,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子烙样,更是在濱河造成了極大的恐慌冯遂,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谒获,死亡現(xiàn)場離奇詭異蛤肌,居然都是意外死亡,警方通過查閱死者的電腦和手機批狱,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門裸准,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人赔硫,你說我怎么就攤上這事炒俱。” “怎么了爪膊?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵权悟,是天一觀的道長。 經常有香客問我推盛,道長峦阁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任耘成,我火速辦了婚禮榔昔,結果婚禮上,老公的妹妹穿的比我還像新娘瘪菌。我一直安慰自己撒会,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布师妙。 她就那樣靜靜地躺著诵肛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪疆栏。 梳的紋絲不亂的頭發(fā)上曾掂,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音壁顶,去河邊找鬼珠洗。 笑死,一個胖子當著我的面吹牛若专,可吹牛的內容都是我干的许蓖。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼膊爪!你這毒婦竟也來了自阱?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤米酬,失蹤者是張志新(化名)和其女友劉穎沛豌,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赃额,經...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡加派,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了跳芳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芍锦。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖飞盆,靈堂內的尸體忽然破棺而出娄琉,到底是詐尸還是另有隱情,我是刑警寧澤吓歇,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布孽水,位于F島的核電站,受9級特大地震影響照瘾,放射性物質發(fā)生泄漏匈棘。R本人自食惡果不足惜丧慈,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一析命、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧逃默,春花似錦鹃愤、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至吟税,卻和暖如春凹耙,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肠仪。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工肖抱, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人异旧。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓意述,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子荤崇,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354

推薦閱讀更多精彩內容