您的位置:首页 > 运维架构

深入理解RunLoop

2017-02-21 11:19 309 查看
一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出。

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。



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

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

根据苹果在文档里的说明,RunLoop 内部的逻辑大致如下:



实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:



RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

系统默认注册了5个Mode:

kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

我们可能会遇到一些 RunLoop 的坑:

[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(foo) userInfo:nil repeats:YES];

一般情况下我们会使用这个方法来添加一个定时器,但是我们用这个方法创建的 NSTimer 会被加到 CFRunLoopDefaultMode 中,此时,如果页面开始滚动 主线程 RunLoop 的 mode 会切换到 UITrackingRunLoopMode,我们刚刚创建的 NSTimer 就不会运行.我们可以将新建的 NSTimer 放到
CFRunLoopCommonModes 中,这样不论在什么状态都是可以执行的.

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(foo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

下图展示了 RunLoop 的模型: 可以看出 RunLoop 是线程中的一个循环,并接收两个事件源的消息:
Input sources 和 Timer sources



Input sources:
Input source 用来投递异步消息,通常消息来自另外的线程或者程序.
消息类型分为以下三种:

Port Input Source

Cocoa 和 Core Foundation 提供了 Mach Port 用于线程或进程间的通信.
如果要建立和 NSMachPort 对象的本地连接,你需要创建端口对象并加入主线程的 RunLoop 里.当运行子线程的时候,你传递端口对象到子线程的入口.子线程通过端口对象将消息传入主线程.

首先在主线程建立端口对象,并在次线程的启动时将端口对象传入:

//设置主线程 port,子线程通过此端口发送消息给主线程
NSPort *myPort = [NSMachPort port];
if (myPort) {
myPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:myPort forMode:NSDefaultRunLoopMode];

//启动子线程,并传入端口信息
[NSThread detachNewThreadSelector:@selector(launchThreadWithPort:) toTarget:	 [MyWorkerClass class] withObject:myPort];
}


Cocoa Perform Selector Sources

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSString *> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

这些 API 中,最后两个是取消当前线程中的 aSelector 调用,其他的 API 是在当前线程执行指定的 aSelector

Timer source:

Timer Source 类型的事件源,就是创建Timer添加到RunLoop中.在Cocoa里,使用NSTimer创建定时器加入 RunLoop,在 Core Foundation 里使用 CFRunLoopTimerRef 类型,本质上 NSTimer 是CFRunLoopTimerRef的简单扩展.

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(foo) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

RunLoop Observer

对比上面说的 sources,它们是在特定的同步事件或异步事件发生时被触发,RunLoop observer 就不一样了,它是在 RunLoop 执行自己的代码到某一个指定状态时被触发.

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};

CFRunLoopObserverContext  context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopBeforeTimers, YES, 0, &runLoopObserver, &context);
if (observer) {
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
}

RunLoop应用场景:
1、保证子线程的长时间存活;
2、让NSTimer在scrollView滚动时能正常运转;
3、……
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: