編程最怕的就是有盲點,不確定,而runloop官網對其提及的又很少;那么看完這篇應該使你有底氣很多~
RunLoop整體介紹
An event-processing loop, during which events are received and dispatched to appropriate handlers.
事件運行循環:就類似下面的while循環部分,當然要復雜很多,可以把它抽象成如下代碼:
main() {
initialize();
do {
message = get_next_message();
process_message(message);
} while (message != quit);
}
“消息”循環,等待消息(會休眠)->接收消息->處理消息。通過上面的代碼,runloop本質就是提供了一種消息處理模式,只不過它封裝抽象的太好了(一般開發的時候根本就感覺不到,或者說不用關心)。
runloop相當於幫我們打包了各種消息,並將消息發送給指定的接受者。
可以將runloop理解為一個函數,功能是一個消息循環,有消息則處理,沒有消息則休眠。(注意:runloop實質是一個對象,但是不影響以上的假設)
簡單使用:新建一個線程,添加一個定時器,然后運行即可
- (void)timerFire {
NSLog(@"mode:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)runLoopTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(modeTestTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerFire forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}
如果你接觸過嵌入式操作系統(純內核)開發,那么對下面代碼肯定很熟悉
void ledTask (void *p_arg)
{
initialize();
while (1) {
LED_ON();
delay_ms(500);
LED_OFF();
delay_ms(500);
};
}
LED閃爍線程,讓一個LED燈1HZ的頻率閃爍,功能很簡單:首先初始化,然后進入while(1)死循環,延遲函數會使線程進入休眠(節省CPU)。直到程序死掉線程結束。是否和runloop很相似?
RunLoop消息類型(事件源)
一句話概括:很復雜,各種各樣 :)
不過,根據上圖我們可以將消息分為二種類型,第一種類型又可以細分為三種,此三種共同點就是它們都是異步執行的
- Port:
監聽程序的Mach ports,Mach ports是一個比較底層的東西,可以簡單的理解為:內核通過port這種方式將信息發送,而mach則監聽內核發來的port信息,然后將其整理,打包發給runloop。
- Customer:
很明顯,由開發人員自己發送。不僅僅是發送,過程的話相當復雜,蘋果也提供了一個CFRunLoopSource來幫助處理。由於很少用到,可以簡單說下核心,但是對幫助我們理解runloop卻很有幫助:
- 定義輸入源(數據結構)
- 將輸入源添加到runloop,那么這樣就有了接受者,即為R1
- 協調輸入源的客戶端(單獨線程),專門監聽消息,然后將消息打包成runloop能夠處理的樣式,即第一步定義的輸入源。它類似Mach的功能
- 誰來發送消息的問題?上面的machport是由內核發送的。自定義的當然要我們自己發送了。。。首先必須是另一個線程來發送(當然如果只是測試的話可以和第三步在同一個線程),先發送消息給輸入源,然后喚醒R1,因為R1一般處於休眠狀態,然后R1根據輸入源來做相應的處理
- Selector Sources:
NSObject類提供了很多方法供我們使用,這些方法是添加到runloop的,所以如果沒有開啟runloop的話,不會運行(不過有個坑,請看下面介紹)。
/// 主線程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
/// 指定線程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
/// 針對當前線程
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
/// 取消,在當前線程,和上面兩個方法對應
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:
下面提供的方法是在指定的線程運行aSelector
,一般情況下aSelector
會添加到指定線程的runloop。但,如果調用線程和指定線程為同一線程,且wait
參數設為YES,那么aSelector
會直接在指定線程運行,不再添加到runloop。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
其實這也很好理解,假設這種情況也添加到指定線程的runloop,我們可以這樣反向理解:1,當前線程runloop還沒有開啟,那么aSelector
就不會被執行,然而你卻一直在等待,造成線程卡死。2,當前線程runloop已經開啟,那么調用performSelector
這個方法的位置肯定是處於runloop的callout方法里面,在這里等待runloop再callout從而調用aSelector
方法完成,顯然也是死等待,線程卡死。。。
還有一些performSelector
方法,是不會添加到runloop的,而是直接執行,可以按照上面的特殊情況進行理解。方法列舉如下:
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
看到這里,是否感覺有些亂???只要記住沒有延遲或者等待的都不會添加到runloop,有延遲或者等待的還有排除上面提到的特殊情況。
-
Timer Sources:它的事件發送是同步的,這個用的比較多,會在下一篇專門介紹
-
Observers,觀察者:首先它並不屬於事件源(不會影響runloop的生命周期),它比較特殊,用於觀察runloop自身的一些狀態的,有以下幾種:
- 進入runloop
- runloop即將執行定時器
- runloop即將執行輸入源(Port,Customer,Selector Sources)
- runloop即將休眠
- runloop被喚醒,在處理完喚醒它的事件之前
- 退出
下面舉例,監聽所有狀態,在非主線程(可以看到一個完整的周期):
+ (void)observerTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
/**
param1: 給observer分配存儲空間
param2: 需要監聽的狀態類型:kCFRunLoopAllActivities監聽所有狀態
param3: 是否每次都需要監聽,如果NO則一次之后就被銷毀,不再監聽,類似定時器的是否重復
param4: 監聽的優先級,一般傳0
param5: 監聽到的狀態改變之后的回調
return: 觀察對象
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即將進入runloop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理input Sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將睡眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"從睡眠中喚醒,處理完喚醒源之前");
break;
case kCFRunLoopExit:
NSLog(@"退出");
break;
default:
break;
}
});
// 沒有任何事件源則不會進入runloop
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:NO];
CFRunLoopAddObserver([[NSRunLoop currentRunLoop] getCFRunLoop], observer, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] run];
});
}
+ (void)doFireTimer {
NSLog(@"---fire---");
}
打印結果:一個完整的周期
RunLoop模式
runloop的模式,使得runloop顯得更加靈活,適應更多的應用場景。
上面提到的事件源,都是處於特定的模式下的,如果和當前runloop的模式不一致則不會得到響應,舉個例子:
如果定時器處於mode1,而runloop運行在mode2,則定時器不會觸發,只有runloop運行在mode1時,定時器才會觸發。
系統為我們提供了多種模式,下面列一些比較常遇到的:
- kCFRunLoopDefaultMode: App的默認 Mode,通常主線程是在這個 Mode 下運行的。
- UITrackingRunLoopMode: 界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響。
- UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。
- NSRunLoopCommonModes: 包含了多種模式:default, modal, 和tracking modes。
除了系統給我們的模式,我們自己也可以自定義。
NSRunLoopMode
的類型為字符串類型,定義:typedef NSString * NSRunLoopMode
,自定義類型就很簡單了,示例代碼如下:直接調用runLoopModeTest方法即可測試
- (void)modeTestTimer {
NSLog(@"mode:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
/// 這里使用非主線程,主要考慮如果一直處於customMode模式,則主線癱瘓
- (void)runLoopModeTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(modeTestTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:@"customMode"];
[[NSRunLoop currentRunLoop] runMode:@"customMode" beforeDate:[NSDate distantFuture]];
});
}
runloop模式的切換
- 對於非主線程,我們可以退出當前模式,然后再進入另一個模式,也可以直接進入另一個模式,即嵌套
- 對於主線程,我們當然也可以像上面一樣操作,但是主線程有其特殊性,有很多系統的事件。系統會做一些切換,我們更關心的是系統是如何切換的?系統切換模式時,並沒有使用嵌套
主線程沒有使用runloop嵌套是根據我的測試得出,沒辦法,官方文檔太太太少,也沒有更底層源碼,只有CFRunLoop的源碼:http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz。
根據以上
最后總結下,thread--runloop--mode--event sources,關系可以表示如下:
RunLoop生命周期
可以分為三步:創建->運行(開啟,內部循環)->退出
1. runloop創建
蘋果是不允許開發人員手動創建runloop,runloop是伴隨着線程的創建而創建,線程與runloop是一一對應的,具有唯一性,另外創建還區分是否為主線程
-
主線程:系統會自動創建
-
非主線程:系統不會自動創建,開發人員必須顯示的調用
[NSRunLoop currentRunLoop]
方法來獲取runloop的時候,系統才會創建,類似懶加載
系統只提供了兩種方法獲取runloop,currentRunLoop
和mainRunLoop
,可以看出非主線程只有在自己的線程內才能獲得runloop。
2. runloop運行
- 開啟:主線程系統會自動運行,那么非主線程也是需要開發人員顯式調用的,可以通過如下方法
NSRunLoop提供的方法:
- (void)run; // 默認模式
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
CFRunLoop提供的函數:
/// 默認模式
void CFRunLoopRun(void);
/// 在指定模式,指定時間,運行
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
當執行了上面的運行方法后,如果runloop所在的模式沒有對應的事件源,即上面圖中提到的input sources、timer sources,會直接退出當前runloop(注意:是當前)。另外注意的是,input sources里面的Selector Sources,它有一些特殊情況,上面也提到了。這些情況下runloop還是會直接退出。
網上有很多說到事件源包括了observe,其實是不包含的,即runloop是否退出與observe沒有關系,observe只是監聽runloop本身的狀態而已。
- 內部循環(略復雜)
這樣看起來還是比較清晰的。
關於自動釋放池提一下(下一篇會做詳細說明):
- 第1步的觀察者(優先級較高)會創建自動釋放池
- 第6步的觀察者,會銷毀老的自動釋放池,並創建新的自動釋放池,對於一個runloop來說,此步驟會不斷的循環
- 第10步的觀察者,銷毀自動釋放池
上面提到的自動釋放池的處理當然是系統幫我們處理的,非主線程和主線程系統都幫我們做了處理。官方說到,如果你使用POSIX thread APIs創建線程,那就是另外一套內存回收系統了,是不會用autoreleasePool,系統當然也不會創建。
3. runloop退出
可以用以下方式退出runloop
- 設置最大時間到期:推薦使用這種方式
- modeItem(事件源)為空:但並不推薦這樣退出,因為一些系統的Item我們並不知道
- 調用CFRunLoopStop,退出runloop並將程序控制權交給調用者(如果runloop有嵌套,則只退出最內層runloop),一些情況下,CFRunLoopStop並不能真正的退出runloop,比如你使用下面的2種方法開啟runloop:
- (void)run; // 默認模式
- (void)runUntilDate:(NSDate *)limitDate;
當執行NSRunLoop的run方法,一旦成功(默認模式下有事件源),那么run會不停的調用runMode:beforeDate:來運行runloop,那么即便CFRunLoopStop退出了一個runloop,很快會有另一個runloop執行。即:如果你想退出一個runloop,那么你就不該調用run方法來開啟runloop
runUntilDate:與run一樣不停的執行runMode:beforeDate:方法,CFRunLoopStop也是退不出來的,不同的是runUntilDate:自己有個期限,超過這個期限會自動退出
很明顯,你會想到利用事件源為空來退出,這種方法上面已經說了,不推薦。。。
一個不想回答的問題:runloop本身的釋放。有人會糾結這個問題,經過多方查問、資料、源碼、測試加自身理解,得出:runloop退出后,是不會被釋放的(或者說立即),它大概很可能是伴隨着線程的釋放而釋放。。。。。。歡迎補充
Runloop嵌套
嵌套,剛接觸時感覺很神奇,然而一入嵌套深似海。。。特別是約瑟夫環的問題(http://www.jianshu.com/p/3c62ac7d9285)。。。
在當前runloop的callout函數里面執行上runloop,例程代碼如下:
/**
runloop嵌套測試,
*/
+ (void)nestTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle1) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:2]];
NSLog(@"-end-");
});
}
/**
不停的運行與退出最內層runloop
*/
+ (void)timerHandle1 {
NSLog(@"timer111-%@",[[NSRunLoop currentRunLoop] currentMode]);
// 防止多次添加timer,開發中應特別注意
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSTimer *tickTimer2 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle2) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tickTimer2 forMode:UITrackingRunLoopMode];
});
[[NSRunLoop currentRunLoop] runMode:UITrackingRunLoopMode beforeDate:[NSDate distantFuture]];
}
+ (void)timerHandle2 {
NSLog(@"timer222-%@",[[NSRunLoop currentRunLoop] currentMode]);
CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]);
}
打印結果
例程中外層runloop運行在NSDefaultRunLoopMode
模式下,然后在它的callout函數(定時器1)又執行runloop,運行在UITrackingRunLoopMode
模式下,實現嵌套,然后在內層runloop的callout(timerHandle2),停止運行當前runloop,即停止內層runloop,這時又回到外層循環。外層runloop只運行2秒到期。-end-
上面嵌套是運行在不同模式下,當同一模式下的runloop出現嵌套時,蘋果依然處理的很好。舉例:
- 將t1(timer1)添加到r1(runloop1),並在
NSDefaultRunLoopMode
模式下運行 - 在t1的響應函數里,將t2添加到r2,r2在
NSDefaultRunLoopMode
模式下運行 - 此時很明顯,r2處於嵌套內層,則只應該運行t2的響應函數
- 在t2的響應函數里,退出r2,此時回到r1
- 會運行t1與t2的響應函數
可能你會覺得很詫異,t2怎么也會運行呢????其實這很符合邏輯:
假設在第2步驟中,我們沒有執行r2,即沒有r2,那么t2還是加到了r1上。既然是加到了r1那執行就不難理解了。(是否感覺蘋果很強大?)
注意:r1與r2代表的是同一runloop,只是調用棧不同,或者說嵌套層。如果把runloop理解為一個函數,那么就可以理解為函數r1調用了自身,那個"自身"稱為r2。