RunLoop(1)

2018-11-02  本文已影响0人  和风细羽

1. CFRunloopRef

CFRunloopRef 是纯 C 的函数,而 NSRunloop 仅仅是 CFRunloopRef 的 OC 封装,没有增加额外的功能,因此主要分析 CFRunloopRef。苹果已经开源了 CFRunloop 源代码

从代码可以看出 CFRunloopRef 其实是 __CFRunloop 这个结构体的指针。

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;       // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

rl->_blocks_head
rl->_commonModes

从代码的执行顺序 CFRunLoopRun() / CFRunLoopRunInMode() -> CFRunLoopRunSpecific() -> __CFRunloopRun() 可知 RunLoop 的核心方法是 __CFRunloopRun()。

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */

    if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
  
    return result;
}

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

为了方便阅读不再直接贴源代码,放一段伪代码:

int32_t __CFRunLoopRun()
{
    // 通知即将进入 runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);

    do
    {
        // 通知将要处理 timer 和 source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

        // 处理非延迟的主线程调用
        __CFRunLoopDoBlocks();
        // 处理 Source0 事件
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(...);

        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks();
         }
        /// 如果有 Source1 (基于 port) 处于 ready 状态,直接处理这个 Source1,然后跳转去处理消息。
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMsg = __CFRunLoopServiceMachPort();
            if (hasMsg) goto handle_msg;
        }

        /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
        if (!sourceHandledThisLoop) {
            __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
        }

        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();

        // 即将进入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

        // 等待内核 mach_msg 事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();

        // 等待 ...

        // 从等待中醒来
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

        // 处理因 timer 的唤醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();

        // 处理异步方法唤醒,如 dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()

        // 处理 Source1
        else
            __CFRunLoopDoSource1();

        // 再次确保是否有同步的方法需要调用
        __CFRunLoopDoBlocks();

    } while (!stop && !timeout);

    // 通知即将退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

现在只要了解上面的伪代码知道核心的方法 __CFRunLoopRun() 内部其实是一个 do while 循环,这也正是 Runloop 运行的本质。执行这个函数以后就一直处于 “等待-处理” 的循环之中,直到循环结束。只是不同于我们写的循环,它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是 Runloop 精华所在。

随着 Swift 的开源,苹果也维护着一个 Swift 版本的跨平台 CoreFoundation,除了 mac 平台,它还适配了 Linux 和 Windows 平台。

下图描述了 Runloop 运行流程(基本描述了上面 Runloop 的核心流程,当然可以查看官方 The Run Loop Sequence of Events 描述):

RunLoop

需要注意的是黄色区域的消息处理中并不包含 source0,因为它在循环开始之初就被处理了,之后的循环中不再处理。

整个流程其实就是一种 Event Loop 的实现,其他平台均有类似的实现,只是名称不同。

既然 RunLoop 是一个消息循环,谁来管理和运行 Runloop ?那么它接收什么类型的消息?休眠过程是怎么样的 ?如何保证休眠时不占用系统资源 ?如何处理这些消息以及何时退出循环?还有一系列问题需要解开。

注意:尽管 CFRunLoopPerformBlock() 在上图中作为唤醒机制(手动)有所体现,但事实上执行 CFRunLoopPerformBlock() 只是入队,下次 RunLoop 运行才会执行,而如果需要立即执行则必须调用 CFRunLoopWakeUp()。

2. Runloop Mode

从源码很容易看出,每次运行 __CFRunLoopRun() 函数时必须指定 Mode,Runloop 总是运行在某种特定的 CFRunLoopModeRef 下。

而通过 CFRunloopRef 对应的结构体 __CFRunLoop 的定义可以很容易知道每种 Runloop 都可以包含若干个 Mode,每个 Mode 又包含 Source/Timer/Observer。

struct __CFRunLoop {
    ...
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
};

struct __CFRunLoopMode {
     ...
     CFMutableSetRef _sources0;
     CFMutableSetRef _sources1;
     CFMutableArrayRef _observers;
     CFMutableArrayRef _timers;
     ...
};

每次调用 __CFRunLoopRun() 时指定的 Mode 是 _currentMode,当切换 Mode 时必须退出当前 Mode,然后重新进入 Runloop 以保证不同 Mode 的Source/Timer/Observer互不影响。

系统提供的 Mode 有

  • kCFRunLoopCommonModes(NSRunLoopCommonModes)
  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
  • UITrackingRunLoopMode。

进入 iOS 程序默认不做任何操作就处于 NSDefaultRunLoopMode 中,此时滑动视图,主线程就切换 Runloop 到 UITrackingRunLoopMode,不再接受其他事件操作,除非你将其他 Source/Timer 设置到 UITrackingRunLoopMode 下。

NSRunLoopCommonModes 并不是某种具体的 Mode,而是一种模式组合,在 iOS 系统中默认包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。

注意:并不是 Runloop 会运行在 kCFRunLoopCommonModes 这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。

当然你也可以通过调用 CFRunLoopAddCommonMode() 方法将自定义 Mode 放到 kCFRunLoopCommonModes 组合中)。

  • 系统框架自定义 Mode,例如 Foundation 中 NSConnectionReplyMode
  • 系统私有 Mode,例如:GSEventReceiveRunLoopMode 接受系统事件,UIInitializationRunLoopMode App 启动过程中初始化 Mode。

更多系统或框架 Mode 查看这里

CFRunLoopRef 和 CFRunloopMode、CFRunLoopSourceRef / CFRunloopTimerRef / CFRunLoopObserverRef 关系如下图:

RunLoopMode

那么 CFRunLoopSourceRef、CFRunLoopTimerRef 和 CFRunLoopObserverRef究竟是什么?它们在 Runloop 运行流程中起到什么作用呢?

3. Source

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

首先看一下官方 Runloop 结构图(注意下图右侧的 Input Source Port 和前面流程图中的 Source0 并不对应,而是对应 Source1。当然 Source0 是 Input Source 中的一类,Input Source 还包括 Custom Input Source,由其他线程手动发出。Source1 和 Timer 都属于端口事件源,不同的是所有的 Timer 都共用一个端口 "Mode Timer Port",而每个 Source1 都有不同的对应端口):

RunLoopSource

结合前面 RunLoop 核心运行流程可以看出 Source0(负责 App 内部事件,由 App 负责管理触发,例如 UITouch 事件) 和 Timer (又叫 Timer Source,基于时间的触发器,上层对应NSTimer) 是两个不同的 Runloop 事件源,RunLoop 被这些事件唤醒之后就会处理并调用事件处理方法(CFRunLoopTimerRef 和 CFRunLoopSourceRef 均包含对应的回调指针)。

但是对于 CFRunLoopSourceRef 除了 Source0 之外还有另一个版本就是 Source1,Source1 除了包含回调指针外包含一个 mach port,和 Source0 需要手动触发不同,Source1 可以监听系统端口和其他线程相互发送消息,它能够主动唤醒 RunLoop(由操作系统内核进行管理,例如 CFMessagePort 消息)。

官方也指出可以自定义 Source,因此对于 CFRunLoopSourceRef 来说它更像一种协议,框架已经默认定义了两种实现,如果有必要开发人员也可以自定义,官方文档

4. Observer

struct __CFRunLoopObserver {
     CFRuntimeBase _base;
     pthread_mutex_t _lock;
     CFRunLoopRef _runLoop;
     CFIndex _rlCount;
     CFOptionFlags _activities;      /* immutable */
     CFIndex _order;         /* immutable */
     CFRunLoopObserverCallBack _callout; /* immutable */
     CFRunLoopObserverContext _context;  /* immutable, except invalidation */
};

相对来说 CFRunloopObserverRef 理解起来并不复杂,它相当于消息循环中的一个监听器,随时通知外部当前 RunLoop 的运行状态(它包含一个函数指针 _callout 将当前状态及时告诉观察者)。具体的 Observer 状态如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),          // 进入 RunLoop 
     kCFRunLoopBeforeTimers = (1UL << 1),   // 即将开始Timer处理
     kCFRunLoopBeforeSources = (1UL << 2),  // 即将开始Source处理
     kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠
     kCFRunLoopAfterWaiting = (1UL << 6),   // 从休眠状态唤醒
     kCFRunLoopExit = (1UL << 7),           // 退出 RunLoop
     kCFRunLoopAllActivities = 0x0FFFFFFFU
};

5. Call out

开发过程中,无论是 Observer 的状态通知还是 Timer、Source 的处理,几乎所有的操作都是通过 Call out 进行回调的,而系统在回调时通常使用如下几个函数进行回调,换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听 Observer 也会先调用下面的函数然后间接通知你,所以在调用堆栈中经常看到这些函数:

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();

例如在控制器的 touchBegin 中打入断点查看堆栈(由于 UIEvent 是 Source0,所以可以看到一个 Source0 的 Call out 函数****CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION****调用):

RunLoop_Source0_UITouch

6. RunLoop 休眠

对于 Event Loop 而言,RunLoop 最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。

RunLoop 的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件 Darwin 中的 Mach 来完成的。可以从下图最底层 Kernel 中找到 Mach:

osx_architecture-kernels_drivers

Mach 是 Darwin 的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。

在 Mach 中,进程、线程间的通信是以消息的方式来完成的,消息在两个 Port 之间进行传递(这也正是 Source1 之所以称之为 Port-based Source 的原因,因为它就是依靠系统发送消息到指定的 Port 来触发的)。消息的发送和接收使用 <mach/message.h> 中的 mach_msg() 函数:

/**
 *  Routine:    mach_msg
 *  Purpose:
 *      Send and/or receive a message.  If the message operation
 *      is interrupted, and the user did not request an indication
 *      of that fact, then restart the appropriate parts of the
 *      operation silently (trap version does not restart).
 */
__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern mach_msg_return_t    mach_msg(
              mach_msg_header_t *msg,
              mach_msg_option_t option,
              mach_msg_size_t send_size,
              mach_msg_size_t rcv_size,
              mach_port_name_t rcv_name,
              mach_msg_timeout_t timeout,
              mach_port_name_t notify);

而 mach_msg() 的本质是一个调用 mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。当程序静止时,RunLoop 停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而这个函数内部就是调用了 mach_msg() 让程序处于休眠状态。

7. Runloop 与线程的关系

Runloop 是基于 pthread 进行管理的,pthread 是基于 c 的跨平台多线程操作底层 API。它是 mach thread 的上层封装(可以参见 Kernel Programming Guide),和 NSThread 一一对应。

pthread

苹果没有开放直接创建 Runloop 的接口,如果需要,通常调用 CFRunLoopGetMain() 和 CFRunLoopGetCurrent() 两个方法来获取。

通过代码不难发现,只有当我们使用线程的方法主动 get 时才会在第一次创建该线程的Runloop,同时将它保存在全局的字典中(线程和 Runloop 一一对应),默认情况下线程并不会创建 Runloop(主线程的 Runloop 比较特殊,任何线程创建之前都会保证主线程的已经存在),同时在线程结束的时也会销毁对应的 Runloop

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

NSRunloop 默认提供了三个常用的 run 方法:

- (void)run; 
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;

8. RunLoop应用

1、NSTimer

前面提到的 Timer Source 作为事件源,事实上它的上层对应就是 NSTimer(其实就是 CFRunloopTimerRef,底层基于使用 mk_timer 实现),甚至很多开发者接触 RunLoop 还是从 NSTimer 开始的。

其实 NSTimer 定时器的触发正是基于 RunLoop 运行的,所以使用 NSTimer 之前必须注册到 RunLoop。但是 RunLoop 为了节省资源并不会在非常准确的时间点调用定时器,如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行,并不会延后执行(NSTimer 提供了一个 tolerance 属性用于设置宽容度,如果确实想要 NSTimer 尽可能的准确,可以设置此属性)。

NSTimer 的创建通常有两种方式,尽管都是类方法,一种是 timerWithXXX:,另一种 scheduedTimerWithXXX:。

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

schedued 方式不仅创建一个定时器,而且会自动以 NSDefaultRunLoopMode 添加到当前线程 RunLoop 中,不添加到 RunLoop 中的 NSTimer 是无法正常工作的。

同时注意,如果触发滚动事件,NSDefaultRunLoopMode 下 NSTimer 是无法正常工作的,但将 NSDefaultRunLoopMode 改为 NSRunLoopCommonModes 则可以正常工作,这也解释了前面介绍的 Mode 内容。

@interface MyViewController ()
@property (nonatomic, weak) NSTimer * timer1;
@property (nonatomic, weak) NSTimer * timer2;
@end    

- (void)viewDidLoad
{
     [super viewDidLoad];

     self.timer1 = [NSTimer scheduledTimerWithTimeInterval:...];

     NSTimer * tempTimer = [NSTimer timerWithTimeInterval:...];
     // 如果不把 tempTimer 添加到 RunLoop 中是无法正常工作的
     [[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
     self.timer2 = tempTimer;
}

注意上面的 timer1 和 timer2 并没有强引用,对于其他的对象而言,执行完 viewDidLoad 方法后的的一个 RunLoop 运行结束,二者应该会被释放,但事实上二者并没有被释放。

为了确保定时器正常运转,当加入到 RunLoop 以后系统会对 NSTimer 执行一次 retain 操作。

特别注意:tempTimer 创建时并没直接赋值给 timer2,原因是 timer2 是 weak 属性,timerWithXXX: 方法创建的 NSTimer 默认并没有加入 RunLoop,如果直接赋值给 timer2 会被立即释放,只有加入 RunLoop 以后才可以将引用指向 timer2。

但是即使使用了弱引用,MyViewController 对象也无法正常释放

创建 NSTimer 时指定了 target : self,导致 NSTimer 对象 对 self 有一个强引用。

解决这个问题的方法通常有两种:
①、将 target 分离出来独立成一个对象,在对象内创建 NSTimer 并将对象本身作为 NSTimer 的 target,Controller 通过这个对象间接使用 NSTimer;

②、增加 NSTimer 分类,让 NSTimer 自身作为 target,同时可以将操作 selector 封装到 block 中。后者相对优雅,也是目前使用较多的方案,例如:NSTimer+Block

显然苹果也认识到了这个问题,如果你确保工程只支持 iOS 10 运行就可以使用iOS 10 新增的系统级 block方案。

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

}];

使用上面第 ② 种方法可以解决控制器无法释放的问题,但是会发现即使控制器被释放了两个定时器仍然正常运行,要解决这个问题就需要调用 NSTimer 的 invalidate 方法(注意:一次性的定时器执行完操作后会自动调用 invalidate 方法)。

- (void)dealloc 
{
     [self.timer1 invalidate];
     [self.timer2 invalidate];
}

其实和定时器相关的另一个问题大家也经常碰到,那就是 NSTimer 不是一种实时机制。官方文档明确说明:

在一个循环中,如果 RunLoop 没有被识别(这个时间大概在 50-100ms),或者说 currentRunLoop 在执行一个长的 call out(例如执行某个循环操作)则 NSTimer 可能就会存在误差,RunLoop 在下一次循环中继续检查并根据情况确定是否执行。

NSTimer 的执行时间总是固定在一定的时间间隔,例如 1:00:00、1:00:01、1:00:02、1:00:05 则跳过了第 4、5 次运行循环。

要演示这个问题请看下面的例子(注意:有些示例中可能会让一个线程中启动一个定时器,再在主线程启动一个耗时任务来演示这个问,如果实际测试可能效果不会太明显,因为现在的 iPhone 都是多核运算的,这样一来这个问题会变得相对复杂,因此下面的例子选择在同一个 RunLoop 中即加入定时器和执行耗时任务)

#import "MyViewController.h"

@interface MyViewController ()
@property (nonatomic, weak) NSTimer * timer;
@property (nonatomic, strong) NSThread * thread;
@end

@implementation MyViewController

- (void)dealloc
{
     [self.timer invalidate];   // 取消定时器
}

- (void)viewDidLoad
{
    [super viewDidLoad];
    
    self.thread = [[NSThread alloc] initWithTarget:self
                                          selector:@selector(performTask)
                                            object:nil];
    [self.thread start];
}

- (void)performTask
{
     // 使用下面的方式创建定时器虽然会自动加入到当前线程的 RunLoop 中,但是除了主线程外其他线程的 RunLoop 默认是不会运行的,必须手动调用
     __weak typeof(self) weakSelf = self;

     self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {

          if ([NSThread currentThread].isCancelled) {
               //[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(longTimeTask) object:nil];
               //[NSThread exit];
               [weakSelf.timer invalidate];
          }
          NSLog(@"111111111");
     }];

     NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);

     // 区分直接调用和「performSelector:withObject:afterDelay:」区别,下面的直接调用无论是否运行RunLoop一样可以执行,但是后者则不行。
     //[self longTimeTask];
     [self performSelector:@selector(longTimeTask) withObject:nil afterDelay:2.0];

     // 取消当前 RunLoop 中注册的 @selector(注意:只是当前 RunLoop,所以也只能在当前 RunLoop 中取消)
     // [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(longTimeTask) object:nil];
     NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);

     // 非主线程RunLoop必须手动调用
     [[NSRunLoop currentRunLoop] run];

     NSLog(@"注意:如果RunLoop 还在运行中,这里的代码并不会执行,RunLoop 本身就是一个循环.");
}

// 长时间任务:打印 9999 次
- (void)longTimeTask 
{
     for (int i = 0;i < 9999;++i) {
          NSLog(@"%i, %@", i, [NSThread currentThread]);
     
          if ([NSThread currentThread].isCancelled) {
               return;
          }
     }
}

// 取消线程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 
{
     [self.thread cancel];
}

@end

如果运行并且不退出上面的程序会发现,前两秒 NSTimer 可以正常执行,但是两秒后由于同一个 RunLoop 中 longTimeTask 循环操作的执行造成定时器跳过了中间执行的机会一直到 longTimeTask 循环完毕,这也正说明了 NSTimer 不是实时系统机制的原因。

以上程序还有几点需要说明一下:

①、NSTimer 会对 target 进行强引用直到任务结束或 exit 之后才会释放。如果上面的程序没有进行线程 cancel 而终止任务,则即使关闭控制器也无法正确释放。

②、非主线程的 RunLoop 并不会自动运行。同时注意,默认情况下非主线程的 RunLoop 直到第一次使用之前并不会自动创建,RunLoop 运行必须要在加入NSTimer 或 Source0、Sourc1、Observer 输入后运行否则会直接退出。例如上面代码如果 run 放到 NSTimer 创建之前,则既不会执行定时任务也不会执行循环运算。

③、performSelector:withObject:afterDelay: 执行的本质还是通过创建一个 NSTimer 然后加入到当前线程 RunLoop(通而过前后两次打印RunLoop信息可以看到此方法执行之后 RunLoop 的 timer 会增加 1 个。类似的还有performSelector:onThread:withObject:afterDelay:,只是它会在另一个线程的 RunLoop 中创建一个 Timer),所以此方法事实上在任务执行完之前会对触发对象形成引用,任务执行完进行释放(例如上面会对 MyViewController 形成引用,注意:performSelector: withObject: 等方法则等同于直接调用,原理与此不同)。

④、同时上面的代码也充分说明了 RunLoop 是一个循环事实,run 方法之后的代码不会立即执行,直到 RunLoop 退出。

⑤、上面程序的运行过程中如果突然 dismiss/pop,则程序的实际执行过程要分为两种情况考虑:如果循环任务 longTimeTask 还没有开始则会停止 timer 运行(停止了线程中第一个任务),然后等待 longTimeTask 执行并 break(停止线程中第二个任务)后线程任务执行结束释放对控制器的引用;如果循环任务 longTimeTask 执行过程中 dismiss/pop 则 longTimeTask 任务执行结束,等待timer 下个周期运行(因为当前线程的 RunLoop 并没有退出,timer 引用计数器并不为 0)时检测到线程取消状态则执行 invalidate 方法(第二个任务也结束了),此时线程释放对于控制器的引用。

CADisplayLink 默认时是一个执行频率 fps 和屏幕刷新相同的定时器,它也需要加入到 RunLoop 才能执行。

CADisplayLink 同样是基于 CFRunloopTimerRef 实现,底层使用 mk_timer。它比 NSTimer 精度更高(尽管 NSTimer 可以修改精度)。不过遇到大任务它和 NStimer 一样存在丢帧现象。

通常情况下 CADisaplayLink 用于构建帧动画,看起来相对更加流畅,而 NSTimer 则有更广泛的用处。

2、AutoreleasePool

AutoreleasePool是另一个与RunLoop相关讨论较多的话题。其实从RunLoop源代码分析,AutoreleasePool与RunLoop并没有直接的关系,之所以将两个话题放到一起讨论最主要的原因是因为在iOS应用启动后会注册两个Observer管理和维护AutoreleasePool。不妨在应用程序刚刚启动时打印currentRunLoop可以看到系统默认注册了很多个Observer,其中有两个Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**,这两个是和自动释放池相关的两个监听。

    <CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
    '' <CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}

第一个Observer会监听RunLoop的进入,它会回调objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象标志创建自动释放池。这个Observer的order是-2147483647优先级最高,确保发生在所有回调操作之前。
第二个Observer会监听RunLoop的进入休眠和即将退出RunLoop两种状态,在即将进入休眠时会调用**objc_autoreleasePoolPop() **和 **objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() **释放自动自动释放池内对象。这个Observer的order是2147483647,优先级最低,确保发生在所有回调操作之后。
主线程的其他操作通常均在这个AutoreleasePool之内(main函数中),以尽可能减少内存维护操作(当然你如果需要显式释放【例如循环】时可以自己创建AutoreleasePool否则一般不需要自己创建)。
其实在应用程序启动后系统还注册了其他Observer(例如即将进入休眠时执行注册回调_UIGestureRecognizerUpdateObserver用于手势处理、回调为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面实时绘制更新)和多个Source1(例如context为CFMachPort的Source1用于接收硬件事件响应进而分发到应用程序一直到UIEvent),这里不再一一详述。

3、UI更新

如果打印App启动之后的主线程RunLoop可以发现另外一个callout为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。
通常情况下这种方式是完美的,因为除了系统的更新,还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView或CALayer的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。

4、NSURLConnection

在前面的网络开发的文章中已经介绍过NSURLConnection的使用,一旦启动NSURLConnection以后就会不断调用delegate方法接收数据,这样一个连续的的动作正是基于RunLoop来运行。
一旦NSURLConnection设置了delegate会立即创建一个线程com.apple.NSURLConnectionLoader,同时内部启动RunLoop并在NSDefaultMode模式下添加4个Source0。其中CFHTTPCookieStorage用于处理cookie ;CFMultiplexerSource负责各种delegate回调并在回调中唤醒delegate内部的RunLoop(通常是主线程)来执行实际操作。
早期版本的AFNetworking库也是基于NSURLConnection实现,为了能够在后台接收delegate回调AFNetworking内部创建了一个空的线程并启动了RunLoop,当需要使用这个后台线程执行任务时AFNetworking通过**performSelector: onThread: **将这个任务放到后台线程的RunLoop中。

9. GCD 和 RunLoop的关系

在RunLoop的源代码中可以看到用到了GCD的相关内容,但是RunLoop本身和GCD并没有直接的关系。当调用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作是全部由libDispatch驱动的。

10. 更多 RunLoop 使用

前面看了很多RunLoop的系统应用和一些知名第三方库使用,那么除了这些究竟在实际开发过程中我们自己能不能适当的使用RunLoop帮我们做一些事情呢?

思考这个问题其实只要看RunLoopRef的包含关系就知道了,RunLoop包含多个Mode,而它的Mode又是可以自定义的,这么推断下来其实无论是Source1、Timer还是Observer开发者都可以利用,但是通常情况下不会自定义Timer,更不会自定义一个完整的Mode,利用更多的其实是Observer和Mode的切换。
例如很多人都熟悉的使用perfromSelector在默认模式下设置图片,防止UITableView滚动卡顿([[UIImageView alloc initWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。还有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空闲状态下计算出UITableViewCell的高度并进行缓存。再有老谭的PerformanceMonitor关于iOS实时卡顿监控,同样是利用Observer对RunLoop进行监视。

关于如何自定义一个Custom Input Source官网给出了详细的流程。

11. 学习文章

崔江涛(KenshinCui)# iOS刨根问底-深入理解RunLoop
一个低调的iOS开发 # Runloop 事件源

上一篇下一篇

猜你喜欢

热点阅读