Runloop概述
关于runloop的概念,我觉得苹果文档已经阐述的相当到位了。原文翻译过来即为:runloop是与线程相关的基础架构的一部分。 runloop是一个事件处理循环,用于调度工作并协调传入事件的接收。 runloop的目的是在有工作时保持线程忙,并在没有事件处理时让线程进入休眠状态。
在iOS系统中,关于runloop的代码有2处,一处是CoreFoundation 框架内的CFRunLoopRef,提供了纯C函数的API且是线程安全的。另一处是Foundation框架内的NSRunLoop,其核心是对CFRunLoopRef在OC中的封装并无提供额外的其他功能,提供面向对象的API,线程不安全。
Runloop事件循环序列
根据苹果文档的描述,每次运行runloop时,runloop都会处理执行若干个task,并为任何外部附加的observers生成通知,即告知runloop当前处于什么状态。
runloop执行此操作的顺序为:
1.通知观察者已经进入了runloop
2.通知观察者任何准备好的Timer即将被触发
3.通知观察者任何非基于端口的输入源即将被触发(即Source0)
4.触发任何准备触发的基于非端口的输入源,即处理Source0回调
5.如果基于端口的输入源准备就绪并等待触发(即Source1),请立即处理该事件。 转到第9步
6.通知观察者thread即将睡眠
7.将线程置于睡眠状态,直到发生以下事件之一:
·事件到达基于端口的输入源,即Source1触发
·Timer触发
·为runloop设置的timeout到期
·runloop被明确唤醒
8.通知观察者thread刚刚醒来
9.处理待处理事件。
·如果触发了用户定义的Timer,则处理Timer事件并重新启动runloop。 转到第2步
·如果输入源被触发,则传递该事件
·如果runloop被明确唤醒但尚未超时,请重新启动循环。 转到第2步
10.通知观察者runloop已退出
其事件循环序列对应的runloop源码即为:
首先调用
if (currentMode->_observerMask &kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result= __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);//1
进入runloop的主函数__CFRunLoopRun
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);//2
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);//3
__CFRunLoopDoBlocks(rl, rlm);//执行被加入的block
在这里“执行被加入的block”应该怎么理解呢?runloop在运行时会处理若干个task,而其中有一种方式是可以被开发者使用的,方法为:
/**即通过CFRunLoopPerformBlock函数将一个block插入目标队列*/
void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void));
调用上面的函数方法,runloop在运行时,会通过__CFRunLoopDoBlocks(rl, rlm)执行队列里的所有block. 方法中有个CFTypeRef mode参数,即调用时定义的为哪种mode,那么在执行时也只执行与此mode相关的block. 关于mode的解释,后面会详细说明。
Boolean sourceHandledThisLoop =__CFRunLoopDoSources0(rl, rlm, stopAfterHandle);if(sourceHandledThisLoop) {
__CFRunLoopDoBlocks(rl, rlm);
}//4
Boolean poll= sourceHandledThisLoop || (0LL == timeout_context->termTSR);if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
msg= (mach_msg_header_t *)msg_buffer;if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), 0)) {goto handle_msg; //5
}
}if (!poll && (rlm->_observerMask &kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);//6
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);//7
__CFRunLoopUnsetSleeping(rl);if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);//8
之后进入步骤9,即如果有计时器(Timer)时间到了,将触发这个Timer的回调,即调用__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())。或者如果开发者调用GCD的API将任务放入主队列中,那么runloop会调用_dispatch_main_queue_callback_4CF(msg)来执行这个block。再或者runloop通过__CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort)判断Source1是否被触发,如果触发则调用__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)处理此事件。
在循环的最后,runloop会判断当前mode返回的原因,即CFRunLoopRunResult. 这是个枚举类型,分别为:
/*Reasons for CFRunLoopRunInMode() to Return*/typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
kCFRunLoopRunFinished= 1, //runloop没有要处理的source和timer
kCFRunLoopRunStopped = 2, //CFRunLoopStop在runloop中被调用
kCFRunLoopRunTimedOut = 3, //超出传入的标记时间
kCFRunLoopRunHandledSource = 4 //runloop被告知处理完此source就返回
};
当runloop循环标识位不为0时,循环停止。
最后调用
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); //10
从CFRunLoopRef的源码中去看,runloop主体就是一个do while循环,这点和runloop概念中提到的“事件处理循环”相吻合。依靠mach_msg系统调用,发送和接收Mach消息。mach_msg可以理解为系统级别的多进程之间的通信机制,使用相同的缓冲区来发送和接收消息(关于mach_msg的相关概念,感兴趣的同学可以去看CFRunLoopActivity状态),然后系统通过mach_msg内核函数对runloop进行唤醒或休眠操作,避免runloop在无事件处理时占用系统资源。
Runloop相关数据结构
runloop的数据结构分为:CFRunLoopRef,CFRunLoopModeRef,CFRunLoopSourceRef,CFRunLoopTimerRef,CFRunLoopObserverRef
CFRunloopRef其实就是__CFRunloop这个结构体指针,其核心方法即是我们上面讲的__CFRunLoopRun函数。对应的结构体源码为:
struct__CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock;/*locked for accessing mode list*/__CFPort _wakeUpPort;//used for CFRunLoopWakeUp
Boolean _ignoreWakeUps;volatile uint32_t *_stopped;
pthread_t _pthread;//代表线程,且runloop与线程是一一对应的
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;//每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为 _currentMode
CFMutableSetRef _modes; //包含多个mode的数据集合,其中mode的数据结构为CFRunLoopMode(NSMutableSet)
struct _block_item *_blocks_head;struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
_commonModes和_commonModeItems在接下来讲述mode的数据结构时再详细说明。
CFRunLoopModeRef对应的源码为:
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;
mach_port_t _timerPort;
};
从mode的源码中可以看到,mode就是为了实现runloop的逻辑行为而设计的。依据文档,mode是要监视的输入源(即Source0和Source1)和计时器的集合,以及要通知的RunLoopObserver的集合。并且runloop在运行时,在同一时段只能且必须(显式或隐式)在一种特定的mode下运行。在指定mode以后,仅监视与该mode关联的Source源并允许其传递其事件。那这个指定的mode就是在__CFRunLoop结构体中的_currentMode.
在Cocoa和Core Foundation框架中定义了标准mode以及何时使用该mode的说明。其分别为:
NSDefaultRunLoopMode(Cocoa),kCFRunLoopDefaultMode(Core Foundation): iOS系统默认的RunloopMode。大多数情况下,应使用此mode启动runloop并配置输入源。
NSConnectionReplyMode(Cocoa):Cocoa将此mode与NSConnection对象结合使用以监视回复。很少用到
NSModalPanelRunLoopMode(Cocoa):Cocoa使用此mode来识别用于modal panels的事件。(移动设备开发用不到)
UITrackingRunLoopMode(iOS),NSEventTrackingRunLoopMode(Cocoa):此mode限制鼠标拖动循环和其他种类的用户界面跟踪循环期间的传入事件。
NSRunLoopCommonModes(Cocoa),kCFRunLoopCommomModes(Core Foundation):这是一组可配置的常用模式,其为一组runloop mode的集合。对于Cocoa应用程序,此集合默认包括默认(NSDefaultRunLoopMode),模态(NSTaskDeathCheckMode)和事件(UITrackingRunLoopMode)跟踪模式。 Core Foundation最初只包含默认模式。 您可以使用CFRunLoopAddCommonMode函数将自定义模式添加到集合中。
这样来看__CFRunLoop结构体中的_commonModes和_commonModeItems就好理解了,一个 Mode 可以将自己标记为”common”属性(通过将其 ModeName 添加到runloop的 “commonModes” 中)。每当runloop的内容发生变化时,runloop都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “common” 标记的所有Mode里。
那么在runloop中runloop mode是怎么切换的呢?在源码中可以看到,为了保证线程安全,每回在mode切换时,必会有:
__CFRunLoopLock(rl);
..............
__CFRunLoopModeLock(rlm);
..............
__CFRunLoopModeUnLock(rlm);
..............
__CFRunLoopUnlock(rl);
所以大致可以认为mode切换有二种方式:一种是在runloop中途切换,一种是按顺序在当前mode结束之后切换。
CFRunLoopSourceRef是事件产生的地方,即事件源。Source分为两种:Source0和Source1
Source0:非基于内核Port的,用于用户主动触发的事件。只包含了一个回调(函数指针),需要手动唤醒线程让其处理事件。
Source1:基于内核Port的,包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。具备唤醒线程的能力。Source1在处理的时候会分发一些操作给Source0去处理,例如button点击事件。
CFRunLoopTimerRef基于事件的触发器,和NSTimer是toll-free bridge的(免费桥转换)。包含一个时间长度和一个回调(函数指针)。我们可以设置Timer的触发方式once或者repeat. 一个repeating timer重复触发依赖的时间是基于上一次的fire time的,并非实际的时间(有可能中间 runloopmode 有改变)。runloop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
CFRunLoopObserverRef对应的源码为:
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*/};
CFRunLoopObserver为runloop中的一个监听器,随时通知外部当前RunLoop的运行状态。其状态对应的枚举类型为:
/*Run Loop Observer Activities*/typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry= (1UL << 0), //进入runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer事件
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources事件
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠,将要进行用户态到内核态的切换
kCFRunLoopAfterWaiting = (1UL << 6), //即将进入唤醒,内核态到用户态的切换后不久
kCFRunLoopExit = (1UL << 7), //退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU};
AutoreleasePool的创建和释放就是监听上述状态的改变来实现的。
Runloop的应用
未完待续.......
!--
p.p1>!--
p.p1>!--
p.p1>!--
p.p1>!--
p.p1>!--
p.p1>!--
p.p1>