RunLoop介绍和使用
上次讲了runtime,这次是runloop,虽然两者都是run开头的名词术语,但是在OC中,这两个东西压根没啥联系。这篇文章主要讲讲runloop的一些概念和用法。其中包含:
- 什么runloop
- runloop是怎么存在的
- runloop中包含哪些东西
- 日常开发中使用到runloop 的场景
一、什么是runloop
一个很容易想到的现象: 当我们将手机解锁进入某个APP之后,如果不操作手机(包括网络请求的行为),手机不会有任何反应,一旦我们进行了操作的时候,手机就会执行响应的动作。那当我们不操作的时候手机在做什么呢? 如果对受的状态进行检测的话,会发现CPU的使用率几乎是零,也就是说,没有进行操作的时候的手机的状态是休眠的! 专业看来说,在手机没有接受事件的时候,手机自动进入休眠状态,但是保持一种随时可唤醒的姿势,等待用户的事件传入。这种机制在很多不同的系统中都有,被称之为事件循环。在苹果中,实现这一机制的就是RunLoop了。
我们大概把runloop想象成一个do-while循环,在不接受退出的指令的状态,会一直执行循环下去。而runloop在这基础之上加入了更复杂的逻辑,使得runloop不仅可以在没有事件输入的情况下进行休眠,同时做到随时随地的响应事件。
逻辑上可以理解成下面的代码。
func loop() { do { var message = get_next_message(); process_message(message); //处理信息 } while (message != quit); }
当然,实际上比这个要复杂的多。至少我不觉的这个循环里我能做一些精确的控制。让我们先看看runloop中还包含了其他的一些的东西。
先看看NSRunLoop。NSRunLoop是基于CF RunLoopRef的封装,但两者并不是 toll-free briged,在OC中,可以使用下面的方式获取。
[aRunLoop getCFRunLoop];
看看苹果对NSRunloop的介绍说明:
The
NSRunLoop
class declares the programmatic interface to objects that manage input sources. AnNSRunLoop
object processes input for sources such as mouse and keyboard events from the window system,NSPort
objects, andNSConnection
objects. AnNSRunLoop
object also processesNSTimer
events.
NSRunLoop管理程序的输入事件,这些事件包含了来自窗口鼠标和键盘的事件,以及NSport端口事件和网络请求事件,同时还包括了timer事件。也即所有的事件,都会经过runloop的管理,在APP中有序的执行。
二、runloop是怎么存在的
在苹果的NSRunloop 的文件中我们看到这些:
FOUNDATION_EXPORT NSRunLoopMode const NSDefaultRunLoopMode; FOUNDATION_EXPORT NSRunLoopMode const NSRunLoopCommonModes @property (class, readonly, strong) NSRunLoop *currentRunLoop; @property (class, readonly, strong) NSRunLoop *mainRunLoop NS_AVAILABLE(10_5, 2_0); #endif @property (nullable, readonly, copy) NSRunLoopMode currentMode; - (CFRunLoopRef)getCFRunLoop CF_RETURNS_NOT_RETAINED; - (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode; - (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode; - (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode; - (nullable NSDate *)limitDateForMode:(NSRunLoopMode)mode; - (void)acceptInputForMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; @end @interface NSRunLoop (NSRunLoopConveniences) - (void)run; - (void)runUntilDate:(NSDate *)limitDate; - (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate; #if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) - (void)configureAsServer NS_DEPRECATED(10_0, 10_5, 2_0, 2_0); #endif /// Schedules the execution of a block on the target run loop in given modes. /// - parameter: modes An array of input modes for which the block may be executed. /// - parameter: block The block to execute - (void)performInModes:(NSArray<NSRunLoopMode> *)modes block:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0)); /// Schedules the execution of a block on the target run loop. /// - parameter: block The block to execute - (void)performBlock:(void (^)(void))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));
这其中,我发现它并不能通过某个方式创建。正常的Object对象,一般都可以通过 init 来创建实力对象。然而,NSRunLoop并没有。但是我们可以看到NSRunLoop有一个属性:
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
事实上,NSRunLoop不能直接创建,只能通过currentRunLoop来获取。并且每个RunLoop只能在当前线程中操作,因为runloop不是线程安全的。看过喵神的技术博客的应该知道,NSRunLoop是基于CFRunloop的封装,而CFRunloop是纯C的API,它是线程安全的。这里一直提到了线程,其实是想说明,NSRunLoop依赖于线程,并且每个线程最多只有一个NSRunLoop,对于主线程,在APP启动时候,main函数中就已经开启了一个NSRunLoop用于服务主线程,这个runloop可以通过 NSRunLoop 的 mainRunLoop获得。如果是CFRunLoop ,则可以通过CFRunLoopGetMain() 和 CFRunLoopGetCurrent()获取当前线程的runloop和主线程的runloop。
线程和runloop一一对应,当线程被销毁的时候,runloop也GG了。所以,要维系一个runloop,首先要保证线程的存状态。其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。
三、runloop中包含哪些东西
runloop管理了线程中的事件,这些事件包含:timer事件、source事件以及Observer。还包含了事件的运行的model-RunLoopMode。
timer事件即是我们使用的定时器。
source事件包含两类,一类是source0,另一类是source1,两者的区别是:
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
Observer是观察者,它可以监听runloop的运行状态,可以监听以下状态:
kCFRunLoopEntry = (1UL << 0), // 即将进入LoopkCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2), // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒kCFRunLoopExit = (1UL << 7), // 即将退出Loop
RunLoopMode:这是runloop的一种标记。它有几种形式:
1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
上面提到的timer、source、Observer都不是直接放在runloop中的,而是作为item的形式被标记为某个model之中,然后runloop运行在对应的model时,才会管理这些item。当我们要处理某个item的时候,首先要确保item处于runloop对应的mode之中。
四、日常开发中使用到runloop 的场景
runloop在日常的开发中,使用的场景是比较少的。我们经常会有这么一种情况,如果使用定时器执行的一个小动画,在拖动scrollView的时候会被暂停。这是因为,runloop通常情况在kCFRunLoopDefaultMode中运行,也即说,直接创建的NStimer将在这个model中。在拖动scrollView的时候,runloop就切换到了UITrackingRunLoopMode下,之前在kCFRunLoopDefaultMode中的item不再被runloop维护,也就停止了运行。要决解这个,只需要通过
- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
将NStimer加入到UITrackingRunLoopMode中即可。 还有一种办法,在所有的mode中,还有一种kCFRunLoopCommonModes,它是的作用类似于它的名字,时公共的mode,也即是在这个mode中的item,会被所有的mode支持。 我们可以将NStimer标记为Commonmode达到同样的目的。
还有其他的类似的情况之后,我们还能在著名的AFNetWorking中看到使用了Runloop :
+ (void)networkRequestThreadEntryPoint:(id)__unused object { @autoreleasepool { [[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; } }
这样开启了一个线程,同时开启runloop,并添加了一个port事件维系runloop 的运行,但是port并不发送时机的消息。这个线程是AFNetWorking用于将NSURLConection置于后台处理请求和回调的。