美文网首页
iOS RunLoop

iOS RunLoop

作者: 搬砖的crystal | 来源:发表于2021-07-12 14:41 被阅读0次

    一、什么是RunLoop

    RunLoop就是控制线程生命周期并接收事件进行处理的机制。RunLoop是iOS事件响应与任务处理最核心的机制,贯穿整改iOS系统。

    RunLoop内部是do-while循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方。RunLoop实际是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如触摸事件、UI刷新事件、定时器事件、Selector事件)和消息,从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源。

    事件循环

    事件循环就是对事件/消息进行管理,事件循环可以达到:

    • 没有消息需要处理时,休眠线程以避免资源占用。从用户态切换到内核态,等待消息;
    • 有消息需要处理时,立刻唤醒线程,回到用户态处理消息;
    • 通过调用 mach_msg() 函数来转移当前线程的控制权给内核态/用户态。
    RunLoop 的基本作用
    • 保持程序的持续运行:
      如果没有RunLoop,main() 函数一执行完,程序就会立刻退出。
      而我们的 iOS 程序能保持持续运行的原因就是在 main() 函数中调用了 UIApplicationMain 函数,这个函数内部会启动主线程的 RunLoop;
    • 处理 App 中的的各种事件(比如触摸事件、定时器事件等);
    • 节省 CPU 资源,提高程序性能:该做事时做事,该休息时休息。
    1.RunLoop对象

    Foundation: NSRunLoop
    Core Foundation: CFRunLoop 核心部分,代码开源,C 语言编写,跨平台

    2.特性、与线程的关系
    • RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Source0、Source1)、Timer,那么就直接退出RunLoop。
    • RunLoop不能自己创建,RunLoop的创建在第一次获取时,在线程结束时销毁。
    • 每条线程都有唯一的一个与之对应的RunLoop对象,关系保存在全局的字典中。
    • 主线程的RunLoop对象默认已经启动,子线程的RunLoop对象需要手动创建。
    • 主线程的RunLoop在程序启动时,系统默认添加了有kCFRunLoopDefaultModeUITrackingRunLoopMode两个预制Mode,保证程序处于等待状态,如果接收到触摸事件等,就会执行任务,否则处于休眠中。

    iOS应用程序里面,程序启动后会有main()函数中的UIApplicationMain()函数,会给主线程设置一个NSRunLoop对象,这就解释了为什么应用在无人操作的时候休息,需要工作的时候能立马响应。

    3.获取RunLoop对象

    (1)Foundation

        //获取当前线程RunLoop
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        //获取主线程RunLoop
        NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
    

    (2)Core Foundation

        //获取当前线程RunLoop
        CFRunLoopRef currentRunLoop = CFRunLoopGetCurrent();
        //获取主线程RunLoop
        CFRunLoopRef mainRunLoop = CFRunLoopGetMain();
    
    4. RunLoop相关类
    (1)CFRunLoopRef
    (2)CFRunLoopModeRef

    RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer。
    每次RunLoop启动时,需要指定其中一个Mode,这个Mode被称作currentMode。如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做的目的是分隔开不同组的Source/Timer/Observer,让其互不影响。
    Mode主要是用来指定事件在运行循环中的优先级:

    • NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态。
    • UITrackingRunLoopMode:界面跟踪Mode,scrollView滑动时会切换到该Mode。
    • UIInitializationRunLoopMode:在RunLoop启动时,会切换到该Mode。
    • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合,一个 Mode 可以将自己标记为Common属性(通过将其 ModeName 添加到 RunLoop 的 commonModes中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common标记的所有Mode里。

    CFRunLoop对外暴漏的管理Mode的接口只有下面两个:

        // 给RunLoop添加到CommonMode中
        CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
        // 返回当前线程中指定mode的CFRunLoop对象
        CFRunLoopRunInMode(CFStringRef modeName, ...);
    

    Mode暴露的管理mode item的接口有下面几个:

        // 添加一个CFRunLoopSource对象到一个run loop mode中(如果添加的Source是source0的话,这个方法将会调用 schedule 回调在source的上下文结构(context structure)的指定方法)。一个runloop source 可以同时被注册到多个 runloop 和 runloop modes 中。当source被发出信号,无论哪一个被注册的 runloop 都会开始检测第一个发出信号的 source 。 如过rl的mode中已经包含source时,这个方法将不会做任何事。
        CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
        // 添加CFRunLoopObserver对象到一个run loop mode中去。 讨论:一个 runloop 观察者只能被同时注册在一个 runloop 中,尽管它可以被通过他的tunloop添加到多个runloop modes中。 如果rl已经在 mode中 包含 obsever 中,这个方法将不会做任何事。
        CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
        CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
        // 添加CFRunLoopTimer 对象到一个runloop mode中 讨论:一个runloop timer 在同一时刻只能注册在一个run loop,尽管它可以被通过他的tunloop添加到多个runloop modes中。 如果rl已经在 mode中 包含 obsever 中,这个方法将不会做任何事
        CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
        // 从run loop mode 移除 Observer 对象,如果 rl 没有包含参数中的Observer,则该函数不做任何处理
        CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
        // 从run loop mode 移除 timer 对象,如果 rl 没有包含参数中的timer,则该函数不做任何处理
        CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
    

    以上接口可以看出,只能通过ModeName操作内部Mode,当你传入一个新的ModeName但RunLoo内部没有对应的Mode时,RunLoo会自动帮你创建对应的CFRunLoopModeRef。并且官方文档明确指出,对于RunLoo来说,其内部的Mode只能增加不能删除。

    注意RunLoop不能在运行在NSRunLoopCommonModes模式,因为NSRunLoopCommonModes其实是个模式集合,而不是一个具体的模式,我可以添加事件源的时候使用NSRunLoopCommonModes,只要RunLoop运行在NSRunLoopCommonModes中任何一个模式,这个事件源都可以被触发。

    (3)CFRunLoopSourceRef

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

    • Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
    • Source1包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程。
    (4)CFRunLoopTimerRef

    是基于时间的触发器,它和NSTimer是toll-free bridged的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

    (5)CFRunLoopObserverRef

    是观察者,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry, // 即将进入Loop--1
        kCFRunLoopBeforeTimers, // 即将处理 Timer--2
        kCFRunLoopBeforeSources // 即将处理 Source--4
        kCFRunLoopBeforeWaiting, // 即将进入休眠--32
        kCFRunLoopAfterWaiting, // 刚从休眠中唤醒--64
        kCFRunLoopExit , // 即将退出Loop--128
    };
    
        CFRunLoopObserverRef observer =CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            NSLog(@"--监听到RunLoop状态发生改变--%zd",activity);
        });
        //添加观察者:监听RunLoop状态
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        //释放observer
        CFRelease(observer);
    

    二、RunLoop的方法

    1.NSRunLoop的运行
    (1)
    // 运行NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制
    - (void)run;
    

    无条件运行,不建议使用,因为这个接口会导致RunLoop永久性的在NSDefaultRunLoopMode模式。即使用CFRunLoopStop(runloopRef)也无法停止RunLoop的运行,除非能移除这个RunLoop上的所有事件源,包括定时器和source时间,不然这个子线程就无法停止,只能永久运行下去。

    (2)
    // 运行NSRunLoop:参数为时间期限,运行模式为默认的NSDefaultRunLoopMode模式
    - (void)runUntilDate:(NSDate *)limitDate;
    

    比上面的接口好点,有个超时时间,可以控制每次RunLoop的运行时间,也是运行在NSDefaultRunLoopMode模式。
    这个方法运行RunLoop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行RunLoop。注意CFRunLoopStop(runloopRef)也无法停止RunLoop的运行。

    (3)
    // 运行NSRunLoop:参数为运行模式、时间期限,返回值为YES表示处理事件后返回的,NO表示是超时或者停止运行导致返回的。
    - (BOOL)runMode:(NSString *)mode beforeDate:(NSDtate *)limitDate;
    

    从方法上来看,比上面多了一个参数,可以设置运行模式。由一点需要注意:这种运行方式是可以被CFRunLoopStop(runloopRef)所停止的。

    2.CFRunLoopRef的运行
    (1)
    // 运行CFRunLoopRef
    void CFRunLoopRun();
    

    运行在默认的kCFRunLoopDefaultMode模式下,知道CFRunLoopStop接口调用停止这个RunLoop,或者RunLoop的所有事件源被删除。
    NSRunLoop是基于CFRunLoop来封装的,NSRunLoop是线程不安全的,而CFRunLoop是线程安全的。
    在这里我们可以看到和上面NSRunLoop有一个直观的区别是:CFRunLoop能直接停止掉所有的CFRunLoop运行起来的RunLoop。

    (2)
    // 运行CFRunLoopRef:参数为运行模式、时间和是否在处理Input Source后退出标志,返回值是exit原因
    SInt32 CFRunLoopRunInMode(mode, second, returnAfterSourceHandled);
    

    其中第一个参数是指RunLoop运行的模式(例如kCFRunLoopDefaultMode或者kCFRunLoopCommonModes),第二个参数是运行事件,第三个参数是是否在处理事件后让RunLoop退出返回。
    关于返回值,我们知道调用RunLoop运行,代码是停在这一行不返回的,当返回的时候RunLoop就结束了,所以这个返回值就是RunLoop结束原因的返回,为一个枚举值。

    enum {
        kCFRunLoopRunFinished = 1, // Run Loop结束,没有Timer或者其他Input Source
        kCFRunLoopRunStopped = 2, // Run Loop被停止,使用CFRunLoopStop停止Run Loop
        kCFRunLoopRunTimedOut = 3, // Run Loop超时
        kCFRunLoopRunHandledSource = 4, // Run Loop处理完事件,注意Timer事件的触发是不会让Run Loop退出返回的,即使CFRunLoopRunInMode的第三个参数是YES也不行
    }
    
    (3)
    // 停止运行CFRunLoop
    void CFRunLoopStop(CFRunLoopRef rl);
    
    (4)
    // 唤醒CFRunLoopRef
    void CFRunLoopWakeUp(CFRunLoopRef rl);
    

    三、内部逻辑

    1.关系图

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


    关系图
    2.内部逻辑
    内部逻辑
    /// 用DefaultMode启动
    void CFRunLoopRun(void) {
        CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
    }
     
    /// 用指定的Mode启动,允许设置RunLoop超时时间
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
        return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
    }
     
    /// RunLoop的实现
    int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
        
        /// 首先根据modeName找到对应mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
        /// 如果mode里没有source/timer/observer, 直接返回。
        if (__CFRunLoopModeIsEmpty(currentMode)) return;
        
        /// 1. 通知 Observers: RunLoop 即将进入 loop。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
        
        /// 内部函数,进入loop
        __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
            
            Boolean sourceHandledThisLoop = NO;
            int retVal = 0;
            do {
     
                /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
                /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
                /// 4. RunLoop 触发 Source0 (非port) 回调。
                sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
     
                /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
                if (__Source0DidDispatchPortLastTime) {
                    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                    if (hasMsg) goto handle_msg;
                }
                
                /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
                if (!sourceHandledThisLoop) {
                    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
                }
                
                /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
                /// • 一个基于 port 的Source 的事件。
                /// • 一个 Timer 到时间了
                /// • RunLoop 自身的超时时间到了
                /// • 被其他什么调用者手动唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                    mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
                }
     
                /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
                
                /// 收到消息,处理消息。
                handle_msg:
     
                /// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。
                if (msg_is_timer) {
                    __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
                } 
     
                /// 9.2 如果有dispatch到main_queue的block,执行block。
                else if (msg_is_dispatch) {
                    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                } 
     
                /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
                else {
                    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                    if (sourceHandledThisLoop) {
                        mach_msg(reply, MACH_SEND_MSG, reply);
                    }
                }
                
                /// 执行加入到Loop的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
     
                if (sourceHandledThisLoop && stopAfterHandle) {
                    /// 进入loop时参数说处理完事件就返回。
                    retVal = kCFRunLoopRunHandledSource;
                } else if (timeout) {
                    /// 超出传入参数标记的超时时间了
                    retVal = kCFRunLoopRunTimedOut;
                } else if (__CFRunLoopIsStopped(runloop)) {
                    /// 被外部调用者强制停止了
                    retVal = kCFRunLoopRunStopped;
                } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                    /// source/timer/observer一个都没有了
                    retVal = kCFRunLoopRunFinished;
                }
                
                /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
            } while (retVal == 0);
        }
        
        /// 10. 通知 Observers: RunLoop 即将退出。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    }
    

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

    3.底层实现

    从上面代码可以看到,RunLoop的核心是基于mach port的,其进入休眠时调用的函数是mach_msg()。为了解释这个逻辑,下面稍微介绍一下OSX/iOS的系统架构。



    苹果官方将整个系统大致划分为上述4个层次:

    • 应用层包括用户能接触到的图形应用,例如Spotlight、Aqua、SpringBoard等。
    • 应用框架层即开发人员接触到的Cocoa等框架。
    • 核心框架层包括各种核心框架、OpenGL等内容。
    • Darwin即操作系统的核心,包括系统内核、驱动、Shell等内容,这一层是开源的,其所有源码都可以在 opensource.apple.com 里找到。

    我们在深入看一下 Darwin 这个核心的架构:



    其中,在硬件层上面的三个组成部分:Mach、BSD、IOKit (还包括一些上面没标注的内容),共同组成了XNU内核。
    XNU内核的内环被称作Mach,其作为一个微内核,仅提供了诸如处理器调度、IPC (进程间通信)等非常少量的基础服务。
    BSD层可以看作围绕 Mach层的一个外环,其提供了诸如进程管理、文件系统和网络等功能。
    IOKit层是为设备驱动提供了一个面向对象(C++)的一个框架。

    Mach本身提供的API非常有限,而且苹果也不鼓励使用Mach的API,但是这些API非常基础,如果没有这些API的话,其他任何工作都无法实施。在Mach中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对象”。和其他架构不同, Mach的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是Mach中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach的IPC(进程间通信)的核心。

    Mach 的消息定义是在 <mach/message.h> 头文件的,很简单:

    typedef struct {
      mach_msg_header_t header;
      mach_msg_body_t body;
    } mach_msg_base_t;
     
    typedef struct {
      mach_msg_bits_t msgh_bits;
      mach_msg_size_t msgh_size;
      mach_port_t msgh_remote_port;
      mach_port_t msgh_local_port;
      mach_port_name_t msgh_voucher_port;
      mach_msg_id_t msgh_id;
    } mach_msg_header_t;
    

    一条Mach消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口local_port和目标端口remote_port,发送和接受消息是通过同一个API进行的,其option标记了消息传递的方向:

    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陷阱(trap),即函数mach_msg_trap(),陷阱这个概念在Mach中等同于系统调用。当你在用户态调用mach_msg_trap()时会触发陷阱机制,切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作,如下图:



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

    四、RunLoop实现的功能

    1.autoreleasePool

    App启动之后,系统启动主线程并创建了RunLoop,在main thread中注册了两个observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler()
    第一个observer 监听的事件:即将进入Loop(kCFRunLoopEnter),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。优先级最高,确保创建释放池发生在其他回调之前。
    第二个observer监听了两个事件:
    1)即将进入休眠(kCFRunLoopBeforWaiting),此时调用_objc_autoreleasePoolPop()_objc_autoreleasePool Push()来释放久的池并创建新的池。
    2)即将退出Loop(kCFRunLoopExit)此时调用_objc_autoreleasePoolPop()释放自动释放池,这个observer的优先级最低,确保池子释放在所有回调之后。

    我们知道AutoRelease对象是被AutoReleasePool管理的,那么AutoRelease对象在什么时候被回收呢?
    1)手动干预释放
    手动干预释放指定autoreleasePool,在当前作用域大括号结束就立即释放。例如在for循环中使用autoreleasePool来管理一些临时变了的autorelease。

     for (int i = 0; i < 10; i ++) {
            @autoreleasepool {
                //操作
            }
        }
    

    2)系统自动释放
    不手动指定autoreleasePool,autorelease对象会在当前的RunLoop迭代结束时释放。
    kCFRunLoopEnter(1):第一次进入会自动创建autorelease对象。
    kCFRunLoopBeforeWaiting(32):进入休眠状态前会自动销毁一个autorelease,然后重新创建一个新的autorelease。
    kCFRunLoopExit(128):退出时RunLoop会自动销毁最后一个创建的autorelease。

    子线程在使用autorelease对象时,如果没有autoreleasePool会在autoreleasNoPage中懒加载一个。
    在RunLoop的run:beforDate,以及一些source的callback中,有autoreleasePool的push和pop操作,总结就是系统在很多地方都差不多都有autorelease的管理操作。
    就算没有pop也没关系,在线程退出的时候会释放资源,会清空autoreleasePool。

    2.NSTimer

    NSTimer的原型就是CFRunLoopTimerRef。一个Timer注册 RunLoop 之后,RunLoop 会为这个Timer的重复时间点注册好事件。
    有几点需要注意:
    1)RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer有个属性叫tolerance(宽容度),标示了当时间点到后,允许有多大最大误差,这个误差默认为0,可以手动设置这个误差值。为了防止时间点偏移,系统有权利给这个属性设置一个值,无论你设置的值是什么,即使RunLoop模式正确,当前线程并不阻塞,系统依然可能在Timer加上很小的容差。
    2)在哪个线程上调用NSTimer,就必须在哪个线程终止。
    3)RunLoop的默认model是NSDefaultRunLoopModel,当处理UI的时候,Timer就停止计时(参考轮播图在滑动界面时不轮播)。需要把RunLoop的model设置为NSRunLoopCommonModes(可以理解为:NSDefaultRunLoopMode+UITrackingRunLoopMode)。
    4)如果想要销毁Timer,则必须先调用invalidate使Timer置为失效,否则Timer就一直占用内存而不会释放,造成逻辑上的内存泄露。这种泄漏不能用Xcode及instruments测出来。
    NSTimer的八种创建方法:

    /**
     需要初始化一个NSInvocation对象,需要手动添加RunLoop,并且调用[timer fire]开始循环
     如果没有加入RunLoop或者没有fire都不会执行循环
     */
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    /**
     需要初始化一个NSInvocation对象,不需要调用fire,会在到设定的时间自动执行,并且自动加入RunLoop
     但如果想循环立即执行,需要调用fire
     */
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    /**
     需要手动加入RunLoop,并调用fire开始循环
     如果没有加入RunLoop或者没有fire都不会执行循环
     */
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    /**
     不需要手动调用fire,会在到设定的时间自动执行,并且自动加入RunLoop
     但如果想循环立即执行,需要调用fire
     */
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
    /**
     需要手动加入RunLoop,并调用fire开始循环
     如果没有加入RunLoop或者没有fire都不会执行循环
     */
    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    /**
     不需要手动调用fire,会在到设定的时间自动执行,并且自动加入RunLoop
     但如果想循环立即执行,需要调用fire
     */
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    /**
     需要手动加入RunLoop,不需要调用fire,会在到设定的循环时间自动执行
     */
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
    /**
     需要手动加入RunLoop,不需要调用fire,会在到设定的循环时间自动执行
     */
    - (instancetype)initWithFireDate:(NSDate *)date interval:(NSTimeInterval)ti target:(id)t selector:(SEL)s userInfo:(nullable id)ui repeats:(BOOL)rep;
    
    3.与GCD关系

    当调用dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的RunLoop发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()里执行这个 block。但这个逻辑仅限于dispatch到主线程,dispatch到其他线程仍然是由 libDispatch处理的。同理,GCD的dispatch_after在dispatch到main_queue时的timer机制才与RunLoop相关。

    4.PerformSelecter

    performSelecter:afterDelay:实际上其内部会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有RunLoop,则这个方法会失效。
    performSelector:onThread:实际上其会创建一个Timer加到对应的线程去,同样的,如果对应线程没有RunLoop该方法也会失效。

    -(void)viewDidLoad{
        [super viewDidLoad];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"1,%@",[NSThread currentThread]);
            [self performSelector:@selector(testMethod) withObject:nil afterDelay:0];
            NSLog(@"3,%@",[NSThread currentThread]);
        });
    }
    -(void)testMethod{
        NSLog(@"2,%@",[NSThread currentThread]);
    }
    //输出结果
    2021-07-08 14:41:22.012363+0800 DJTestDemo[47626:221235] 1,<NSThread: 0x6000002c2c80>{number = 6, name = (null)}
    2021-07-08 14:41:22.012581+0800 DJTestDemo[47626:221235] 3,<NSThread: 0x6000002c2c80>{number = 6, name = (null)}
    

    没有打印出2,需要创建RunLoop,并run起来。

    -(void)viewDidLoad{
        [super viewDidLoad];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"1,%@",[NSThread currentThread]);
            NSRunLoop *runloop = [NSRunLoop currentRunLoop];
            [self performSelector:@selector(testMethod) withObject:nil afterDelay:0];
            [runloop run];
            NSLog(@"3,%@",[NSThread currentThread]);
        });
    }
    -(void)testMethod{
        NSLog(@"2,%@",[NSThread currentThread]);
    }
    //输出结果
    2021-07-08 14:50:54.531266+0800 DJTestDemo[47747:228167] 1,<NSThread: 0x6000002acc00>{number = 6, name = (null)}
    2021-07-08 14:50:54.531485+0800 DJTestDemo[47747:228167] 2,<NSThread: 0x6000002acc00>{number = 6, name = (null)}
    2021-07-08 14:50:54.531596+0800 DJTestDemo[47747:228167] 3,<NSThread: 0x6000002acc00>{number = 6, name = (null)}
    

    打印出3,说明RunLoop退出了,创建的timer在触发performSelecter:afterDelay:方法后就销毁了。
    如果给当前RunLoop加入事件源或定时器timer,当前RunLoop就不会退出了,只是在不需要执行任务的时候进入休眠。其实这种方式有种说法也叫创建常驻线程(内存),AFNetworking也用到这种技法。

    -(void)viewDidLoad{
        [super viewDidLoad];
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
            NSLog(@"1,%@",[NSThread currentThread]);
            NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
                NSLog(@"timer");
            }];
            NSRunLoop *runloop = [NSRunLoop currentRunLoop];
            [runloop addTimer:timer forMode:NSRunLoopCommonModes];
            [self performSelector:@selector(testMethod) withObject:nil afterDelay:0];
            [runloop run];
            NSLog(@"3,%@",[NSThread currentThread]);
        });
    }
    -(void)testMethod{
        NSLog(@"2,%@",[NSThread currentThread]);
    }
    //输出结果
    2021-07-08 14:55:36.031211+0800 DJTestDemo[47803:231519] 1,<NSThread: 0x600001004b40>{number = 5, name = (null)}
    2021-07-08 14:55:36.031507+0800 DJTestDemo[47803:231519] 2,<NSThread: 0x600001004b40>{number = 5, name = (null)}
    2021-07-08 14:55:37.036029+0800 DJTestDemo[47803:231519] timer
    2021-07-08 14:55:38.034162+0800 DJTestDemo[47803:231519] timer
    2021-07-08 14:55:39.036318+0800 DJTestDemo[47803:231519] timer
    2021-07-08 14:55:40.035312+0800 DJTestDemo[47803:231519] timer
    2021-07-08 14:55:41.035354+0800 DJTestDemo[47803:231519] timer
    2021-07-08 14:55:42.035364+0800 DJTestDemo[47803:231519] timer
    ······
    

    注意,repeats参数要设置为YES,否则执行完timer之后,RunLoop就不再持有timer,RunLoop就退出来了。还可以通过[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];加入事件源的方法,使RunLoop一直不退出。

    5.事件响应

    苹果注册了一个 Source1(基于mach port的) 用来接收系统事件,其回调函数为__IOHIDEventSystemClientQueueCallback()
    当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接收。SpringBoard只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后用mach port转发给需要的App进程。随后苹果注册的那个Source1就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。

    事件响应过程

    相关文章

      网友评论

          本文标题:iOS RunLoop

          本文链接:https://www.haomeiwen.com/subject/gnbkyltx.html