Runloop是一個神奇的東西蛛倦,它貫穿了一個iOS應(yīng)用的生命周期而一直為伴。本文會對Runloop有一部分講解啦桌,但看這篇文章之前溯壶,你仍需要對Runloop有一個基本的了解,可以看大神的這篇文章甫男。我留意到網(wǎng)絡(luò)上對Runloop原理講解的文章很多且改,但示例代碼很少。本文主要用代碼展示一些Runloop的玩法板驳,會涉及到部分的CoreFoundation的API調(diào)用钾虐。
大家都知道Runloop的一個Mode里可包含三樣?xùn)|西:Source, Observer, Timer,它們被稱為Mode Item笋庄。簡而言之效扫,Runloop依據(jù)Mode去跑,任何一個Item都需要添加進(jìn)一個Mode里才為之有效直砂。這里涉及的方法有:
- CFRunLoopAddSource()
- CFRunLoopAddObserver()
- CFRunLoopAddTimer()
以上是Core Foundation的API菌仁,我省略了參數(shù)沒寫,CF的API太嚇人了静暂。lol济丘。
好吧,其實分別涉及三個參數(shù):Runloop自身洽蛀,item自身摹迷,以及Mode囖!
在Cocoa對Runloop的封裝里郊供,API就沒那么豐富了峡碉。添加mode item的方法有:
- addTimer:forMode:
- addPort:forMode:
Timer也就是NSTimer對象,在常規(guī)開發(fā)里涉及Runloop最多可能也就它了驮审;Port就厲害了鲫寄,Mach port是iOS系統(tǒng)(Darwin)的進(jìn)程間通信方式,屬于Source的一種疯淫,這個下面再說地来。
Observer
首先我們說Observer。它是一個對象沒錯熙掺,但簡單點理解:它是一個回調(diào)未斑。
Apple的Runloop實現(xiàn)中會在特定的6個時刻嘗試觸發(fā)Observer調(diào)用(這里的時刻是也可以理解為一種事件)。分別是:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 所有時刻
};
為什么我說“嘗試觸發(fā)”而不是“觸發(fā)”呢币绩?(自己想)
例如:iOS模板工程的main函數(shù)里使用了@autoreleasepool包裹蜡秽,實際蘋果向主線程Runloop注冊了兩個Observer。一個監(jiān)聽Entry事件类浪,這個Observer回調(diào)中調(diào)用_objc_autoreleasePoolPush()來創(chuàng)建自動釋放池载城;一個監(jiān)聽BeforeWaiting和Exit事件,這個Observer調(diào)用_objc_autoreleasePoolPop()和_objc_autoreleasePoolPush()來釋放引用池和新建池费就,Exit時釋放池诉瓦。因此實現(xiàn)了每一個Runloop循環(huán)都釋放引用池的效果。
說了那么多力细,我們?nèi)绾巫约簩懸粋€Observer呢睬澡?
Cocoa里沒有涉及Observer的的API,我們使用CoreFoundation的眠蚂。
在這里我們將注冊一個監(jiān)聽所有事件的Observer煞聪。
我們新建一個線程,開啟它的Runloop逝慧,然后把自定義的observer添加進(jìn)它的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);
}
這個線程啟動后講進(jìn)入它的main方法啄糙。我們定義了一個監(jiān)聽所有事件的observer,在回調(diào)里打印出每個事件描述云稚。從創(chuàng)建observer的方法CFRunLoopObserverCreateWithHandler(...)
可見observer包含了一個block回調(diào)隧饼。當(dāng)然也可使用另外一個CFRunLoopObserverCreate(...)
方法,里面包含了一個回調(diào)函數(shù)指針參數(shù)静陈,道理是一樣的燕雁。
如果在observer的回調(diào)函數(shù)里打斷點,可以看到調(diào)用函數(shù)棧鲸拥,最終它是通過一串很長的函數(shù)__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
來調(diào)用出去拐格。
這串很長的函數(shù)的源代碼:
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存在才去回調(diào),而它就是設(shè)置在observer的回調(diào)函數(shù)(這里就是那個block)了刑赶。
在開啟Runloop前捏浊,添加了一個Port,防止Runloop在無source和timer的情況下直接退出角撞,僅僅有observer是不夠的呛伴。前面說過port是一種source,當(dāng)然這里你也可以添加timer谒所,這里添加一個不會使用到的port只是寫起來方便热康。眾所周知大名鼎鼎的AFNetworking也使用了這種套路,不過它是addPort
完之后就直接調(diào)用-run
來開啟Runloop了劣领。
開啟Runloop
這里說下開啟Runloop的幾種方法:
Cocoa API
-
runMode:beforeDate:
指定Runloop的Mode和超時時間姐军。返回YES,如果Runloop跑起來并且處理了一個source尖淘,或者超時時間到奕锌;如果沒有添加source或timer,則直接退出Runloop并返回NO村生。
注意這里timer并不是source惊暴。如果處理了一次timer并不會導(dǎo)致返回,原因在于timer也許是重復(fù)的趁桃。
-
run
Runloop默認(rèn)以NSRunloopDefaultMode一直跑下去辽话,實際是通過循環(huán)調(diào)用runMode:beforeDate:
去實現(xiàn)的。用這個方法跑無法在Runloop過程中改變mode卫病,因此如果希望Runloop有所終止就不應(yīng)用此方法油啤,而是用第一個。 -
run:untilDate:
跟run
差不多但有超時時間蟀苛。
CoreFoundation API
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
指定mode和timeout益咬,第三個參數(shù)指定是否在處理了一個source后就返回。返回值類型為一個整型枚舉:
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished = 1, // 沒有timer或source
kCFRunLoopRunStopped = 2, // runloop被外界終止(調(diào)用CFRunloopStop)
kCFRunLoopRunTimedOut = 3, // 超時返回
kCFRunLoopRunHandledSource = 4 // 處理了一個source而返回
};
可見CF的API提供了比Cocoa更豐富的接口帜平。所以我們采用CF的API幽告,可根據(jù)返回值類型而決定是否要重啟Runloop梅鹦。很多的Runloop實踐都是將開啟Runloop的方法嵌套在一個while循環(huán)里來實現(xiàn)的。如上一節(jié)的Demo所示评腺。
上面的線程跑起來后帘瞭,將會進(jìn)入到一個Runloop的循環(huán)到隨眠,直至Runloop超時后被重啟(因為沒有source和timer來喚醒Runloop)蒿讥。observer回調(diào)的輸出可見于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秒處進(jìn)入到58秒被喚醒而退出,恰好是設(shè)置的超時時間抛腕。程序設(shè)定若是由于timeout退出的Runlooph會被重啟芋绸。
以上是observer的使用和開啟Runloop的方法。下面我們將通過添加Source來進(jìn)一步考察Runloop的機制担敌。
Source
Source分兩種版本:source0和source1摔敛。source1是基于mach port的,而source0為自定義的source全封。
最新的iOS Cocoa 已發(fā)現(xiàn)無法使用mach port的API了马昙,可能跟iOS加強沙盒安全有關(guān)。CF的我沒試刹悴,知道的同學(xué)可以告訴我行楞。
在iOS應(yīng)用里,蘋果注冊了一些自定義的source(包括source0和source1)來響應(yīng)各種硬件事件土匀。(有些文章說硬件事件都注冊成了source1子房,我自己測試并不全是這樣。例如就轧,我測試發(fā)現(xiàn)鎖屏事件是被source0觸發(fā)的证杭,而屏幕旋轉(zhuǎn)事件為source1。不知道真機與模擬器會不會不一樣妒御,如果有什么黑盒我遺漏的歡迎同學(xué)們指出解愤。。這里先不過多糾結(jié)這個問題了)
下面說說source0的用法乎莉。
自定義source
source主要包含了一個context結(jié)構(gòu)
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;
可見它主要都是一些回調(diào)送讲。本例中我們用到后三個,其中schedule
是source被添加到Runloop后的回調(diào)梦鉴,cancel
為Runloop退出并清除source時的回調(diào)李茫,最后也是最關(guān)鍵的perform
為source被觸發(fā)時的回調(diào)。
剛才的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幾個回調(diào)函數(shù), 它們已經(jīng)被加入到source context結(jié)構(gòu)中:
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");
}
然后再主線程定義觸發(fā)source的函數(shù)(比如在ViewController設(shè)置一個點擊事件):
- (IBAction)fireSourceToRunloopOf2ndThread:(id)sender {
CFRunLoopSourceRef source = self.anotherThread->source;
CFRunLoopSourceSignal(source);
CFRunLoopWakeUp(self.anotherThread->runLoop);
}
CFRunLoopSourceSignal
和CFRunLoopWakeUp
函數(shù)觸發(fā)一個source并把目標(biāo)線程的Runloop從隨眠中換醒來味榛。
調(diào)用順序日志:
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時候觸發(fā)source,從日志可看出予跌,Runloop的事件處理時序是對應(yīng)官方描述的搏色。引用一個圖:
在本例中Runloop被喚醒后跳回到了第2步。
在perform
回調(diào)中打個斷點可看到函數(shù)調(diào)用棧:
自定義的
perform
回調(diào)最終就是通過那一長串函數(shù)__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
來調(diào)用出去券册。這里與observer的回調(diào)是類似的频轿。
實際上observer和source的核心就是一個回調(diào)。
Perform Selector Source
我們實際編程中會較常接觸到的烁焙,這也是一種自定義的Source航邢。
它們是Cocoa對CFRunloopSource的高層封裝,它們都可以用Core Foundation的Source API去實現(xiàn)骄蝇。
Hint: 這里的
withObject:
參數(shù)對應(yīng)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:
我們也可以用它來對目標(biāo)線程添加并觸發(fā)一個source膳殷。例如在一個控制器里(主線程),觸發(fā)一個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);
}
函數(shù)調(diào)用棧剛才自定義source是類似的:
第2行多了一項__NSThreadPerformPerform
調(diào)用, 這就是Cocoa的封裝九火。
輸出日志這里不貼出來了赚窃,類似的。
Timer
關(guān)于Timer的用法資料就很多了岔激,暫時這里先不詳述勒极,日后待更。
本文的示例代碼以上傳Github, 歡迎來查看點贊~
參考資料: