Runloop

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有:

  • NSDefaultRunLoopMode:默认的运行模式,除了NSConnection对象的事件。
  • NSRunLoopCommonModes:是一组常用的模式集合,将一个input source关联到这个模式集合上,等于将input source关联到这个模式集合中的所有模式上。在iOS系统中NSRunLoopCommonMode包含NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode。

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

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

  • UITrackingRunLoopMode:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动), 主线程当触摸事件触发会设置为这个模式,可以用来在控件事件触发过程中设置Timer。
  • GSEventReceiveRunLoopMode:用于接受系统事件,属于内部的Run Loop模式。
  • 自定义Mode:可以设置自定义的运行模式Mode,你也可以用CFRunLoopAddCommonMode添加到NSRUnLoopCommonModes中。

总结

  • Run Loop 运行时只能以一种固定的模式运行,如果我们需要它切换模式,只有停掉它,再重新开其它
  • 运行时它只会监控这个模式下添加的Timer Source和Input Source,如果这个模式下没有相应的事件源,RunLoop的运行也会立刻返回的。注意Run Loop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以添加事件源的时候使用NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。

CFRunLoopSourceRef

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

  • Source1 :基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好。
  • Source0 :非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。

简单举个例子:一个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);
  • 方法就是创建一个observer,绑定一个runloop和模式,而block回调就是监听到runloop每种状态的时候回触发。
  • 其中CFRunLoopActivity是一枚举值,与每种状态对应:
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; // 有一个超时时间限制
  • 比上面的接口好点,有个超时时间,可以控制每次Run Loop的运行时间,也是运行在NSDefaultRunLoopMode模式。
  • 这个方法运行Run Loop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行Run Loop。

注意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;
  • 从方法上来看,比上面多了一个参数,可以设置运行模式。

  • 由一点需要注意:这种运行方式是可以被CFRunLoopStop(runloopRef)所停止的。

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();
  • 运行在默认的kCFRunLoopDefaultMode模式下,知道CFRunLoopStop接口调用停止这个RunLoop,或者RunLoop的所有事件源被删除。

  • NSRunLoop是基于CFRunLoop来封装的,NSRunLoop是线程不安全的,而CFRunLoop是线程安全的。

  • 在这里我们可以看到和上面NSRunLoop有一个直观的区别是:CFRunLoop能直接停止掉所有的CFRunLoop运行起来的runloop,其实之前讲到的:

- (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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348

推荐阅读更多精彩内容