iOS之OC深入理解iOS Developerios developers

玩转Runloop - 代码示例使用Source, Observ

2017-04-12  本文已影响174人  4d1487047cf6

Runloop是一个神奇的东西,它贯穿了一个iOS应用的生命周期而一直为伴。本文会对Runloop有一部分讲解,但看这篇文章之前,你仍需要对Runloop有一个基本的了解,可以看大神的这篇文章。我留意到网络上对Runloop原理讲解的文章很多,但示例代码很少。本文主要用代码展示一些Runloop的玩法,会涉及到部分的CoreFoundation的API调用。

大家都知道Runloop的一个Mode里可包含三样东西:Source, Observer, Timer,它们被称为Mode Item。简而言之,Runloop依据Mode去跑,任何一个Item都需要添加进一个Mode里才为之有效。这里涉及的方法有:

以上是Core Foundation的API,我省略了参数没写,CF的API太吓人了。lol。

好吧,其实分别涉及三个参数:Runloop自身,item自身,以及Mode囖!
在Cocoa对Runloop的封装里,API就没那么丰富了。添加mode item的方法有:

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

注意这里timer并不是source。如果处理了一次timer并不会导致返回,原因在于timer也许是重复的。

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, 欢迎来查看点赞~

参考资料:

上一篇下一篇

猜你喜欢

热点阅读