iOS RunLoop

一、什么是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:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合,一个 Mode 可以将自己标记为Common属性(通过将其 ModeName 添加到 RunLoop 的 commonModes中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 Common标记的所有Mode里。
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到。

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.内部逻辑
内部逻辑

Source0 :触摸事件处理,performSelector:onThread:
Source1:基于 Port 的线程间通讯,系统事件捕捉
Timers:NSTimer,performSelector:withObject:afterDelay:
Observers:用于监听 RunLoop 状态,UI 刷新,AutoreleasePool

  1. 通知观察者runloop已经启动
  2. 通知观察者即将启动定时器
  3. 通知观察者即将启动非基于端口的事件源
  4. 启动任何准备好的非基于端口的事件源
  5. 如果基于端口的事件源准备好并且进入等待状态,立即启动,并进入步骤九
  6. 通知观察者线程进入休眠
  7. 将线程置于休眠直到下面任何事件的发生
    (1)某一事件到达基于端口的源
    (2)定时器启动
    (3)RunLoop设置的时间已经超时.(系统底层会给RunLoop设置一个超时时间,源码中设置的是:9999999999.0)
    (4)RunLoop被手动唤醒
  8. 通知观察者线程将被唤醒
  9. 处理未处理的事件
    (1)如果用户定义的定时器启动,处理定时器事件
    (2)如果事件源启动,传递相应的消息
    (3) 如果RunLooop被显示唤醒而且时间还没超时
    进入步骤2继续loop
  10. 通知观察者runloop结束
/// 用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()进行应用内部的分发。

事件响应过程

6.线程常驻

开启线程需要占用一定的内存空间(默认的情况下,主线程占 1 M,子线程占用 512 KB)且每次开辟子线程都会消耗 CPU。如果频繁使用子线程的情况下,频繁开辟释放子线程会消耗大量的 CPU 和内存,而且创建的线程中的任务执行完成之后也就释放了,不能再次利用,所以造成资源和性能的浪费。这种情况下可以通过创建一个常驻线程来解决。

常驻线程通过 NSThreadRunLoop 来实现。新建的子线程默认没有开启 RunLoop,因此需要给这个线程添加了一个 RunLoop,并且加了一个 NSMachPort 端口监听,防止新建的线程由于没有活动直接退出。

#pragma mark- 异步常驻线程
+ (void)logThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"com.log.LOGServiceThread"];
        
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)logThread {
    static NSThread *_logThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _logThread = [[NSThread alloc] initWithTarget:self selector:@selector(logThreadEntryPoint:) object:nil];
        [_logThread start];
    });
    
    return _logThread;
}

使用

[self performSelector:@selector(token) onThread:[[self class] logThread] withObject:nil waitUntilDone:NO];

退出

[NSRunLoop currentRunLoop]removePort:<#(nonnull NSPort *)#> forMode:<#(nonnull NSRunLoopMode)#>

只有从 RunLoop 中移除我们之前添加的端口,这样 RunLoop 没有任何事件,所以直接退出。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,444评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,421评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,036评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,363评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,460评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,502评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,511评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,280评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,736评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,014评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,190评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,848评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,531评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,159评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,411评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,067评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,078评论 2 352

推荐阅读更多精彩内容

  • 1 Runloop机制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi阅读 4,002评论 4 30
  • 最近看了很多RunLoop的文章,看完很懵逼,决心整理一下,文章中大部分内容都是引用大神们的,但好歹对自己有个交代...
    小凉介阅读 6,707评论 12 79
  • 概述 RunLoop作为iOS中一个基础组件和线程有着千丝万缕的关系,同时也是很多常见技术的幕后功臣。尽管在平时多...
    阳明先生_X自主阅读 1,100评论 0 17
  • 阅读本篇文章需要有一定的runloop基础、runloop的基础认知还请先自行搜索 RunLoop运行流程 代码运...
    Avery_AN阅读 535评论 0 0
  • 简介 RunLoop在OS X/ iOS中一项比较基础的知识点,虽然基础,但是十分重要。它与线程息息相关,是用于处...
    nuclear阅读 1,028评论 1 20