iOS中RunLoop机制的探索

来源:互联网 发布:2016网络作家排名 编辑:程序博客网 时间:2024/04/30 12:32

RunLoop是ios中一个非常重要的机制,ios系统底层很多模块都是通过RunLoop机制实现的,例如界面更新、事件响应等。本质上RunLoop是一种用于循环处理事件,而又不至于使CPU无意义空转的方式。

一、基本概念(了解过的可以跳过这一节)

1、NSRunLoop对象

这里写图片描述

(1)CFRunLoopRef

NSRunLoop对象是OC对象,是对CFRunLoopRef的封装,可以通过getCFRunLoop方法获取其对应的CFRunLoopRef对象。注意,NSRunLoop不是线程安全的,但CFRunLoopRef是线程安全的。

(2)RunLoopMode

NSRunLoop对象是一系列RunLoopMode的集合,每个mode包括有这个模式下所有的Source源、Timer源和观察者。每次RunLoop调用的时候都只能调用其中的一个mode,接收这个mode下的源,通知这个mode下的观察者。这样设计的主要目的就是为了隔离各个模式下的源和观察者,使其不相互影响。

其中系统默认注册的5个mode有:

  • kCFRunLoopDefaultMode : App默认的mode,一般情况下App都是运行在这个mode下的。
  • UITrackingRunLoopMode : 界面跟踪时的mode,一般用于ScrollView滚动的时候追踪的,保证滑动的时候不受其他事件影响。
  • UIInitializationRunLoopMode : 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • GSEventReceiveRunLoopMode : 接受系统事件的内部 Mode,一般用不到。
  • kCFRunLoopCommonModes : 占位mode,可以向其中添加其他mode用以检测多个mode的事件

(3)CFRunLoopSourceRef

CFRunLoopSourceRef是事件源产生的地方,主要有两种:

  • Source0 :只包含一个函数指针(回调方法),不能自动触发,只能手动触发,触发方式是先通过CFRunLoopSourceSignal(source)将这个Source标记为待处理,然后再调用CFRunLoopWakeUp(runloop) 来唤醒RunLoop处理这个事件。

  • Source1 :基于port的Source源,包含一个port和一个函数指针(回调方法)。该Source源可通过内核和其他线程相互发送消息,而且可以主动唤醒RunLoop。

(4)CFRunLoopTimerRef

CFRunLoopTimerRef是基于事件的触发器,其中包含一段时间长度、延期容忍度和一个函数指针(回调方法)。当其加入到RunLoop中时,RunLoop会注册一个时间点,当到达这个时间点后,会触发对应的事件。

(5)performSEL

performSEL其实和NSTimer一样,是对CFRunLoopTimerRef的封装。因此,当调用performSelecter:afterDelay: 后,实际上内部会转化成CFRunLoopTimerRef并添加到当前线程的RunLoop中去,因此,如果当前线程中没有启动RunLoop的时候,该方法会失效。

(6)CFRunLoopObserverRef

CFRunLoopObserverRef是RunLoop的观察者。每个观察者都可以观察RunLoop在某个模式下事件的触发并处理。可以观察的时间点包括以下几点:

  • kCFRunLoopEntry:即将进入RunLoop
  • kCFRunLoopBeforeTimers:即将处理Timer
  • kCFRunLoopBeforeSources:即将处理Source
  • kCFRunLoopBeforeWaiting:即将进入休眠
  • kCFRunLoopAfterWaiting:刚从休眠中被唤醒
  • kCFRunLoopExit:即将退出RunLoop

(7)modeItem

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

2、RunLoop的驱动

 BOOL isRunning = NO; do {        isRunning = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDatedistantFuture]]; } while (isRunning);

RunLoop本身是不能循环的,要通过外部的while循环循环驱动。

3、RunLoop内部的基本流程

每次运行RunLoop,内部都会处理之前没有处理的消息,并且在各个阶段通知相应的观察者。大致步骤如下:

(1)通知观察者RunLoop启动
(2)通知观察者即将处理Timer
(3)通知观察者即将处理Source0
(4)触发Source0回调
(5)如果有Source1(基于port)处于ready状态,直接处理该Source1然后跳转到()去处理消息
(6)如果没有待处理消息,则通知观察者RunLoop所在线程即将进入休眠。
(7)休眠前,RunLoop会添加一个dispatchPort,底层调用mach_ msg接收mach_ port的消息。线程进入休眠,直到下面某个事件触发唤醒线程
  • 基于port的Source1事件到达
  • Timer时间到达
  • RunLoop启动时设置的最大超时时间到了
  • 手动唤醒
(8)唤醒后,将休眠前添加的dispatchPort移除,并通知观察者RunLoop已经被唤醒
(9)通过handle_ msg处理消息
(10)如果消息是Timer类型,则触发该Timer的回调
(11)如果消息是dispatch到main_ queue的block,执行block
(12)如果消息是Source1类型,则处理Source1回调
(13)以下条件中满足时候退出循环,否则从(2)继续循环
  • 事件处理完毕而且启动RunLoop的时候参数设置为一次性执行
  • 启动RunLoop时设置的最大运行时间到期
  • RunLoop被外部调用强行停止
  • 启动RunLoop的mode items为空
(14)上一步退出循环后退出RunLoop,通知观察者RunLoop退出

流程图如下:

这里写图片描述

二、系统利用RunLoop处理事件

1、自动释放池的创建与释放

程序启动后,通过打印主线程的RunLoop,我们可以看到,系统在主线程自动生成了两个观察者。

<CFRunLoopObserver 0x7fad438034b0 [0x10c874a40]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10c9fb4c2), context = <CFArray 0x7fad43803070 [0x10c874a40]>{type = mutable-small, count = 1, values = (    0 : <0x7fad44000048>)}}...<CFRunLoopObserver 0x7fad43809d30 [0x10c874a40]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10c9fb4c2), context = <CFArray 0x7fad43803070 [0x10c874a40]>{type = mutable-small, count = 1, values = (    0 : <0x7fad44000048>)}}

一个观察者监测RunLoop的kCFRunLoopEntry状态,最高优先级order = -2147483647。设置这个观察者的目的在于在RunLoop启动刚进入的时候生成自动释放池。由于优先级最高,因此先于所有其他回调操作生成。

另一个观察者监测RunLoop的kCFRunLoopBeforeWaiting状态和kCFRunLoopExit状态,此时是最低优先级order = 2147483647。该观察者的任务是当RunLoop在即将休眠时调用_objc_autoreleasePoolPop()销毁自动释放池,根据池中的记录向池中所有对象发送realse方法真正地减少其引用计数,并调用_objc_autoreleasePoolPush()创建新的自动释放池,当RunLoop即将退出的时候调用_objc_autoreleasePoolPop()销毁池子。

而主线程中执行的代码,通常都是写在RunLoop事件回调,Timer回调中,处于自动释放池范围内,因此不会出现内存泄漏,开发人员也不需要显示在主线程创建自动释放池了。

oc中的自动释放池本质上就是延迟释放,将向自动释放池中对象发送的释放消息存在pool中,当pool即将被销毁的时候向其中所有对象发送realse消息使其计数减小。而自动释放池的创建和销毁也是由RunLoop控制的。

2、识别硬件和手势

当一个硬件事件(触摸、锁屏、摇晃、加速等)发生后,先由IOKit.framework生成一个IOHIDEEvent事件并由SpringBoard接收,然后由mach port转发到需要处理的App进程中。

<CFRunLoopSource 0x7fad43801e30 [0x10c874a40]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fad43801970 [0x10c874a40]>{valid = Yes, port = 1e03, source = 0x7fad43801e30, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10eb2a444), context = <CFMachPort context 0x7fad42508470>}}···<CFRunLoopSource 0x7fad43801cb0 [0x10c874a40]>{signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fad43801690 [0x10c874a40]>{valid = Yes, port = 1d03, source = 0x7fad43801cb0, callout = __IOHIDEventSystemClientQueueCallback (0x10eb2a293), context = <CFMachPort context 0x7fad42508470>}}

上面第一个Source源中是对硬件可用性变化敏感的,当某个硬件可用性发生变化就会触发这个Source源,进程就会在__IOHIDEventSystemClientAvailabilityCallback回调方法中处理

另一个是对硬件事件进行处理的,当有硬件事件发生时,底层就会包装成一个IOHIDEvent事件,通过mach port转发到该Source1源中,触发__IOHIDEventSystemClientQueueCallback回调,并调用_UIApplicationHandleEventQueue()进行内部分发。这个方法会把IOHIDEvent进行预处理并包装成UIEvent进行处理和分发,包括UIGesture/处理屏幕旋转/发送给 UIWindow。

注意:当用户点击屏幕的时候最先是产生了一个硬件事件,底层封装后触发RunLoop的Source1源,回调__IOHIDEventSystemClientQueueCallback函数处理,然后再触发RunLoop中的一个Source0的源,回调_UIApplicationHandleEventQueue函数将事件包装成UIEvent处理并分发,通过函数调用栈可以看到:

0   test2                               0x000000010024356d -[UITestGestureRecognizer touchesBegan:withEvent:] + 1571   UIKit                               0x0000000101575bcf -[UIGestureRecognizer _touchesBegan:withEvent:] + 1132   UIKit                               0x00000001010fb196 -[UIWindow _sendGesturesForEvent:] + 3773   UIKit                               0x00000001010fc6c4 -[UIWindow sendEvent:] + 8494   UIKit                               0x00000001010a7dc6 -[UIApplication sendEvent:] + 2635   UIKit                               0x0000000101081553 _UIApplicationHandleEventQueue + 66606   CoreFoundation                      0x0000000100bf6301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 177   CoreFoundation                      0x0000000100bec22c __CFRunLoopDoSources0 + 5568   CoreFoundation                      0x0000000100beb6e3 __CFRunLoopRun + 8679   CoreFoundation                      0x0000000100beb0f8 CFRunLoopRunSpecific + 48810  GraphicsServices                    0x0000000104550ad2 GSEventRunModal + 16111  UIKit                               0x0000000101086f09 UIApplicationMain + 17112  test2                               0x0000000100243adf main + 11113  libdyld.dylib                       0x00000001034aa92d start + 1

调用栈显示,处理事件相应的时候是由Source0分发的event。而实际上这个事件的识别和包装是由Source0处理的。

3、手势识别

1   test2                               0x000000010c34e55f -[ViewController touchedButton] + 632   UIKit                               0x000000010d68ab28 _UIGestureRecognizerSendTargetActions + 1533   UIKit                               0x000000010d68719a _UIGestureRecognizerSendActions + 1624   UIKit                               0x000000010d685197 -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 8435   UIKit                               0x000000010d68d655 ___UIGestureRecognizerUpdate_block_invoke898 + 796   UIKit                               0x000000010d68d4f3 _UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks + 3427   UIKit                               0x000000010d67ae75 _UIGestureRecognizerUpdate + 26348   UIKit                               0x000000010d20748e -[UIWindow _sendGesturesForEvent:] + 11379   UIKit                               0x000000010d2086c4 -[UIWindow sendEvent:] + 84910  UIKit                               0x000000010d1b3dc6 -[UIApplication sendEvent:] + 26311  UIKit                               0x000000010d18d553 _UIApplicationHandleEventQueue + 666012  CoreFoundation                      0x000000010cd02301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 1713  CoreFoundation                      0x000000010ccf822c __CFRunLoopDoSources0 + 55614  CoreFoundation                      0x000000010ccf76e3 __CFRunLoopRun + 86715  CoreFoundation                      0x000000010ccf70f8 CFRunLoopRunSpecific + 48816  GraphicsServices                    0x000000011065cad2 GSEventRunModal + 16117  UIKit                               0x000000010d192f09 UIApplicationMain + 17118  test2                               0x000000010c34eaef main + 11119  libdyld.dylib                       0x000000010f5b692d start + 1

上述是点击事件的调用栈,可见,当发生点击并包装好事件后RunLoop会进行事件分发,然后会触发_UIGestureRecognizerUpdate,并调用其中的回调函数,最后发送给对应GestureRecognizer的target。

再回看RunLoop,我们也可以看到:

···<CFRunLoopSource 0x7fad4380a9b0 [0x10c874a40]>{signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>{version = 0, info = 0x7fad43900630, callout = _UIApplicationHandleEventQueue (0x10c9fbb4f)}}···<CFRunLoopObserver 0x7fad4388e010 [0x10c874a40]>{valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10ceea41f), context = <CFRunLoopObserver context 0x0>}

系统默认注册了一个Source0源用以分发事件,回调_UIApplicationHandleEventQueue方法,然后系统会将对应的UIGestureRecognizer标记为待处理,然后还注册了一个观察者,监听RunLoop即将进入休眠的事件,回调_UIGestureRecognizerUpdateObserver()函数,该函数内部会获取到所有被标记为待处理的UIGestureRecognizer,执行相应的回调。

4、界面刷新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

系统注册了一个观察者:

<CFRunLoopObserver 0x7fad42419d40 [0x10c874a40]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x110afb79c), context = <CFRunLoopObserver context 0x0>}

这个观察者监听RunLoop即将进入休眠和即将退出的事件,回调_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()方法,该方法中会遍历所有待处理的UIView或CALayer以执行绘制和调整,更新UI界面。

三、日常用法

日常情况下,除了系统使用RunLoop,某些情况下我们也可以在底层合理使用RunLoop机制达到功能要求。

1、线程池

线程池的使用是为了避免程序使用线程的时候频繁地创建和销毁线程从而造成不必要的过度消耗,因此通过线程池机制维持几个常驻线程,避免使用的时候频繁创建和销毁,达到节省资源和加快响应速度的目的。

但是除了主线程外的线程默认是创建后执行完毕销毁的,这个时候,如果还需要维护住该线程,则需要手动创建该线程对应的RunLoop。RunLoop启动后,可以向该RunLoop添加一个port或者Timer用以触发线程接收自己生成的事件,从而分发处理。

在这种方式中,RunLoop执行的主要任务是维持线程不退出,然后应用就可以利用performSelector:onThread:等方式将自己的事件传递给RunLoop处理了。著名的开源框架AFNetworking就是这样生成网络线程的。

+ (void)networkRequestThreadEntryPoint:(id)__unused object {    @autoreleasepool {        [[NSThread currentThread] setName:@"AFNetworking"];        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];        [runLoop run];    }}+ (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)start {    [self.lock lock];    if ([self isCancelled]) {        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];    } else if ([self isReady]) {        self.state = AFOperationExecutingState;        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];    }    [self.lock unlock];}

2、通过监听RunLoop处理事件

通过添加Observer监听主线程RunLoop,可以仿造主线程RunLoop进行UI更新,例如Facebook的开源框架AsyncDisplayKit。

AsyncDisplayKit框架就是仿造QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制。平时的时候将UI对象的创建、设置属性的事件放在队列中,通过在主线程添加Observer监听主线程RunLoop的kCFRunLoopBeforeWaiting 和 kCFRunLoopExit事件,在收到回调时,遍历队列里的所有事件并执行。

因此,通过合理利用RunLoop机制,可以将很多不是必须在主线程中执行的操作放在子线程中实现,然后在合适的时机同步到主线程中,这样可以节省在主线程执行操作的时间,避免卡顿。

参考文章:http://blog.ibireme.com/2015/05/18/runloop/
附上底层CFRunLoop源码链接:
https://github.com/opensource-apple/CF/blob/master/CFRunLoop.c

0 0