iOS RunLoop

RunLoop
概念
数据结构
事件循环机制
RunLoop与NSTimer
RunLoop与对线程

一、RunLoop概念

RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象:

  • 没有消息需要处理时,休眠以避免资源占用;
    在这里插入图片描述
  • 有消息需要处理时,立刻被唤醒;
    在这里插入图片描述

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoopCFRunLoopRef

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

问题:main函数是如何保持不退出的?
main函数保持不退出

二、数据结构

通常所说的RunLoop指的是NSRunloop或者CFRunloopRefCFRunloopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRef的OC封装,并未提供额外的其他功能,苹果已经开源了CoreFoundation源代码,因此很容易找到CFRunloop源代码

__CFRunLoopRun内部其实是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数以后就一直处于“等待-处理”的循环之中,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源,当然这是由于系统内核负责实现的,也是Runloop精华所在。

  • CFRunLoop
  • CFRunLoopMode
  • Source/Timer/Observer
CFRunLoop
pthread
一一对应:RunLoop和线程的关系
currentMode
CFRunLoopMode
modes
NSMutableSet:<CGRunLoopMode*>
commonModes
NSMutableSet:<NSString*>
commonModeItems
Source/Timer/Observer

CFRunLoopRefCFRunloopModeCFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef关系如下图:
这里写图片描述
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

1、Runloop Mode

CFRunLoopMode
name
不同模式的名称:NSDefaultRunLoopMode
sources0
NSMutableSet
sources
observer
NSMutableArray
timers
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;
};

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

RunLoopModes:

  • NSDefaultRunLoopMode:默认的Runloop Mode,例如进入iOS程序默认不做任何操作就处于这种Mode中
  • UITrackingRunLoopMode:滑动UIScrollView,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作(除非你将其他Source/Timer设置到UITrackingRunLoopMode下)
  • kCFRunLoopCommonModes(NSRunLoopCommonModes):
    1. 一种模式组合,在iOS系统中默认包含了
      NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。
    2. 是同步Source/Timer/Observer到多个Mode中的一个技术方案

2、Source

首先看一下官方Runloop结构图(注意下图的Input Source Port和前面流程图中的Source0并不对应,而是对应Source1。Source1和Timer都属于端口事件源,不同的是所有的Timer都共用一个端口“Mode Timer Port”,而每个Source1都有不同的对应端口):
这里写图片描述
再结合前面RunLoop核心运行流程可以看出Source0(负责App内部事件,由App负责管理触发,例如UITouch事件)和Timer(又叫Timer Source,基于时间的触发器,上层对应NSTimer)是两个不同的Runloop事件源(当然Source0是Input Source中的一类,Input Source还包括Custom Input Source,由其他线程手动发出),RunLoop被这些事件唤醒之后就会处理并调用事件处理方法(CFRunLoopTimerRef的回调指针和CFRunLoopSourceRef均包含对应的回调指针)。
但是对于CFRunLoopSourceRef除了Source0之外还有另一个版本就是Source1,Source1除了包含回调指针外包含一个mach port,和Source0需要手动触发不同,Source1可以监听系统端口和其他线程相互发送消息,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)。

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

3、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
    };

4、timer

基于事件的定时器 和 NSTimer是toll-free bridged的。

5、Call out

在开发过程中几乎所有的操作都是通过Call out进行回调的(无论是Observer的状态通知还是Timer、Source的处理),而系统在回调时通常使用如下几个函数进行回调(换句话说你的代码其实最终都是通过下面几个函数来负责调用的,即使你自己监听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调用):
这里写图片描述

6、RunLoop休眠

其实对于Event Loop而言RunLoop最核心的事情就是保证线程在没有消息时休眠以避免占用系统资源,有消息时能够及时唤醒。RunLoop的这个机制完全依靠系统内核来完成,具体来说是苹果操作系统核心组件Darwin中的Mach来完成的(Darwin是开源的)。可以从下图最底层Kernel中找到Mach:
这里写图片描述
Mach是Darwin的核心,可以说是内核的核心,提供了进程间通信(IPC)、处理器调度等基础服务。在Mach中,进程、线程间的通信是以消息的方式来完成的,消息在两个Port之间进行传递(这也正是Source1之所以称之为Port-based Source的原因,因为它就是依靠系统发送消息到指定的Port来触发的)。消息的发送和接收使用<mach/message.h>中的mach_msg()函数(事实上苹果提供的Mach API很少,并不鼓励我们直接调用这些API):

 /*
     *  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**让程序处于休眠状态。

三、事件循环的实现机制

下图描述了Runloop运行流程
这里写图片描述

四、RunLoop与NSTimer

问题:滑动tableView的时候我们的定时器还会生效吗?
在这里插入图片描述
解决办法:
在这里插入图片描述

五、RunLoop 与线程的关系

  • 线程是和RunLoop一一对应的。
  • 自己创建的线程默认是没有RunLoop的。
  • 主线程的RunLoop默认是开启的。

问题:怎么样实现一个常驻线程

  • 为当前线程开启一个RunLoop、
  • 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
  • 启动该RunLoop。

实现方案:

#import "XYObject.h"

static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;

@implementation XYObject
+ (NSThread *)threadForDispatch {
    if (thread == nil) {
        @synchronized (self) {
            if (thread == nil) {
                // 线程的创建
                thread = [[NSThread alloc]initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"com.imooc.thread"];
            }
        }
    }
    return thread;
}
+ (void)runRequest {
    // 创建一个source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    // 创建RunLoop,同时向RunLoop的DefatltMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    // 如果可以运行
    while (runAlways) {
        @autoreleasepool {
            // 令当前RunLoop运行在DefaultMode下面
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    // 某一时机 静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}
@end

1、主线程相关联的RunLoop创建

// 创建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建主线程 根据传入的主线程创建主线程对应的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

2、创建与子线程相关联的RunLoop

// 从字典中获取子线程的runloop 
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops,pthreadPointer(t)); 
__CFUnlock(&loopsLock); 
if (!loop) { 
	// 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop 
	CFRunLoopRef newLoop = __CFRunLoopCreate(t); 
	__CFLock(&loopsLock); 
	loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
	// 把当前子线程和对应的runloop保存到字典中 
	if (!loop) { 
		CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); 
		loop = newLoop; 
	} 
	// don't release run loops inside the loopsLock, because 
CFRunLoopDeallocate may end up taking it 
	__CFUnlock(&loopsLock);
	CFRelease(newLoop);
}

Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装,和NSThread一一对应(而NSThread是一套面向对象的API,所以在iOS开发中我们也几乎不用直接使用pthread)。比如,你可以通过 pthread_main_np() 或 [NSThread mainThread] 来获取主线程;也可以通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。CFRunLoop 是基于 pthread 来管理的。

从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 里。所以我们创建子线程RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop];如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其 RunLoop。

[NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。

RunLoop 的销毁发生在线程结束时。

iOS开发过程中对于开发者而言更多的使用的是NSRunloop,它默认提供了三个常用的run方法:

- (void)run; 
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;
  • run方法对应上面CFRunloopRef中的CFRunLoopRun并不会退出,除非调用CFRunLoopStop();通常如果想要永远不会退出RunLoop才会使用此方法,否则可以使用runUntilDate。
  • runMode:beforeDate:只执行一次,执行完就退出;通常用于手动控制RunLoop(例如在while循环中)。
  • runUntilDate:执行完并不会退出,继续下一次RunLoop直到timeout。

六、RunLoop实例

1、AutoreleasePool

App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _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,优先级最低,确保发生在所有回调操作之后。

2、UI更新

当UI改变( Frame变化、 UIView/CALayer 的继承结构变化等)时,或手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理。

苹果注册了一个用来监听BeforeWaiting和Exit的Observer,在它的回调函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaoxiaobukuang

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值