美文网首页iOS之OC深入理解iOS Developerios developers
玩转Runloop - 代码示例使用Source, Observ

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

作者: 4d1487047cf6 | 来源:发表于2017-04-12 22:56 被阅读174次

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

    参考资料:

    相关文章

      网友评论

        本文标题:玩转Runloop - 代码示例使用Source, Observ

        本文链接:https://www.haomeiwen.com/subject/yefmattx.html