一.RunLoop结构模型
1.概念
RunLoop
是通过内部维护的事件循环(Event Loop
)来对事件/消息进行管理的一个对象。
- 没有消息处理时,就会进行休眠避免资源占用,由
用户态
切换到内核态
(CPU-内核态和用户态) - 有消息需要处理时,立刻被唤醒,由
内核态
切换到用户态
Event Loop模型
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message)
} while (message != quit);
}
OSX/iOS
系统中,提供了两个这样的对象:NSRunLoop
和 CFRunLoopRef
。
CFRunLoopRef
是在 CoreFoundation
框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的
。
NSRunLoop
是基于 CFRunLoopRef
的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
CFRunLoopRef
的代码是开源的,你可以在这里下载到整个 CoreFoundation 的源码来查看。
2.RunLoop
数据结构
NSRunLoop(Foundation)
是CFRunLoop(CoreFoundation)
的封装,提供了面向对象的API
结构如下
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; //存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //存储的是CFRunLoopModeRef
struct _block_item *_blocks_head;//doblocks的时候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
一个RunLoop对象
,主要包含了一个线程
,若干个Mode
,若干个commonMode
,还有一个当前运行的Mode
。
RunLoop
相关的主要涉及五个类:
CFRunLoop
:RunLoop对象CFRunLoopMode
:运行模式CFRunLoopSource
:输入源/事件源CFRunLoopTimer
:定时源,基于时间的触发器CFRunLoopObserver
:用于监听runLoop运行状态的观察者
2.1.CFRunLoop
CFRunLoop
由五部分组成:
pthread
:线程对象,说明RunLoop
和线程
是一一对应
的currentMode
:当前所处的运行模式modes
:多个运行模式的集合commonModes
:模式名称字符串集合commonModelItems
:Observer,Timer,Source
集合
typedef struct __CFRunLoop * CFRunLoopRef;
///CFRunLoop.c 结构体
struct __CFRunLoop {
//..
CFMutableSetRef _commonModes; // <Set> String UITrackingRunLoopMode/kCFRunLoopDefaultMode
CFMutableSetRef _commonModeItems;// <Set> observer/source/timer
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //内置的modes;
//...
};
2.2.CFRunLoopMode
由name、source0、source1、observers、timers
构成
///CFRunLoop.c
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name; //mode名称
Boolean _stopped; //mode是否被终止
char _padding[3];
//几种事件
CFMutableSetRef _sources0; //sources0
CFMutableSetRef _sources1; //sources1
CFMutableArrayRef _observers; //通知
CFMutableArrayRef _timers; //定时器
CFMutableDictionaryRef _portToV1SourceMap; //字典 key是mach_port_t,value是CFRunLoopSourceRef
__CFPortSet _portSet; //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
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 */
};
苹果文档中提到的 Mode
有五个,分别是:
NSDefaultRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
NSEventTrackingRunLoopMode
NSRunLoopCommonModes
iOS中公开暴露出来的只有 NSDefaultRunLoopMode
和 NSRunLoopCommonModes
。 NSRunLoopCommonModes
实际上是一个 Mode 的集合
,默认包括 NSDefaultRunLoopMode
和 NSEventTrackingRunLoopMode
(注意:并不是说Runloop
会运行
在 kCFRunLoopCommonModes
这种模式下,而是相当于分别注册
了 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
。当然你也可以通过调用CFRunLoopAddCommonMode()
方法将自定义Mode
放到 kCFRunLoopCommonModes
组合)。
///`RunLoop`指定`Mode`运行
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
程序在运行的过程中,会处理基于时间的、系统的、用户的事件,这些事件在程序运行期间有着不同的优先级,为了满足应用层依据优先级对这些事件的管理,系统采用RunLoopMode
对这些事件分组,然后交由RunLoop
去管理。除了系统定义的默认模式和常用模式,我们也可以自定义模式,但是自定义的模式中必须有关联的事件,否则自定义模式没有任何意义。
_commonModeItems
与_commonModes
是kCFRunLoopCommonModes (NSRunLoopCommonModes)
背后的实现逻辑,可以理解为采用RunLoopMode
对事件进行分组后,我们又希望一些事件可以同时被多个Mode
处理,于是我们将这些事件(sources/timers/observers
)放入_commonModeItems
,将需要同时处理这些事件的多个Mode
放入_commonModes
集合进行标记;当事件指定kCFRunLoopCommonModes
模式进行添加时,先会添加到_commonModeItems
中,然后将_commonModeItems
中的所有事件追加到_commonModes
中已经标记的模式下。
///添加一个`Mode`到`RunLoop`的`commonMode`集合中,一旦添加无法移除。只加不减
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFRunLoopMode mode);
举个例子:
主线程默认运行在kCFRunLoopDefaultMode
下,当我们滑动ScrollView
时会切换到UITrackingRunLoopMode
,而主线程的RunLoop
的_commonModes
默认包含这两种模式;
开发中会遇到在主线程启动一个定时器时,会受视图滑动的影响的问题,解决办法:
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
将定时器添加到NSRunLoop
对象的NSRunLoopCommonModes
模式下,最终定时器会被添加到kCFRunLoopDefaultMode
和UITrackingRunLoopMode
下;当然也可以自行添加到这两个模式中。
查看CoreFoundation
中CFRunLoopAddTimer
方法的实现,可以更深入的理解_commonModeItems
与_commonModes
的工作原理:
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
///...
if (modeName == kCFRunLoopCommonModes) { ///是否是`kCFRunLoopCommonModes`
///取`Runloop`的`_commonModes`
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) {
///创建`_commonModeItems`<Set>
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
///添加定时器到`_commonModeItems`
CFSetAddValue(rl->_commonModeItems, rlt);
if (NULL != set) { //`_commonModes`有值
CFTypeRef context[2] = {rl, rlt};
/* add new item to all common-modes */
///为Set集合中的每个元素都调用该方法
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
} else {
///非`kCFRunLoopCommonModes`,先找找`runloop`的modes是否有,找不到创建
CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
if (NULL != rlm) {
if (NULL == rlm->_timers) { ///创建存放`timer`的数组
CFArrayCallBacks cb = kCFTypeArrayCallBacks;
cb.equal = NULL;
rlm->_timers = CFArrayCreateMutable(kCFAllocatorSystemDefault, 0, &cb);
}
}
/// mode有了,定时器对象的modes又没有该mode
if (NULL != rlm && !CFSetContainsValue(rlt->_rlModes, rlm->_name)) {
///...
if (NULL == rlt->_runLoop) {
rlt->_runLoop = rl;
} else if (rl != rlt->_runLoop) {
//...
//定时器已关联的runloop与当前runloop不一致,返回
return;
}
///定时器的modes 添加该mode的名称
CFSetAddValue(rlt->_rlModes, rlm->_name);
//采用mktimer(mach kernel timer)通过machport 和 machmsg 触发定时器事件
__CFRepositionTimerInMode(rlm, rlt, false);
///...
}
///...
}
///..
}
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);//add source
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName); // add observer
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName); // add timer
}
}
CoreFoundation
中向指定RunLoopMode
中添加和移除事件的函数有:
//Source
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);
//Timer
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
//Observer
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);
2.3.CFRunLoopSource
结构如下
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; ///同observer
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; //source0
CFRunLoopSourceContext1 version1; //source1
} _context;
};
typedef struct {
CFIndex version;
void * info;
const void *(*retain)(const void *info);
void (*release)(const void *info);
CFStringRef (*copyDescription)(const void *info);
Boolean (*equal)(const void *info1, const void *info2);
CFHashCode (*hash)(const void *info);
void (*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);
void (*perform)(void *info);
} CFRunLoopSourceContext;
typedef struct {
///...同`CFRunLoopSourceContext`前7个属性
#if TARGET_OS_OSX || TARGET_OS_IPHONE
mach_port_t (*getPort)(void *info);
void * (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
///...
#endif
} CFRunLoopSourceContext1;
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
分为 source0
和 source1
两种
source0
:- 即
非基于port
的,也就是用户触发的事件
。需要手动唤醒线程
,将当前线程从内核态切换到用户态
- 即
source1
:基于port
的,包含一个mach_port
和一个回调
,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop
,接收分发系统事件。- 具备
唤醒线程
的能力
2.4.CFRunLoopTimer
基于时间的触发器
,基本上说的就是 NSTimer
(CFRunLoopTimer
和 NSTimer
是toll-free bridged的,可以相互转换)。在预设的时间点唤醒 RunLoop
执行 回调。
因为它是基于 RunLoop
的,因此它不是实时的
(就是 NSTimer
是不准确的
, 因为 RunLoop
只负责 分发源的消息
。如果线程当前正在处理繁重的任务
,就有可能导致 Timer本次延时
,或者少执行一次
)。
结构如下
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits; //标记fire状态
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; //添加该timer的runloop
CFMutableSetRef _rlModes; //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; //理想时间间隔 /* immutable */
CFTimeInterval _tolerance; //时间偏差 /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
2.5.CFRunLoopObserver
CFRunLoopObserver
是观察者,可以观察RunLoop
的各种状态,并抛出回调。
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
可以观察的状态有如下6种:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入run loop
kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//被唤醒但是还没开始处理事件
kCFRunLoopExit = (1UL << 7),//run loop已经退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
3.RunLoop
内部逻辑
Runloop
通过监控 Source
来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer
来监控 Runloop
本身的状态。 Runloop Observer
可以监控上面的 Runloop
事件,具体流程如下图。
顺序如下:
- 通知观察者RunLoop已经启动
- 通知观察者即将要开始的定时器
- 通知观察者任何即将启动的非基于端口的源
- 启动任何准备好的非基于端口的源
- 如果基于端口的源准备好并处于等待状态,立即启动,并进入步骤9
- 通知观察者线程进入休眠状态
- 将线程置于休眠直到任一下面的事件发生:
- 某一事件到达基于端口的源
- 定时器启动
- RunLoop设置的时间已经超时
- RunLoop被显示唤醒
- 通知观察者线程将被唤醒
- 处理未处理的事件
- 如果用户定义的定时器启动,处理定时器事件并重启RunLoop,进入步骤2
- 如果输入源启动,传递相应的消息
- 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
- 通知观察者RunLoop结束。
3.1.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
总是运行在某种特定的CFRunLoopModeRef
下(每次运行__CFRunLoopRun()函数
时必须指定 Mode
),该Mode
会被设置为_currentMode
,只有与该Mode
关联的输入源source0
、source1
才能被处理,同样的,监听RunLoop
的运行,只有与该Mode
关联的observers
才能收到通知。
3.2.CFRunLoopRunSpecific
/*
* RunLoop的实现,指定mode运行runloop
* @param rl 当前运行的runloop
* @param modeName 需要运行的mode的name
* @param seconds runloop的超时时间
* @param returnAfterSourceHandled 是否处理完事件就返回
*/
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根据modeName找到对应mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
//如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
if (NULL == currentMode ||
__CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false; if (currentMode)
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
//取上一次运行的mode
CFRunLoopModeRef previousMode = rl->_currentMode;
//如果本次mode和上次的mode一致
rl->_currentMode = currentMode;
//初始化一个result为kCFRunLoopRunFinished
int32_t result = kCFRunLoopRunFinished;
/// 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;
}
/// 6.通知 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);
/// 9.收到消息,处理消息。
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);
}
__CFRunLoopModeIsEmpty() {
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
return true;
}
通过 CFRunLoopRunSpecific
的内部逻辑,我们可以得出:
- 如果指定了一个
不存在的mode
来运行RunLoop
,那么会失败,mode
不会
被创建
,所以这里传入的mode
必须是存在
的 - 如果指定了一个
mode
,但是这个mode
中不包含任何 modeItem,那么 RunLoop 也不会运行,所以必须要* 传入至少包含一个 modeItem的mode - 在进入 runloop 之前通知 observer,状态为 kCFRunLoopEntry
- 在退出 runloop 之后通知 observer,状态为 kCFRunLoopExit
RunLoop
的运行的最核心函数是 __CFRunLoopRun
,接下来我们分析 __CFRunLoopRun
的源码。
3.3.__CFRunLoopRun
/**
* 运行run loop
*
* @param rl 运行的RunLoop对象
* @param rlm 运行的mode
* @param seconds run loop超时时间
* @param stopAfterHandle true:run loop处理完事件就退出 false:一直运行直到超时或者被手动终止
* @param previousMode 上一次运行的mode
*
* @return 返回4种状态
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
//获取系统启动后的CPU运行时间,用于控制超时时间
uint64_t startTSR = mach_absolute_time();
//如果RunLoop或者mode是stop状态,则直接return,不进入循环
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
//mach端口,在内核中,消息在端口之间传递。 初始为0
mach_port_name_t dispatchPort = MACH_PORT_NULL;
//判断是否为主线程
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
//如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
#if USE_DISPATCH_SOURCE_FOR_TIMERS
mach_port_name_t modeQueuePort = MACH_PORT_NULL;
if (rlm->_queue) {
//mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
if (!modeQueuePort) {
CRASH("Unable to get port for run loop mode queue (%d)", -1);
}
}
#endif
//GCD管理的定时器,用于实现runloop超时机制
dispatch_source_t timeout_timer = NULL;
struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
//立即超时
if (seconds <= 0.0) { // instant timeout
seconds = 0.0;
timeout_context->termTSR = 0ULL;
}
//seconds为超时时间,超时时执行__CFRunLoopTimeout函数
else if (seconds <= TIMER_INTERVAL_LIMIT) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_retain(timeout_timer);
timeout_context->ds = timeout_timer;
timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
dispatch_resume(timeout_timer);
}
//永不超时
else { // infinite timeout
seconds = 9999999999.0;
timeout_context->termTSR = UINT64_MAX;
}
//标志位默认为true
Boolean didDispatchPortLastTime = true;
//记录最后runloop状态,用于return
int32_t retVal = 0;
do {
//初始化一个存放内核消息的缓冲池
uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *msg = NULL;
mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
HANDLE livePort = NULL;
Boolean windowsMessageReceived = false;
#endif
//取所有需要监听的port
__CFPortSet waitSet = rlm->_portSet;
//设置RunLoop为可以被唤醒状态
__CFRunLoopUnsetIgnoreWakeUps(rl);
//2.通知observer,即将触发timer回调,处理timer事件
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
//3.通知observer,即将触发Source0回调
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
//执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
//4.处理source0事件
//有事件处理返回true,没有事件返回false
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
//执行加入当前runloop的block
__CFRunLoopDoBlocks(rl, rlm);
}
//如果没有Sources0事件处理 并且 没有超时,poll为false
//如果有Sources0事件处理 或者 超时,poll都为true
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
//第一次do..whil循环不会走该分支,因为didDispatchPortLastTime初始化是true
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
//从缓冲区读取消息
msg = (mach_msg_header_t *)msg_buffer;
//5.接收dispatchPort端口的消息,(接收source1事件)
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
//如果接收到了消息的话,前往第9步开始处理msg
goto handle_msg;
}
#elif DEPLOYMENT_TARGET_WINDOWS
if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
goto handle_msg;
}
#endif
}
didDispatchPortLastTime = false;
//6.通知观察者RunLoop即将进入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
//设置RunLoop为休眠状态
__CFRunLoopSetSleeping(rl);
// do not do any user callouts after this point (after notifying of sleeping)
// Must push the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced.
__CFPortSetInsert(dispatchPort, waitSet);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//这里有个内循环,用于接收等待端口的消息
//进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
do {
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
//7.接收waitSet端口的消息
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
//收到消息之后,livePort的值为msg->msgh_local_port,
if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
// Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
if (rlm->_timerFired) {
// Leave livePort as the queue port, and service timers below
rlm->_timerFired = false;
break;
} else {
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
}
} else {
// Go ahead and leave the inner loop.
break;
}
} while (1);
#else
if (kCFUseCollectableAllocator) {
objc_clear_stack(0);
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
#elif DEPLOYMENT_TARGET_WINDOWS
// Here, use the app-supplied message queue mask. They will set this if they are interested in having this run loop receive windows messages.
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, poll ? 0 : TIMEOUT_INFINITY, rlm->_msgQMask, &livePort, &windowsMessageReceived);
#endif
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
// Must remove the local-to-this-activation ports in on every loop
// iteration, as this mode could be run re-entrantly and we don't
// want these ports to get serviced. Also, we don't want them left
// in there if this function returns.
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
// user callouts now OK again
//取消runloop的休眠状态
__CFRunLoopUnsetSleeping(rl);
//8.通知观察者runloop被唤醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
//9.处理收到的消息
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
#if DEPLOYMENT_TARGET_WINDOWS
if (windowsMessageReceived) {
// These Win32 APIs cause a callout, so make sure we're unlocked first and relocked after
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
if (rlm->_msgPump) {
rlm->_msgPump();
} else {
MSG msg;
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE | PM_NOYIELD)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
// To prevent starvation of sources other than the message queue, we check again to see if any other sources need to be serviced
// Use 0 for the mask so windows messages are ignored this time. Also use 0 for the timeout, because we're just checking to see if the things are signalled right now -- we will wait on them again later.
// NOTE: Ignore the dispatch source (it's not in the wait set anymore) and also don't run the observers here since we are polling.
__CFRunLoopSetSleeping(rl);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
__CFRunLoopWaitForMultipleObjects(waitSet, NULL, 0, 0, &livePort, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
__CFRunLoopUnsetSleeping(rl);
// If we have a new live port then it will be handled below as normal
}
#endif
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
//通过CFRunloopWake唤醒
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
//什么都不干,跳回2重新循环
// do nothing on Mac OS
#if DEPLOYMENT_TARGET_WINDOWS
// Always reset the wake up port, or risk spinning forever
ResetEvent(rl->_wakeUpPort);
#endif
}
#if USE_DISPATCH_SOURCE_FOR_TIMERS
//如果是定时器事件
else if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
//9.1 处理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer, because we apparently fired early
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
#if USE_MK_TIMER_TOO
//如果是定时器事件
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
// On Windows, we have observed an issue where the timer port is set before the time which we requested it to be set. For example, we set the fire time to be TSR 167646765860, but it is actually observed firing at TSR 167646764145, which is 1715 ticks early. The result is that, when __CFRunLoopDoTimers checks to see if any of the run loop timers should be firing, it appears to be 'too early' for the next timer, and no timers are handled.
// In this case, the timer port has been automatically reset (since it was returned from MsgWaitForMultipleObjectsEx), and if we do not re-arm it, then no timers will ever be serviced again unless something adjusts the timer list (e.g. adding or removing timers). The fix for the issue is to reset the timer here if CFRunLoopDoTimers did not handle a timer itself. 9308754
//9.1处理timer事件
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
//如果是dispatch到main queue的block
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
#if DEPLOYMENT_TARGET_WINDOWS
void *msg = 0;
#endif
//9.2执行block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
CFRUNLOOP_WAKEUP_FOR_SOURCE();
// Despite the name, this works for windows handles as well
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
// 有source1事件待处理
if (rls) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
mach_msg_header_t *reply = NULL;
//9.2 处理source1事件
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
if (NULL != reply) {
(void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
}
#elif DEPLOYMENT_TARGET_WINDOWS
sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
#endif
}
}
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#endif
__CFRunLoopDoBlocks(rl, rlm);
if (sourceHandledThisLoop && stopAfterHandle) {
//进入run loop时传入的参数,处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
}else if (timeout_context->termTSR < mach_absolute_time()) {
//run loop超时
retVal = kCFRunLoopRunTimedOut;
}else if (__CFRunLoopIsStopped(rl)) {
//run loop被手动终止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
}else if (rlm->_stopped) {
//mode被终止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
}else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
//mode中没有要处理的事件
retVal = kCFRunLoopRunFinished;
}
//除了上面这几种情况,都继续循环
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}
源代码尽管不算太长,但是如果不太熟悉的话面对这么一堆不知道做什么的函数调用还是会给人一种神秘感。但是现在可以不用逐行阅读,后面慢慢解开这层神秘面纱。现在只要了解上面的伪代码知道核心的方法 __CFRunLoopRun
内部其实是一个_do while_循环
,这也正是Runloop运行
的本质
。执行了这个函数以后就一直处于“等待-处理”的循环之中
,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源
,当然这是由于系统内核负责实现
的,也是Runloop精华
所在
3.4.__CFRunLoopServiceMachPort
这个函数在runloop
中起到了至关重要的作用
/**
* 接收指定内核端口的消息
*
* @param port 接收消息的端口
* @param buffer 消息缓冲区
* @param buffer_size 消息缓冲区大小
* @param livePort 暂且理解为活动的端口,接收消息成功时候值为msg->msgh_local_port,超时时为MACH_PORT_NULL
* @param timeout 超时时间,单位是ms,如果超时,则RunLoop进入休眠状态
*
* @return 接收消息成功时返回true 其他情况返回false
*/
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;) { /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0; //消息头的标志位
msg->msgh_local_port = port; //源(发出的消息)或者目标(接收的消息)
msg->msgh_remote_port = MACH_PORT_NULL; //目标(发出的消息)或者源(接收的消息)
msg->msgh_size = buffer_size; //消息缓冲区大小,单位是字节
msg->msgh_id = 0; //唯一id
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
//通过mach_msg发送或者接收的消息都是指针,
//如果直接发送或者接收消息体,会频繁进行内存复制,损耗性能
//所以XNU使用了单一内核的方式来解决该问题,所有内核组件都共享同一个地址空间,因此传递消息时候只需要传递消息的指针
ret = mach_msg(msg,
MACH_RCV_MSG|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
0,
msg->msgh_size,
port,
timeout,
MACH_PORT_NULL);
CFRUNLOOP_WAKEUP(ret);
//接收/发送消息成功,给livePort赋值为msgh_local_port
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
//MACH_RCV_TIMEOUT
//超出timeout时间没有收到消息,返回MACH_RCV_TIMED_OUT
//此时释放缓冲区,把livePort赋值为MACH_PORT_NULL
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
//MACH_RCV_LARGE
//如果接收缓冲区太小,则将过大的消息放在队列中,并且出错返回MACH_RCV_TOO_LARGE,
//这种情况下,只返回消息头,调用者可以分配更多的内存
if (MACH_RCV_TOO_LARGE != ret) break;
//此处给buffer分配更大内存
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
3.5.逻辑导图
将 3.1
到 3.4
的代码逻辑画成一张图,逻辑如下:
二.RunLoop
相关的功能
当 RunLoop
进行回调时,一般都是通过一个很长的函数
调用出去 (call out
), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:
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__();
实际代码块
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即将触发 Timer 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 触发 Source0 (非基于port的) 回调。 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,线程被唤醒 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer唤醒的,回调Timer __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop(); __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
1.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
)。
在主线程执行的代码,通常是写在诸如 事件回调
、Timer
回调内的。这些回调会被 RunLoop
创建好的 AutoreleasePool
环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool
了。
自动释放池的创建和释放,销毁的时机如下所示
kCFRunLoopEntry
; // 进入runloop之前,创建一个自动释放池kCFRunLoopBeforeWaiting
; // 休眠之前,销毁自动释放池,创建一个新的自动释放池kCFRunLoopExit
; // 退出runloop之前,销毁自动释放池
2.CADisplayLink
CADisplayLink
是一个执行频率(fps
)和屏幕刷新相同(可以修改 preferredFramesPerSecond
改变刷新频率)的定时器,它也需要加入到 RunLoop
才能执行。与 NSTimer
类似,CADisplayLink
同样是基于 CFRunloopTimerRef
实现,底层使用 mk_timer
(可以比较加入到 RunLoop
前后 RunLoop
中 timer
的变化)。和 NSTimer
相比它精度更高(尽管 NSTimer
也可以修改精度),不过和 NStimer
类似的是如果遇到大任务它仍然存在丢帧现象。通常情况下 CADisaplayLink
用于构建帧动画
,看起来相对更加流畅,而 NSTimer
则有更广泛的用处。
3.GCD Timer
GCD
则不同,GCD
的线程管理是通过系统来直接管理的。GCD Timer
是通过 dispatch port
给 RunLoop
发送消息,来使 RunLoop
执行相应的 block
,如果所在线程没有 RunLoop
,那么 GCD
会临时创建一个线程去执行 block
,执行完之后再销毁掉,因此 GCD
的 Timer
是不依赖 RunLoop
的。
至于这两个 Timer
的准确性问题,如果不在 RunLoop
的线程里面执行,那么只能使用 GCD Timer
,由于 GCD Timer
是基于 MKTimer(mach kernel timer)
,已经很底层了,因此是很准确的。
如果在 RunLoop
的线程里面执行,由于 GCD Timer
和 NSTimer
都是通过 port
发送消息的机制来触发 RunLoop
的,因此准确性差别应该不是很大。如果线程 RunLoop
阻塞了,不管是 GCD Timer
还是 NSTimer
都会存在延迟问题。
4.NSTimer
前面一直提到 Timer Source
作为事件源,事实上它的上层对应就是 NSTimer
(其实就是 CFRunloopTimerRef
)这个开发者经常用到的定时器(底层基于使用 mk_timer
实现)
NSTimer
其实就是 CFRunLoopTimerRef
,他们之间是 toll-free bridged
的。一个 NSTimer
注册到 RunLoop
后,RunLoop
会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop
为了节省资源,并不会在非常准确的时间点回调这个 Timer
。Timer
有个属性叫做 Tolerance (宽容度)
,标示了当时间点到后,容许有多少最大误差。由于 NSTimer
的这种机制,因此 NSTimer
的执行必须依赖于 RunLoop
,如果没有 RunLoop
,NSTimer
是不会执行的。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
5.事件响应
苹果注册了一个 Source1
(基于 mach port
的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()
。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework
生成一个 IOHIDEvent
事件并由 SpringBoard
接收。这个过程的详细情况可以参考这里。SpringBoard
只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event
,随后用 mach port
转发给需要的App进程
。随后苹果注册的那个 Source1
就会触发回调,并调用 _UIApplicationHandleEventQueue()
进行应用内部的分发。
_UIApplicationHandleEventQueue()
会把 IOHIDEvent
处理并包装成 UIEvent
进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
6.手势识别
当上面的 _UIApplicationHandleEventQueue()
识别了一个手势时,其首先会调用 Cancel
将当前的 touchesBegin/Move/End
系列回调打断。随后系统将对应的 UIGestureRecognizer
标记为待处理。
苹果注册了一个 Observer
监测 BeforeWaiting
(Loop即将进入休眠) 事件,这个 Observer
的回调函数是 _UIGestureRecognizerUpdateObserver()
,其内部会获取所有刚被标记为待处理的 GestureRecognizer
,并执行 GestureRecognizer
的回调。
当有 UIGestureRecognizer
的变化(创建/销毁/状态改变
)时,这个回调都会进行相应处理。
7.UI更新
如果打印App启动之后的主线程 RunLoop
可以发现另外一个 callout
为 _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的Observer
,这个监听专门负责UI变化后的更新,比如修改了 frame
、调整了UI
层级(UIView/CALayer
)或者手动设置了setNeedsDisplay/setNeedsLayout
之后就会将这些操作提交到全局容器。而这个 Observer
监听了主线程 RunLoop
的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。
这个函数内部的调用栈大概是这样的:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
通常情况下这种方式是完美的,因为除了系统的更新,还可以利用 setNeedsDisplay
等方法手动触发下一次 RunLoop
运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此 facebook
推出了 Texture
来解决这个问题。Texture
其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类 UIView
或 CALayer
的相关属性,尽可能保证开发者的开发习惯。这个过程中 Texture
在主线程 RunLoop
中增加了一个 Observer
监听即将进入休眠和退出 RunLoop
两种状态,收到回调时遍历队列中的待处理任务一一执行。
8.Core Animation
iOS 图形服务接收到 VSync
信号后,会通过 IPC
通知到 App
内。App
的 Runloop
在启动后会注册对应的 CFRunLoopSource
通过 mach_port
接收传过来的时钟信号通知,随后 Source
的回调会驱动整个 App
的动画与显示。Core Animation
在 RunLoop
中注册了一个 Observer
,监听了 BeforeWaiting
和 Exit
事件。这个 Observer
的优先级是 2000000,低于常见的其他 Observer
。
当一个触摸事件到来时,RunLoop
被唤醒,App
中的代码会执行一些操作,比如创建和调整视图层级、设置 UIView
的 frame
、修改 CALayer
的透明度、为视图添加一个动画;这些操作最终都会被 CALayer
捕获,并通过 CATransaction
提交到一个中间状态去(CATransaction
的文档略有提到这些内容,但并不完整)。
当上面所有操作结束后,RunLoop
即将进入休眠(或者退出)时,关注该事件的 Observer
都会得到通知。这时 CA 注册的那个 Observer
就会在回调中,把所有的中间状态合并提交到 GPU
去显示;如果此处有动画,CA 会通过 DisplayLink
等机制多次触发相关流程。
三. RunLoop
的应用
1.UIImageView
延迟加载图片
给 UIImageView 设置图片可能耗时不少,如果此时要滑动 tableView
等则可能影响到界面的流畅。解决是:使用 performSelector:withObject:afterDelay:inModes:
方法,将设置图片的方法放到 DefaultMode
中执行。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
2.常驻线程
子线程默认是完成任务后结束。当要经常使用子线程,每次开启子线程比较耗性能。此时可以开启子线程的 RunLoop
,保持 RunLoop
运行,则使子线程保持不死。
具体实现:
/* 返回一个线程 */
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
// 创建一个线程,并在该线程上执行下一个方法
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
// 开启线程
[_networkRequestThread start];
});
return _networkRequestThread;
}
/* 在新开的线程中执行的第一个方法 */
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
// 获取当前线程对应的 RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 为 RunLoop 添加 source,模式为 DefaultMode
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 开始运行 RunLoop
[runLoop run];
}
}
因为 RunLoop
启动前必须设置一个 mode
,而 mode
要存在则至少需要一个 source / timer
。所以上面的做法是为 RunLoop
的 DefaultMode
添加一个 NSMachPort
对象,虽然消息是可以通过 NSMachPort
对象发送到 loop
内,但这里添加的 port
只是为了 RunLoop
一直不退出,而没有发送什么消息。当然我们也可以添加一个超长启动时间的 timer
来既保持 RunLoop
不退出也不占用资源。
3.滚动 Scrollview
导致定时器失效
在界面上有一个 UIScrollview
控件,如果此时还有一个定时器在执行一个事件,你会发现当你滚动 Scrollview
的时候,定时器会失效。
- (void)viewDidLoad {
[super viewDidLoad];
[self timer1];
[self timer2];
}
//下面两种添加定时器的方法效果相同,都是在主线程中添加定时器
- (void)timer1 {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopDefaultModes];
}
- (void)timer2 {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
复制代码
因为当你滚动 Scrollview
的时候,RunLoop
会切换到 UITrackingRunLoopMode
模式,而定时器运行在 defaultMode
下面,系统一次只能处理一种模式的 RunLoop
,所以导致 defaultMode
下的定时器失效。
解决方法:
- 把
timer
注册到NSRunLoopCommonModes
,它包含了defaultMode
和trackingMode
两种模式。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
复制代码
- 使用
GCD
创建定时器,GCD
创建的定时器不会受RunLoop
的影响
// 获得队列
dispatch_queue_t queue = dispatch_get_main_queue();
// 创建一个定时器(dispatch_source_t本质还是个OC对象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
// GCD的时间参数,一般是纳秒(1秒 == 10的9次方纳秒)
// 比当前时间晚1秒开始执行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
//每隔一秒执行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);
// 设置回调
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);
});
// 启动定时器
dispatch_resume(self.timer);
4.观察事件状态,优化性能
假设我们想实现 cell
的高度缓存计算,因为“计算cell的预缓存高度
”的任务需要在最无感知的时刻进行,所以应该同时满足:
RunLoop
处于“空闲”
状态Mode
- 当这一次
RunLoop
迭代处理完成了所有事件,马上要休眠时
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFStringRef runLoopMode = kCFRunLoopDefaultMode;
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler
(kCFAllocatorDefault, kCFRunLoopBeforeWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity _) {
// TODO here
});
CFRunLoopAddObserver(runLoop, observer, runLoopMode);
在其中的 TODO 位置,就可以开始任务的收集和分发了,当然,不能忘记适时的移除这个 observer
5.监控界面卡顿
@interface XCatonMonitoring ()
@property (nonatomic,assign) BOOL isMonitoring;
@property (nonatomic,assign) NSInteger timeoutCount;
@property (nonatomic,assign) CFRunLoopObserverRef observer;
@property (nonatomic,assign) CFRunLoopActivity currentActivity;
@property (nonatomic,strong) dispatch_semaphore_t semaphore;
+ (instancetype) instance;
@end
@implementation XCatonMonitoring
+ (instancetype) instance {
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[super allocWithZone:NSDefaultMallocZone()] init];
});
return instance;
}
+ (instancetype) allocWithZone:(struct _NSZone *)zone {
return [self instance];
}
- (void) dealloc {
[self endMonitor];
}
/**
* 开启卡顿监控
* @Param timeOut 卡顿超时时间(毫秒)
*/
- (void) beginMonitorWithTimeOut:(NSInteger) timeOut {
if (_isMonitoring && _observer) return;
_isMonitoring = YES;
// 创建观察者
CFRunLoopObserverContext context = {
0,
(__bridge void*)self,
&CFRetain,
&CFRelease,
NULL
};
// static CFRunLoopObserverRef observer;
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runloopObserverCallback, &context);
// 观察主线程
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 在子线程中监控卡顿
_semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 开启持续的loop来监控
while ([XCatonMonitoring instance].isMonitoring) {
// 假定连续5次超时50ms认为卡顿(当然也包含了单次超时250ms)
// 因为下面 runloop 状态改变回调方法runLoopObserverCallBack中会将信号量递增 1,所以每次 runloop 状态改变后,下面的语句都会执行一次
// dispatch_semaphore_wait:Returns zero on success, or non-zero if the timeout occurred.
long st = dispatch_semaphore_wait([XCatonMonitoring instance].semaphore, dispatch_time(DISPATCH_TIME_NOW, timeOut*NSEC_PER_MSEC));
if (st != 0) { // 信号量超时了 - 即 runloop 的状态长时间没有发生变更,长期处于某一个状态下
if (![XCatonMonitoring instance].observer) {
[XCatonMonitoring instance].timeoutCount = 0;
[XCatonMonitoring instance].semaphore = 0;
[XCatonMonitoring instance].currentActivity = 0;
return;
}
// kCFRunLoopBeforeSources - 即将处理source kCFRunLoopAfterWaiting - 刚从休眠中唤醒
// 获取kCFRunLoopBeforeSources到kCFRunLoopBeforeWaiting再到kCFRunLoopAfterWaiting的状态就可以知道是否有卡顿的情况。
// kCFRunLoopBeforeSources:停留在这个状态,表示在做很多事情
if ([XCatonMonitoring instance].currentActivity == kCFRunLoopBeforeSources ||
[XCatonMonitoring instance].currentActivity == kCFRunLoopAfterWaiting) { // 发生卡顿,记录卡顿次数
// 不足 5 次,直接 continue 当次循环,不将timeoutCount置为0
[XCatonMonitoring instance].timeoutCount ++;
if ([XCatonMonitoring instance].timeoutCount < 5) continue;
[LXDBacktraceLogger lxd_logMain];
}
}
[XCatonMonitoring instance].timeoutCount = 0;
}
});
}
- (void) endMonitor {
if (!_observer) return;
if (!_isMonitoring) return;
_isMonitoring = NO;
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = nil;
}
static void runloopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
[XCatonMonitoring instance].currentActivity = activity;
dispatch_semaphore_t semaphore = [XCatonMonitoring instance].semaphore;
dispatch_semaphore_signal(semaphore);
}
@end
四.面试题
1.app如何接收到触摸事件的
2.为什么只有主线程的runloop
是开启的
mian()
函数中调用 UIApplicationMain
,这里会创建一个主线程,用于UI处理,为了让程序可以一直运行并接收事件,所以在主线程中开启一个 runloop
,让主线程常驻.
3.为什么只在主线程刷新UI
我们所有用到的UI都是来自于UIKit这个基础库.因为objc不是一门线程安全的语言所以存在多线程读写不同步的问题,如果使用加锁的方式操作系统开销很大,会耗费大量的系统资源(内存、时间片轮转、cpu处理速度…),加上系统事件的接收处理都在主线程,如果UI异步线程的话 还会存在 同步处理事件的问题,所以多点触摸手势等一些事件要保持和UI在同一个线程相对是最优解.
另一方面是 屏幕的渲染是 60帧(60Hz/秒), 也就是1秒钟回调60次的频率,(iPad Pro 是120Hz/秒),我们的runloop 理想状态下也会按照时钟周期 回调60次(iPad Pro 120次), 这么高频率的调用是为了 屏幕图像显示能够垂直同步 不卡顿.在异步线程的话是很难保证这个处理过程的同步更新. 即便能保证的话 相对主线程而言 系统资源开销 线程调度等等将会占据大部分资源和在同一个线程只专门干一件事有点得不偿失.
4.PerformSelector
和runloop
的关系
当调用 NSObect
的 performSelector
: 相关的时候,内部会创建一个 timer 定时器添加到当前线程的 runloop
中,如果当前线程没有启动 runloop
,则该方法不会被调用.
开发中遇到最多的问题就是这个 performSelector
: 导致对象的延迟释放,这里开发过程中注意一下,可以用单次的 NSTimer
替代.
5.如何使线程保活
实现一个常驻线程,参考【RunLoop 的应用 - 常驻线程
】