玩轉Runloop - 代碼示例使用Source, Observer, Timer

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()來創建自動釋放池;一個監聽BeforeWaitingExit事件,這個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__來調用出去。

Paste_Image.png

這串很長的函數的源代碼:

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,或者超時時間到;如果沒有添加sourcetimer,則直接退出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);
}

CFRunLoopSourceSignalCFRunLoopWakeUp函數觸發一個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_1.png

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

perform回調中打個斷點可看到函數調用棧:

Paste_Image.png

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


Paste_Image.png

第2行多了一項__NSThreadPerformPerform調用, 這就是Cocoa的封裝。

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

Timer

關于Timer的用法資料就很多了,暫時這里先不詳述,日后待更。

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

參考資料:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內容