玩转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阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容