Runloop

2022-05-11  本文已影响0人  fleshMe

1 Rumloop 在三方库的使用

1.1 AFN2.x

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        /// 如果没有和人事件源添加到Run Loop上,Run Loop就会立刻exit,这也是为什么需要绑定一个Port的原因
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

首先我们要明确一个概念,线程一般都是一次执行完毕任务,就销毁了。
而在线程中添加了runloop,并运行起来,实际上是添加了一个do,while循环,这样这个线程的程序就一直卡在do,while循环上,这样相当于线程的任务一直没有执行完,所有线程一直不会销毁。

所有,一旦我们添加了一个runloop,并run了,我们如果要销毁这个线程,必须停止runloop,至于停止的方式,我们接着往下看。

这里创建了一个线程,取名为AFNetworking,因为添加了一个runloop,所以这个线程不会被销毁,直到runloop停止。

[runloop addPort: [NSMachPort port] forMode: NSDefaultRunLoopMode];

这行代码的目的是添加一个端口监听这个端口的事件,这也是我们后面会讲到的一种线程见的通信方式-基于端口的通信。

[runloop run];

runloop开始跑起来,但是要注意,这种runloop,只有一种方式能停止。

[NSRunloop currentRunloop] removePort: <#(nonnull NSPort)#> forMode: <#(nonull NSRunLoopMode)#>

只有从runloop中移除我们之前添加的端口,这样的runloop没有任何事件,所有直接退出。
再次回到AFN2.x的这行源码上,因为他用的是run,而且并没有记录下自己添加的NSMachPort,所有显然,它没有打算退出这个runloop,这是一个常驻线程。

NSURLConnection的delegate方法需要在connection发起的线程runloop中调用,于是AFNetWorking单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也是它接收,不占用主线程,也不耗CPU资源。

1.2 AFN3.x

需要开启的时候:

CFRunLoopRun();

终止的时候:

CFRunloopStop(CFRunLoopGetCurrent());

由于NSUrlSession参考了AFN2.x的优点,自己维护了一个线程池,做Request线程的调度与管理,所有在AFN3.x中,没有了常驻线程,都是用的run,结束的时候stop。

1.3 RAC

再看RAC中runloop:

do {
    [NSRunloop.mainRunloop runMode:NSDefaultRunLoopMode beforeDate: [NSDate dateWithTimeIntervalSinceNow: 0.1]];
} while(!done);

大致讲下这段代码实现的内容,自己用一个Bool值done去控制runloop的运行,每次只运行这个模式的runloop,0.1秒。0.1秒后开启runloop的下次运行。

2 Runloop 定义

Runloop,顾名思义就是跑圈,他的本质就是一个do,while循环,当有事做时就做事,没事做时就休眠。

每个线程都由一个Run Loop,主线程的Run Loop会在App运行的时自动运行,子线程需要手动获取运行,第一次获取时,才会去创建。

每个Run Loop都会以一个模式mode来运行,可以使用NSRunLoop的方法运行在某个特定的mode。

- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

Run Loop的处理两大类事件源:Timer Source和Input Source(包括performSelector *方法簇、Port或者自定义的Input Source),每个事件源都会绑定在Run Loop的某个特定模式mode上,而且只有RunLoop在这个模式下运行的时候,才会触发Timer和Input Source。

最后,如果没有和人事件源添加到Run Loop上,Run Loop就会立刻exit,这也是一开始AFN例子,为什么需要绑定一个Port的原因。

2.1 RunLoop构成

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。


db5745a609f12cab8cb40d203a172bbe.png

RunLoop Mode

OS下Run Loop的主要运行模式mode有:

当然,默认情况下NSRunLoopCommonModes包含了NSDefaultRunLoopMode和UITrackingRunLoopMode。

注意: 让Run Loop运行在NSRunLoopCommonModes下是没有意义的,因为一个时刻Run Loop只能运行在一个特定模式下,而不可能是个模式集合。

总结

CFRunLoopSourceRef

是事件产生的地方。Source有两个版本:Source0 和 Source1。

简单举个例子:一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:

我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event, Event先告诉source1(mach_port),source1唤醒RunLoop, 然后将事件Event分发给source0,然后由source0来处理。

CFRunLoopTimerRef

是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
上图提到了Observer:

CFRunLoopObserverRef

是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
Observer的创建以及添加到Run Loop中需要使用Core Foundation的接口:
方法很简单如下:

// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1 // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64
// 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 128 // 即将退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 可以监听以上所有状态
};

2.2 Run Loop运行接口

Foundation层和Core Foundation层都有相应的接口可以操作Run Loop:Foundation层对应的是NSRunLoop,Core Foundation层对应的是CFRunLoopRef;

*两组接口差不多,不过功能上还是有许多区别的:
例如CF层可以添加自定义的Input Source事件源、(CFRunLoopSourceRef)RunLoop观察者Observer(CFRunLoopObserverRef),很多类似功能的接口特性也是不一样的。

2.2.1NSRunLoop的运行接口:

// 运行NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
- (void)run;
// 运行NSRunLoop:参数为时间期限,运行模式为默认的NSDefaultRunLoopMode模式
- (void)runUntilDate:(NSDate *)limitDate;
// 运行NSRunLoop:参数为运行模式、时间期限,返回值为YES表示处理事件后返回的,NO表示是超时或者停止运行导致返回的。
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;

详细讲解下NSRunLoop的三个运行接口:

- (void)run; // 无条件运行

不建议使用,因为这个接口会导致Run Loop永久性的在NSDefaultRunLoopMode模式。

即使用CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,除非能移除这个runloop上的所有事件源,包括定时器和source时间,不然这个子线程就无法停止,只能永久运行下去。

- (void)runUntilDate:(NSDate *)limitDate; // 有一个超时时间限制

注意CFRunLoopStop(runloopRef), 也无法停止Run Loop的运行。
使用如下的代码:

while(!Done) {
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow: 10]];
    NSLog(@"exiting runloop, ......");
}

注意这个Done是我们自定义的一个Bool值,用来控制是否还需要开启下一次runloop。

这个例子大概做了如下的事情: 这个RunLoop会每10秒退出一次,然后输出exiting runloop ……,然后下次根据我们的Done值来判断是否再去运行runloop。

// 有一个超时时间限制,而且设置运行模式
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

2.2.2 CFRunLoopRef的运行接口

// 运行CFRunLoopRef
void CFRunLoopRun();
// 运行CFRunLoopRef:参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
// 停止运行CFRunLoop
void CFRunLoopStop(CFRunLoopRef rl);
// 唤醒CFRunLoopRef
void CFRunLoopWakeUp(CFRunLoopRef rl);

分析一下Core Foundation中运行的runloop的接口:

void CFRunLoopRun();
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

这种方式运行起来的runloop也能用CFRunLoopStop 停止掉的,原因是它完全是基于下面这种方式封装的:

SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);

可以看到参数几乎一模一样,前者默认returnAfterSourceHandled参数为YES,当触发一个非timer事件后,runloop就终止了。

这里比较简单,就不举例赘述了。

SInt32 CFRunLoopRunInMode(mode, seconds, returnAfterSourceHandled);

总结一下:

runloop的运行方法一共有5种:包括NSRunLoop的3种,CFRunLoop的2种;
而取消的方式一共为3种:
1)移除掉runloop种的所有事件源(timer和source)。
2)设置一个超时时间。
3)只要CFRunLoop运行起来就可以用:void CFRunLoopStop(CFRunLoopRef rl); 去停止。
除此之外用 NSRunLoop下面这个方法也能使用void CFRunLoopStop(CFRunLoopRef rl); 停止:

[NSRunLoop currentRunLoop] runMode:<#(nonull NSRunLoopMode)#> beforeDate:<#(nonull NSDate)#>

3 Runloop 的执行顺序

执行顺序.png
上一篇 下一篇

猜你喜欢

热点阅读