玩轉(zhuǎn)Runloop - 代碼示例使用Source, Observer, Timer

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)聽BeforeWaitingExit事件,這個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)用出去拐格。

Paste_Image.png

這串很長的函數(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尖淘,或者超時時間到奕锌;如果沒有添加sourcetimer,則直接退出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);
}

CFRunLoopSourceSignalCFRunLoopWakeUp函數(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_1.png

在本例中Runloop被喚醒后跳回到了第2步。

perform回調(diào)中打個斷點可看到函數(shù)調(diào)用棧:

Paste_Image.png

自定義的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是類似的:


Paste_Image.png

第2行多了一項__NSThreadPerformPerform調(diào)用, 這就是Cocoa的封裝九火。

輸出日志這里不貼出來了赚窃,類似的。

Timer

關(guān)于Timer的用法資料就很多了岔激,暫時這里先不詳述勒极,日后待更。

本文的示例代碼以上傳Github, 歡迎來查看點贊~

參考資料:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鹦倚,一起剝皮案震驚了整個濱河市河质,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌震叙,老刑警劉巖掀鹅,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異媒楼,居然都是意外死亡乐尊,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門划址,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扔嵌,“玉大人,你說我怎么就攤上這事夺颤×《校” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵世澜,是天一觀的道長独旷。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么嵌洼? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任案疲,我火速辦了婚禮,結(jié)果婚禮上麻养,老公的妹妹穿的比我還像新娘褐啡。我一直安慰自己,他們只是感情好鳖昌,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布备畦。 她就那樣靜靜地躺著,像睡著了一般遗遵。 火紅的嫁衣襯著肌膚如雪萍恕。 梳的紋絲不亂的頭發(fā)上战坤,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天硼被,我揣著相機與錄音抬旺,去河邊找鬼。 笑死翼岁,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的司光。 我是一名探鬼主播琅坡,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼残家!你這毒婦竟也來了榆俺?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坞淮,失蹤者是張志新(化名)和其女友劉穎茴晋,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體回窘,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡诺擅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啡直。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片烁涌。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖酒觅,靈堂內(nèi)的尸體忽然破棺而出撮执,到底是詐尸還是另有隱情,我是刑警寧澤舷丹,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布抒钱,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏继效。R本人自食惡果不足惜症杏,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望瑞信。 院中可真熱鬧厉颤,春花似錦、人聲如沸凡简。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽秤涩。三九已至帜乞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間筐眷,已是汗流浹背黎烈。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留匀谣,地道東北人照棋。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像武翎,于是被迫代替她去往敵國和親烈炭。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容