您的位置:首页 > 运维架构

CFRunloop 源码阅读笔记

2015-02-03 10:04 309 查看
第一次接触 runloop 时,是在用 Timer 写重复动画的时候(用 Timer 写动画?很傻逼是吧,我也这么觉得)。大概是这么一行代码启动一个定时器,然后每隔0.1秒去翻转一张 Loading 图的角度。

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:.1f target:self selector:@selector(rotationLoading:) userInfo:nil repeats:YES]


那时候经常遇到问题。当首页的 TableView 在滚动的时候,Loading 图却不转了。经过一翻查找,大概知道了 Runloop 这个概念。当时刚入门,找到解决方法后也没继续深入。最近又经常接触到这家伙,便去找了 Core Foundation 的源码来看了个大概。

runloop 是什么

runloop 大致跟 Windows 下的消息循环机制一样。一个runloop是一种消息机制,用于线程间、异步通信。runloop的主要功能是等待事件的发生,并将该事件分发到相应的处理方法。比如,用户点击按钮,定时器,线程间通信,异步代码,网络请求回调,等等。runloop接收到消息后,将该消息投递到相关的处理方法中去。简单来说,runloop在线程中可以简单等价于以下伪代码:

run (runloop) {
do {
runloop.waitForSomeEvent();
var message = runloop.messageQueue.dequeue();
dispatchMessage(message);
} while (YES);
}

post (runloop, message) {
runloop.messageQueue.queue(message);
runloop.wakeup();
}
在iOS中,替线程干这种脏活的叫,NSRunloop。NSRunloop是对CFRunloop的封装。并远强大于上述的伪代码。除了初始化、手动启动的线程及GCD中分发到后台线程的block,你写的大部分代码都是在合适的时间被CFRunloop调用。

 CFRunloop

CFRunLoop中有一项重要的特性——运行模式(CFRunLoopModes)。同一时间内CFRunLoop运行在特定的RunLoopModes下。CFRunLoop中运行着不同的Run Loop Sources。 Sources与一个或多个runloopModes绑定。也就是说runloop会自动过滤和其他Mode相关的事件源,而只监视和当前设置Mode相关的源。此外CFRunLoop也可以在应用中手动激活。你仅需调用CFRunLoopRun()方法。

在CFRunLoop里面定义了以下6种函数:

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__();


这些销魂的名字仅仅是用来帮助调试的。CFRunLoop都是通过这些函数来调用对应的事件。

static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(
CFRunLoopObserverCallBack func,
CFRunLoopObserverRef observer,
CFRunLoopActivity activity,
void *info);

这个Observers有些特殊。CFRunLoop 提供CFRunLoopObserver API能让你在应用中观察CFRunLoop的状态。当Run Loop 进入的执行,Run Loop 处理一个Timer,Run Loop 处理一个Input Source,Run Loop 进入睡眠,Run Loop 被唤醒,在唤醒它的事件被处理之前Run Loop 停止等等。这些状态非常有用,系统某些框架工作,比如CoreAnimation就运行在观察者通知回调中。

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(
void (^block)(void));

正如名字一样,这个函数则是用来调用那些由CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void))添加的 Blocks。

static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(
void *msg);
这个函数当然是用来调用GCD中Main Dispatch Queue的Blocks。显然,在主线程中,至少同时运行着GCD 和 CFRunLoop。尽管GCD可以创建没有CFRunLoop的线程,在只有一条线程时(也就是只有主线程时),GCD将被阻塞。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(
CFRunLoopTimerCallBack func,
CFRunLoopTimerRef timer,
void *info);
用来调用定时器的方法。在iOS中,上层的类NSTimer或方法performSelector:afterDelay:都是用CFRunLoop timers来实现。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(
void (*perform)(void *),
void *info);
这个函数是用来处理 Version0的事件源。CFRunLoopSources有两个版本的源。Version0 和 Version1。虽然他们长得很像,而且有共同的API,但实际上它们是不太相同的东西。Version0的Sources只是在应用内只能由手动处理的消息机制。比如UI事件。当你收到一个Version0的Sources的信号( 可由CFRunLoopSourceSignal()发起),必须唤醒CFRunLoop(调用CFRunLoopWakeUp()函数)来处理Version0的Sources。
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
void (*perform)(void *),
void *info);
有处理Version0的函数当然也就有处理 Version1的函数。Version1的Sources通过mach ports来处理内核事件。由代码看来,这对CFRunLoop非常重要。当没有时间发生时,CFRunLoop将在mach_msg处等待事件发生。

__CFRunLoopRun()

不管你是用CFRunLoopRun() 或 CFRunLoopRunInMode()来启动runloop,CFRunLoop的核心是__CFRunLoopRun()函数。

__CFRunLoopRun()退出的情况有:

kCFRunLoopRunTimedOut:超时
kCFRunLoopRunFinished:所有的Sources都被移除:
kCFRunLoopRunHandledSource:与returnAfterSourceHandled共同决定一旦事件被分发处理
kCFRunLoopRunStopped: 手动调用CFRunLoopStop()
如果没出现这几种情况,runloop将会一直运行。并且内部将依次按以下顺序处理消息

调用通过 CFRunLoopPerformBlock() API 这个函数添加的 Blocks。
检查Version0 Sources,如果有的话,则调用 perform 函数。
轮询内部分发队列。

如果没事件发生则进入睡眠。当有消息到达时,runloop被唤醒。(因为要兼容Win32,所以源代码加了许多#ifdef,#elif, #endif。但主要是通过mach_msg()来配置等待的ports。这样做可以让Timer,GCD,手动唤醒,或version0 Sources都能在同一时间唤醒runloop。)

runloop 被唤醒,依次检查以下:
手动唤醒。仅是继续执行runloop blocks或者version0 Source。

一个或多个定时事件唤醒。执行事件函数。

GCD唤醒,执行Blocks。调用的是 dispatch_queue API 中特殊的_dispatch_main_queue_callback_4CF()函数。

查找并执行由内核发出version0 的Sources。

再次调用由CFRunLoopPerformBlock() API 这个函数添加的 Blocks。
检查退出条件(Finished, Stopped, TimedOut, HandledSource)。

进行下一次循环。
伪代码如下:

setupTimeoutTimer();
do {
observerCallbacks(kCFRunLoopBeforeTimers);
observerCallbacks(kCFRunLoopBeforeSources);

performBlocks();

var hasHandledSource0 = performVersion0Sources();

if (hasHandledSources0) performBlocks();

if (checkIfExistMessageInMainQueue) {

do {
performNextBlockInMainQueue();
}while(hasBlocksInMainQueue);

} else {

observerCallbacks(kCFRunLoopBeforeWaiting);
var event = sleepAndWaitForEventWakeup();//这里runloop 进入睡眠直到有 Timer 的 fire 事件、GCD 中dispatch block 到 main_queue 或 source 信号。
observerCallbacks(kCFRunLoopAfterWaiting);
if (event == timer) {
performTimers();
} else if (event == mainDispatchQueue) {
do {
performNextBlockInMainQueue();
} while(hasBlocksInMainQueue);
} else {
performVersion1Sources();
}
}
performBlocks();
} while (true);


以上便是__CFRunLoopRun()的主要流程。很容易,对不对?但是CoreFoundation用C实现的,并不是很好理解。目前也没有看到苹果在 Swift 中重写这一部分内容。只能自己多研究研究了~

 注:本文是关于iOS Runloop的学习笔记。有不对的地方,请指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Runloop 原理 模型