深入了解 runloop

什么是Runloop

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:


function loop() {

initialize();

do {

var message = get_next_message();

process_message(message);

} while (message != quit);

}

这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

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

在OC中有这样一段代码,main.m 中


int main(int argc, char * argv[]) {

@autoreleasepool {

return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));

}

}

而Runloop其实就是一个循环,插在{}中的一个do-while,执行各种任务,处理各种事件,直到超时或被手动停止,该函数才会返回。

Runloop的底层实现原理

RunLoop 对外的接口

在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef CFRunLoopRef开源代码

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopTimerRef

CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

相关类.png

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

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。

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

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

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 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 的内部逻辑

ibireme图.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实现的功能

AutoreleasePool

事件响应

手势识别

界面更新

定时器

PerformSelecter

关于GCD

ibireme的详细解释

RunLoop在工作中场景中的使用

NSTimer


- (void)timer

{

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 定时器只运行在NSDefaultRunLoopMode下,一旦RunLoop进入其他模式,这个定时器就不会工作

//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

// 定时器只运行在UITrackingRunLoopMode下,一旦RunLoop进入其他模式,这个定时器就不会工作

//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// 定时器会跑在标记为common modes的模式下

// 标记为common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

}

ImageView显示

需求:当用户在拖拽时(UI交互时)不显示图片,拖拽完成时显示图片

方法1 监听UIScrollerView滚动 (通过UIScrollViewDelegate监听,此处不再举例)

方法2 RunLoop 设置运行模式


// 只在NSDefaultRunLoopMode模式下显示图片

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

以后为了增加用户体验 在用户UI交互的时候 不做事件处理 我们可以把需要做的操作放到NSDefaultRunLoopMode

PerformSelector

常驻线程

应用场景:经常在后台进行耗时操作,如:监控联网状态,扫描沙盒等 不希望线程处理完事件就销毁,保持常驻状态

首先创建一个线程

- (void)viewDidLoad {

[super viewDidLoad];

//创建一个线程

self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByNormal) object:nil] ;

// self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByCFObserver) object:nil] ;

// self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByCFTimer) object:nil] ;

// self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByCFSource) object:nil] ;

//启动线程

[self.thread start];

}

测试一下线程是否退出

- (IBAction)btnClick:(id)sender {

NSLog(@"-----btnClick--------");

[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];

}

- (void)test{

//能打印说明没有退出

NSLog(@"----->Test");

}

退出-退出当前线程

[NSThread exit];

自动释放池

在休眠前(kCFRunLoopBeforeWaiting)进行释放,处理事件前创建释放池,中间创建的对象会放入释放池

GCD定时器

AFNetworking

类似常驻线程

AsyncDisplayKit

特别注意:

在启动RunLoop之前建议用 @autoreleasepool {...}包裹

意义:创建一个大释放池,释放{}期间创建的临时对象,一般好的框架的作者都会这么做

RunLoop会涉及到的一些面试题

经常会有喜欢装B的面试官,面试的时候就喜欢问RunLoop,其实他真的会吗? 说不定他自己都不太理解

下面我对有关RunLoop的面试做一个简单的总结,也算是对全文一个总结

什么是RunLoop?

从字面上看:运行循环、跑圈

其实它内部就是do-while循环,在这个循环内部不断的处理各种任务(比如Source、Timer、Observer)

一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动(调用run方法)

RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Soure、Timer、Observer,那么就直接退出RunLoop

在开发中如何使用RunLoop?什么应用场景?

开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)

在子线程中开启一个定时器

在子线程中进行一些长期监控

可以控制定时器在特定模式下执行

可以让某些事件(行为、任务)在特定模式下执行

可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)

参考链接

ibireme的详细解释

官方文档

解密-神秘的RunLoop

iOS NSRunloop 详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容