Runloop是一個神奇的東西,它貫穿了一個iOS應用的生命周期而一直為伴。本文會對Runloop有一部分講解,但看這篇文章之前,你仍需要對Runloop有一個基本的了解,可以看大神的這篇文章。我留意到網絡上對Runloop原理講解的文章很多,但示例代碼很少。本文主要用代碼展示一些Runloop的玩法,會涉及到部分的CoreFoundation的API調用。
大家都知道Runloop的一個Mode里可包含三樣東西:Source, Observer, Timer,它們被稱為Mode Item。簡而言之,Runloop依據Mode去跑,任何一個Item都需要添加進一個Mode里才為之有效。這里涉及的方法有:
- CFRunLoopAddSource()
- CFRunLoopAddObserver()
- CFRunLoopAddTimer()
以上是Core Foundation的API,我省略了參數沒寫,CF的API太嚇人了。lol。
好吧,其實分別涉及三個參數:Runloop自身,item自身,以及Mode囖!
在Cocoa對Runloop的封裝里,API就沒那么豐富了。添加mode item的方法有:
- addTimer:forMode:
- addPort:forMode:
Timer也就是NSTimer對象,在常規開發里涉及Runloop最多可能也就它了;Port就厲害了,Mach port是iOS系統(Darwin)的進程間通信方式,屬于Source的一種,這個下面再說。
Observer
首先我們說Observer。它是一個對象沒錯,但簡單點理解:它是一個回調。
Apple的Runloop實現中會在特定的6個時刻嘗試觸發Observer調用(這里的時刻是也可以理解為一種事件)。分別是:
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
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有時刻
};
為什么我說“嘗試觸發”而不是“觸發”呢?(自己想)
例如:iOS模板工程的main函數里使用了@autoreleasepool包裹,實際蘋果向主線程Runloop注冊了兩個Observer。一個監聽Entry事件,這個Observer回調中調用_objc_autoreleasePoolPush()來創建自動釋放池;一個監聽BeforeWaiting和Exit事件,這個Observer調用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()來釋放引用池和新建池,Exit時釋放池。因此實現了每一個Runloop循環都釋放引用池的效果。
說了那么多,我們如何自己寫一個Observer呢?
Cocoa里沒有涉及Observer的的API,我們使用CoreFoundation的。
在這里我們將注冊一個監聽所有事件的Observer。
我們新建一個線程,開啟它的Runloop,然后把自定義的observer添加進它的Runloop里。
#import "RLThread.h"
@implementation RLThread
- (void)main {
[[NSThread currentThread] setName:@"MyRunLoopThread"];
CFRunLoopRef myCFRunLoop = CFRunLoopGetCurrent();
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"observer: loop entry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"observer: before timers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"observer: before sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"observer: before waiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"observer: after waiting");
break;
case kCFRunLoopExit:
NSLog(@"observer: exit");
break;
case kCFRunLoopAllActivities:
NSLog(@"observer: all activities");
break;
default:
break;
}
});
CFRunLoopAddObserver(myCFRunLoop, observer, kCFRunLoopDefaultMode);
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
[myRunLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
BOOL done = NO;
do
{
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 30, YES);
if (result == kCFRunLoopRunFinished) {
NSLog(@"====runloop finished(no sources or timers), exit");
done = YES;
} else if (result == kCFRunLoopRunStopped) {
NSLog(@"====runloop stopped, exit");
done = YES;
} else if (result == kCFRunLoopRunTimedOut) {
NSLog(@"====runloop timeout, exit");
done = NO;
} else if (result == kCFRunLoopRunHandledSource) {
NSLog(@"====runloop process a source, exit");
done = YES;
}
}
while (!done);
}
這個線程啟動后講進入它的main方法。我們定義了一個監聽所有事件的observer,在回調里打印出每個事件描述。從創建observer的方法CFRunLoopObserverCreateWithHandler(...)
可見observer包含了一個block回調。當然也可使用另外一個CFRunLoopObserverCreate(...)
方法,里面包含了一個回調函數指針參數,道理是一樣的。
如果在observer的回調函數里打斷點,可以看到調用函數棧,最終它是通過一串很長的函數__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
來調用出去。
這串很長的函數的源代碼:
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(CFRunLoopObserverCallBack func, CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
if (func) {
func(observer, activity, info);
}
asm __volatile__(""); // thwart tail-call optimization
}
可見它會判斷是否func存在才去回調,而它就是設置在observer的回調函數(這里就是那個block)了。
在開啟Runloop前,添加了一個Port,防止Runloop在無source和timer的情況下直接退出,僅僅有observer是不夠的。前面說過port是一種source,當然這里你也可以添加timer,這里添加一個不會使用到的port只是寫起來方便。眾所周知大名鼎鼎的AFNetworking也使用了這種套路,不過它是addPort
完之后就直接調用-run
來開啟Runloop了。
開啟Runloop
這里說下開啟Runloop的幾種方法:
Cocoa API
-
runMode:beforeDate:
指定Runloop的Mode和超時時間。返回YES,如果Runloop跑起來并且處理了一個source,或者超時時間到;如果沒有添加source或timer,則直接退出Runloop并返回NO。
注意這里timer并不是source。如果處理了一次timer并不會導致返回,原因在于timer也許是重復的。
-
run
Runloop默認以NSRunloopDefaultMode一直跑下去,實際是通過循環調用runMode:beforeDate:
去實現的。用這個方法跑無法在Runloop過程中改變mode,因此如果希望Runloop有所終止就不應用此方法,而是用第一個。 -
run:untilDate:
跟run
差不多但有超時時間。
CoreFoundation API
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
指定mode和timeout,第三個參數指定是否在處理了一個source后就返回。返回值類型為一個整型枚舉:
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished = 1, // 沒有timer或source
kCFRunLoopRunStopped = 2, // runloop被外界終止(調用CFRunloopStop)
kCFRunLoopRunTimedOut = 3, // 超時返回
kCFRunLoopRunHandledSource = 4 // 處理了一個source而返回
};
可見CF的API提供了比Cocoa更豐富的接口。所以我們采用CF的API,可根據返回值類型而決定是否要重啟Runloop。很多的Runloop實踐都是將開啟Runloop的方法嵌套在一個while循環里來實現的。如上一節的Demo所示。
上面的線程跑起來后,將會進入到一個Runloop的循環到隨眠,直至Runloop超時后被重啟(因為沒有source和timer來喚醒Runloop)。observer回調的輸出可見于log:
2017-04-12 15:09:28.465 RunloopPlayer[89041:22264822] observer: loop entry
2017-04-12 15:09:28.465 RunloopPlayer[89041:22264822] observer: before timers
2017-04-12 15:09:28.465 RunloopPlayer[89041:22264822] observer: before sources
2017-04-12 15:09:28.466 RunloopPlayer[89041:22264822] observer: before waiting
2017-04-12 15:09:58.466 RunloopPlayer[89041:22264822] observer: after waiting
2017-04-12 15:09:58.467 RunloopPlayer[89041:22264822] observer: exit
2017-04-12 15:09:58.467 RunloopPlayer[89041:22264822] ====runloop timeout, exit
2017-04-12 15:09:58.467 RunloopPlayer[89041:22264822] observer: loop entry
2017-04-12 15:09:58.468 RunloopPlayer[89041:22264822] observer: before timers
2017-04-12 15:09:58.468 RunloopPlayer[89041:22264822] observer: before sources
2017-04-12 15:09:58.469 RunloopPlayer[89041:22264822] observer: before waiting
可見Runloop在28秒處進入到58秒被喚醒而退出,恰好是設置的超時時間。程序設定若是由于timeout退出的Runlooph會被重啟。
以上是observer的使用和開啟Runloop的方法。下面我們將通過添加Source來進一步考察Runloop的機制。
Source
Source分兩種版本:source0和source1。source1是基于mach port的,而source0為自定義的source。
最新的iOS Cocoa 已發現無法使用mach port的API了,可能跟iOS加強沙盒安全有關。CF的我沒試,知道的同學可以告訴我。
在iOS應用里,蘋果注冊了一些自定義的source(包括source0和source1)來響應各種硬件事件。(有些文章說硬件事件都注冊成了source1,我自己測試并不全是這樣。例如,我測試發現鎖屏事件是被source0觸發的,而屏幕旋轉事件為source1。不知道真機與模擬器會不會不一樣,如果有什么黑盒我遺漏的歡迎同學們指出。。這里先不過多糾結這個問題了)
下面說說source0的用法。
自定義source
source主要包含了一個context結構
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
可見它主要都是一些回調。本例中我們用到后三個,其中schedule
是source被添加到Runloop后的回調,cancel
為Runloop退出并清除source時的回調,最后也是最關鍵的perform
為source被觸發時的回調。
剛才的demo,在Runloop啟動前,加入如下代碼:
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL, RunloopSourceScheduleRoutine, RunloopSourceCancelRoutine, RunloopSourcePerformRoutine };
source = CFRunLoopSourceCreate(NULL, 0, &context);
runLoop = CFRunLoopGetCurrent();
CFRunLoopAddSource(runLoop, source, kCFRunLoopDefaultMode);
這樣就添加了一個source。
再定義schedule,cancel,perform幾個回調函數, 它們已經被加入到source context結構中:
void RunloopSourceScheduleRoutine(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {
NSLog(@"Schedule routine: source is added to runloop");
}
void RunloopSourceCancelRoutine(void *info, CFRunLoopRef rl, CFRunLoopMode mode) {
NSLog(@"Cancel Routine: source removed from runloop");
}
void RunloopSourcePerformRoutine(void *info) {
NSLog(@"Perform Routine: source has fired");
}
然后再主線程定義觸發source的函數(比如在ViewController設置一個點擊事件):
- (IBAction)fireSourceToRunloopOf2ndThread:(id)sender {
CFRunLoopSourceRef source = self.anotherThread->source;
CFRunLoopSourceSignal(source);
CFRunLoopWakeUp(self.anotherThread->runLoop);
}
CFRunLoopSourceSignal
和CFRunLoopWakeUp
函數觸發一個source并把目標線程的Runloop從隨眠中換醒來。
調用順序日志:
2017-04-12 16:45:52.445 RunloopPlayer[91055:22478145] Schedule routine: source is added to runloop
2017-04-12 16:45:52.449 RunloopPlayer[91055:22478145] observer: loop entry
2017-04-12 16:45:52.450 RunloopPlayer[91055:22478145] observer: before timers
2017-04-12 16:45:52.450 RunloopPlayer[91055:22478145] observer: before sources
2017-04-12 16:45:52.451 RunloopPlayer[91055:22478145] observer: before waiting
2017-04-12 16:46:00.677 RunloopPlayer[91055:22478145] observer: after waiting
2017-04-12 16:46:00.678 RunloopPlayer[91055:22478145] observer: before timers
2017-04-12 16:46:00.678 RunloopPlayer[91055:22478145] observer: before sources
2017-04-12 16:46:00.678 RunloopPlayer[91055:22478145] Perform Routine: source has fired
2017-04-12 16:46:00.679 RunloopPlayer[91055:22478145] observer: exit
2017-04-12 16:46:00.679 RunloopPlayer[91055:22478145] ====runloop process a source, exit
2017-04-12 16:46:12.857 RunloopPlayer[91055:22478145] Cancel Routine: source removed from runloop
注意在16:46:00時候觸發source,從日志可看出,Runloop的事件處理時序是對應官方描述的。引用一個圖:
在本例中Runloop被喚醒后跳回到了第2步。
在perform
回調中打個斷點可看到函數調用棧:
自定義的
perform
回調最終就是通過那一長串函數__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
來調用出去。這里與observer的回調是類似的。
實際上observer和source的核心就是一個回調。
Perform Selector Source
我們實際編程中會較常接觸到的,這也是一種自定義的Source。
它們是Cocoa對CFRunloopSource的高層封裝,它們都可以用Core Foundation的Source API去實現。
Hint: 這里的
withObject:
參數對應CFRunLoopSourceContext的void *info
;
performSelector方法簇包含了以下方法:
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:
我們也可以用它來對目標線程添加并觸發一個source。例如在一個控制器里(主線程),觸發一個source:
- (IBAction)start2ndThread:(UIButton *)sender {
RLThread *thread = [[RLThread alloc] init];
self.anotherThread = thread;
[thread start];
}
- (IBAction)performOn2ndThread:(id)sender {
NSThread *theThread = self.anotherThread;
[self performSelector:@selector(greetingFromMain:) onThread:theThread withObject:@"hello" waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
}
- (void)greetingFromMain:(NSString *)greeting {
NSLog(@"greeting from main: %@", greeting);
}
函數調用棧剛才自定義source是類似的:
第2行多了一項__NSThreadPerformPerform
調用, 這就是Cocoa的封裝。
輸出日志這里不貼出來了,類似的。
Timer
關于Timer的用法資料就很多了,暫時這里先不詳述,日后待更。
本文的示例代碼以上傳Github, 歡迎來查看點贊~
參考資料: