注册

runloop 小结

OC的两大核心runtime和runloop

runloop简介

runloop本质上是一个do-while循环,当有任务处理时唤醒,没有任务时休眠,如果没有任务没有观察者的时候退出。

OSX/iOS系统中,提供了两个这样的对象:NSRunLoop和CFRunLoopRef.
CFRunLoopRef是CoreFoundation框架提供的纯c的api,所有这些api都是线程安全的。
NSRunLoop是对CFRunLoopRef的OC封装,提供了面向对象的api,这些api不是线程安全的。

runloop和线程的关系

首先,iOS提供了两个线程对象pthread_t和NSThread,这两个线程对象不能互相转换,但是一一对应。比如:可以通过pthread_main_thread_np()和[NSThread mainThread]获取主线程;也可以通过pthread_self()和[NSThread currentThread]获取当前线程。CFRunLoopRef是基于pthread来管理的。

苹果不允许直接创建runloop,它只有两个获取的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。这两个函数的内部实现大致是:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);

if (!loopsDic) {
// 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}

/// 直接从 Dictionary 里获取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

if (!loop) {
/// 取不到时,创建一个
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}

OSSpinLockUnLock(&loopsLock);
return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}

可以看出来,线程和RunLoop是一一对应的,保存在一个全局的CFMutableDictionaryRef,key为pthread,value为runloop。线程刚创建时没有runloop,如果你没有主动获取,那它一直不会有。当你第一次获取runloop时,创建runloop,当线程结束时,runloop销毁。

主线程的runloop默认开启,程序启动时,main方法,applicationMain方法内开启runloop。

runloop的类
在Core Foundation框架中提供了五个类关于runloop:

1、CFRunLoopRef
2、CFRunLoopModeRef
3、CFRunLoopSourceRef
4、CFRunLoopTimerRef
5、CFRunLoopObserverRef

它们的关系如下:

2cd377cab6c60cb4a6ffedd0e094adc9.png

一个runloop包含若干个Mode,一个Mode又包含若干个Source/Timer/Observer。每次调用runloop的主函数时,只能指定其中一个mode,如果想切换mode,需要退出当前runloop,再重新指定一个mode进入。这样的好处是,不同组的Source/Timer/Observer互不影响。

CFRunLoopSourceRef是事件产生的地方。Source有两个版本,Source 0(非端口Source)和Source 1(端口Source)。

1、Source 0 只包含一个回调函数指针,它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(Source 0)将该source标记为待处理,然后手动调用CFRunLoopWakeUp()唤醒runloop,处理该事件。
2、Source 1 包含一个mach port(端口)和一个回调的函数指针,被用于通过内核和其他线程相互发送消息。这种source能主动唤醒runloop。
CFRunLoopTimerRef 是基于时间的触发器。其包含一个时间长度和一个回调的函数指针。当其加入到runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行这个回调。

CFRunLoopObserverRef 是观察者,每个Observer都包含一个回调,当runloop的状态发生改变时,观察者可以通过回调接受到这个变化。可以接受到的状态有如下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

上述的Source/Timer/Observer被统称为一个mode item,一个item可以被加入多个mode,但一个item被重复加入同一个mode,是没有效果的。如果一个mode中一个item都没有,则runloop会自动退出。

runloop的mode

CFRunLoopMode和CFRunLoop的结构大致如下

struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};

struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};

runloop的mode包含:

1、NSDefaultRunLoopMode:默认的mode;
2、UITrackingRunLoopMode:跟踪用户触摸事件的mode,如UIScrollView的上下滚动;
3、NSRunLoopCommonModes:模式集合,将一组item关联到这个模式集合上,等于将这个item关联到这个集合下的所有模式上;
4、自定义Mode。

这里主要解释一下NSRunLoopCommonModes,这个模式集合。
默认NSDefaultRunLoopMode和UITrackingRunLoopMode都是包含在这个模式集合内的,当然也可以自定义一个mode,通过CFRunLoopAddCommonMode添加到这个模式集合中。

应用场景举例:

当一个控制器里有一个UIScrollview和一个NSTimer,UIScrollView不滚动的时候,runloop运行在NSDefaultRunLoopMode下,此时Timer会得到回调,但当UIScrollView滑动时,会将mode切换成UITrackingRunLoopMode,此时Timer得不到回调。一个解决办法就是将这个NSTimer分别绑定到NSDefaultRunLoopMode和UITrackingRunLoopMode,另一个解决办法是将这个NSTimer绑定到NSRunLoopCommonModes,两种方法都能使NSTimer在两个模式下都能得到回调。

ps.让runloop运行在NSRunLoopCommonModes模式下是没有意思的,因为runloop一个时间只能运行在一个模式下。

端口Source通信的步骤
demo如下:

- (void)testDemo3
{
//声明两个端口 随便怎么写创建方法,返回的总是一个NSMachPort实例
NSMachPort *mainPort = [[NSMachPort alloc]init];
NSPort *threadPort = [NSMachPort port];
//设置线程的端口的代理回调为自己
threadPort.delegate = self;

//给主线程runloop加一个端口
[[NSRunLoop currentRunLoop]addPort:mainPort forMode:NSDefaultRunLoopMode];

dispatch_async(dispatch_get_global_queue(0, 0), ^{

//添加一个Port
[[NSRunLoop currentRunLoop]addPort:threadPort forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

});

NSString *s1 = @"hello";

NSData *data = [s1 dataUsingEncoding:NSUTF8StringEncoding];

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSMutableArray *array = [NSMutableArray arrayWithArray:@[mainPort,data]];
//过2秒向threadPort发送一条消息,第一个参数:发送时间。msgid 消息标识。
//components,发送消息附带参数。reserved:为头部预留的字节数(从官方文档上看到的,猜测可能是类似请求头的东西...)
[threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0];

});

}

//这个NSMachPort收到消息的回调,注意这个参数,可以先给一个id。如果用文档里的NSPortMessage会发现无法取值
- (void)handlePortMessage:(id)message
{

NSLog(@"收到消息了,线程为:%@",[NSThread currentThread]);

//只能用KVC的方式取值
NSArray *array = [message valueForKeyPath:@"components"];

NSData *data = array[1];
NSString *s1 = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"%@",s1);

// NSMachPort *localPort = [message valueForKeyPath:@"localPort"];
// NSMachPort *remotePort = [message valueForKeyPath:@"remotePort"];

}

声明两个端口,sendPort,receivePort,设置receivePort的代理,分别将sendPort和receivePort绑定到两个线程的自己的runloop上,然后回到发送线程用接收端口发送数据([threadPort sendBeforeDate:[NSDate date] msgid:1000 components:array from:mainPort reserved:0]; from参数标注从发送端口发出),注意这里发送的数据格式为array,内容格式只能为NSPort或者NSData,在代理方法- (void)handlePortMessage:(id)message中接收数据;

RunLoop的内部实现

12c5c02f7741cd54a46ad62b04ad1c62.png

内部代码整理,不想看可以跳过,看下方总结:

/// 用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() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

runloop的运行逻辑:

1、通知监听者,即将进入runloop;
2、通知监听者,将要处理Timer;
3、通知监听者,将要处理Source0(非端口InputSource);
4、处理Source0;
5、如果有Source1,跳到第9步;
6、通知监听者,线程即将进入休眠;
7、runloop进入休眠,等待唤醒;
   1.source0;
   2.Timer启动;
   3.外部手动唤醒
8、通知监听者,线程将被唤醒;
9、处理未处理的任务;
   1.如果用户定义的定时器任务启动,处理定时器任务并重启runloop,进入步骤2;
   2.如果输入源启动,传递相应的消息;
   3.如果runloop被显示唤醒,且没有超过设置的时间,重启runloop,进入步骤2;
10、通知监听者,runloop结束。
   1.runloop结束,没有timer或者没有source;
   2.runloop被停止,使用CFRunloopStop停止Runloop;
   3.runloop超时;
   4.runloop处理完事件。

苹果用runloop实现的功能

1、自动释放池,在主程序启动时,再即将进入runloop的时候会执行autoreleasepush(),新建一个autoreleasePoolPage,同时push一个哨兵对象到这个page中;当runloop进入休眠模式时,会执行autoreleasepop(),释放旧池,同时autoreleasepush(),创建新池;当runloop退出时,清空自动释放池。

2、定时器NSTimer实际上就是CFRunloopTimerRef。

觉得有用,请帮忙点亮红心

Better Late Than Never!
努力是为了当机会来临时不会错失机会。
共勉!

链接:https://www.jianshu.com/p/8fdda9f64459

0 个评论

要回复文章请先登录注册