编程最怕的就是有盲点,不确定,而runloop官网对其提及的又很少;那么看完这篇应该使你有底气很多~

RunLoop整体介绍

An event-processing loop, during which events are received and dispatched to appropriate handlers.

事件运行循环:就类似下面的while循环部分,当然要复杂很多,可以把它抽象成如下代码:

main() {
initialize();
do {
message = get_next_message();
process_message(message);
} while (message != quit);
}

“消息”循环,等待消息(会休眠)->接收消息->处理消息。通过上面的代码,runloop本质就是提供了一种消息处理模式,只不过它封装抽象的太好了(一般开发的时候根本就感觉不到,或者说不用关心)。

runloop相当于帮我们打包了各种消息,并将消息发送给指定的接受者。

可以将runloop理解为一个函数,功能是一个消息循环,有消息则处理,没有消息则休眠。(注意:runloop实质是一个对象,但是不影响以上的假设)

简单使用:新建一个线程,添加一个定时器,然后运行即可

- (void)timerFire {
NSLog(@"mode:%@",[[NSRunLoop currentRunLoop] currentMode]);
} - (void)runLoopTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(modeTestTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerFire forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
});
}

如果你接触过嵌入式操作系统(纯内核)开发,那么对下面代码肯定很熟悉

void ledTask (void *p_arg)
{
initialize();
while (1) {
LED_ON();
delay_ms(500);
LED_OFF();
delay_ms(500);
};
}

LED闪烁线程,让一个LED灯1HZ的频率闪烁,功能很简单:首先初始化,然后进入while(1)死循环,延迟函数会使线程进入休眠(节省CPU)。直到程序死掉线程结束。是否和runloop很相似?

RunLoop消息类型(事件源)

一句话概括:很复杂,各种各样 :)

不过,根据上图我们可以将消息分为二种类型,第一种类型又可以细分为三种,此三种共同点就是它们都是异步执行的

  • Port:

监听程序的Mach ports,Mach ports是一个比较底层的东西,可以简单的理解为:内核通过port这种方式将信息发送,而mach则监听内核发来的port信息,然后将其整理,打包发给runloop。

  • Customer:

很明显,由开发人员自己发送。不仅仅是发送,过程的话相当复杂,苹果也提供了一个CFRunLoopSource来帮助处理。由于很少用到,可以简单说下核心,但是对帮助我们理解runloop却很有帮助:

  1. 定义输入源(数据结构)
  2. 将输入源添加到runloop,那么这样就有了接受者,即为R1
  3. 协调输入源的客户端(单独线程),专门监听消息,然后将消息打包成runloop能够处理的样式,即第一步定义的输入源。它类似Mach的功能
  4. 谁来发送消息的问题?上面的machport是由内核发送的。自定义的当然要我们自己发送了。。。首先必须是另一个线程来发送(当然如果只是测试的话可以和第三步在同一个线程),先发送消息给输入源,然后唤醒R1,因为R1一般处于休眠状态,然后R1根据输入源来做相应的处理
  • Selector Sources:

NSObject类提供了很多方法供我们使用,这些方法是添加到runloop的,所以如果没有开启runloop的话,不会运行(不过有个坑,请看下面介绍)。

/// 主线程
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
/// 指定线程
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
/// 针对当前线程
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
/// 取消,在当前线程,和上面两个方法对应
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

下面提供的方法是在指定的线程运行aSelector ,一般情况下aSelector会添加到指定线程的runloop。但,如果调用线程和指定线程为同一线程,且wait 参数设为YES,那么aSelector会直接在指定线程运行,不再添加到runloop。

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes: performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

其实这也很好理解,假设这种情况也添加到指定线程的runloop,我们可以这样反向理解:1,当前线程runloop还没有开启,那么aSelector就不会被执行,然而你却一直在等待,造成线程卡死。2,当前线程runloop已经开启,那么调用performSelector这个方法的位置肯定是处于runloop的callout方法里面,在这里等待runloop再callout从而调用aSelector方法完成,显然也是死等待,线程卡死。。。

还有一些performSelector方法,是不会添加到runloop的,而是直接执行,可以按照上面的特殊情况进行理解。方法列举如下:

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

看到这里,是否感觉有些乱???只要记住没有延迟或者等待的都不会添加到runloop,有延迟或者等待的还有排除上面提到的特殊情况。

  • Timer Sources:它的事件发送是同步的,这个用的比较多,会在下一篇专门介绍

  • Observers,观察者:首先它并不属于事件源(不会影响runloop的生命周期),它比较特殊,用于观察runloop自身的一些状态的,有以下几种:

    1. 进入runloop
    2. runloop即将执行定时器
    3. runloop即将执行输入源(Port,Customer,Selector Sources)
    4. runloop即将休眠
    5. runloop被唤醒,在处理完唤醒它的事件之前
    6. 退出

下面举例,监听所有状态,在非主线程(可以看到一个完整的周期):

+ (void)observerTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
/**
param1: 给observer分配存储空间
param2: 需要监听的状态类型:kCFRunLoopAllActivities监听所有状态
param3: 是否每次都需要监听,如果NO则一次之后就被销毁,不再监听,类似定时器的是否重复
param4: 监听的优先级,一般传0
param5: 监听到的状态改变之后的回调
return: 观察对象
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即将进入runloop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理input Sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将睡眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"从睡眠中唤醒,处理完唤醒源之前");
break;
case kCFRunLoopExit:
NSLog(@"退出");
break;
default:
break;
}
});
// 没有任何事件源则不会进入runloop
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:NO];
CFRunLoopAddObserver([[NSRunLoop currentRunLoop] getCFRunLoop], observer, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] run];
});
} + (void)doFireTimer {
NSLog(@"---fire---");
}

打印结果:一个完整的周期

RunLoop模式

runloop的模式,使得runloop显得更加灵活,适应更多的应用场景。

上面提到的事件源,都是处于特定的模式下的,如果和当前runloop的模式不一致则不会得到响应,举个例子:

如果定时器处于mode1,而runloop运行在mode2,则定时器不会触发,只有runloop运行在mode1时,定时器才会触发。

系统为我们提供了多种模式,下面列一些比较常遇到的:

  • kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
  • UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
  • NSRunLoopCommonModes: 包含了多种模式:default, modal, 和tracking modes。

除了系统给我们的模式,我们自己也可以自定义。

NSRunLoopMode 的类型为字符串类型,定义:typedef NSString * NSRunLoopMode,自定义类型就很简单了,示例代码如下:直接调用runLoopModeTest方法即可测试

- (void)modeTestTimer {
NSLog(@"mode:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
/// 这里使用非主线程,主要考虑如果一直处于customMode模式,则主线瘫痪
- (void)runLoopModeTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 target:self selector:@selector(modeTestTimer) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:@"customMode"];
[[NSRunLoop currentRunLoop] runMode:@"customMode" beforeDate:[NSDate distantFuture]];
});
}

runloop模式的切换

  • 对于非主线程,我们可以退出当前模式,然后再进入另一个模式,也可以直接进入另一个模式,即嵌套
  • 对于主线程,我们当然也可以像上面一样操作,但是主线程有其特殊性,有很多系统的事件。系统会做一些切换,我们更关心的是系统是如何切换的?系统切换模式时,并没有使用嵌套

主线程没有使用runloop嵌套是根据我的测试得出,没办法,官方文档太太太少,也没有更底层源码,只有CFRunLoop的源码:http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz

根据以上

最后总结下,thread--runloop--mode--event sources,关系可以表示如下:

RunLoop生命周期

可以分为三步:创建->运行(开启,内部循环)->退出

1. runloop创建

苹果是不允许开发人员手动创建runloop,runloop是伴随着线程的创建而创建,线程与runloop是一一对应的,具有唯一性,另外创建还区分是否为主线程

  • 主线程:系统会自动创建

  • 非主线程:系统不会自动创建,开发人员必须显示的调用[NSRunLoop currentRunLoop]方法来获取runloop的时候,系统才会创建,类似懒加载

系统只提供了两种方法获取runloop,currentRunLoopmainRunLoop,可以看出非主线程只有在自己的线程内才能获得runloop。

2. runloop运行

  • 开启:主线程系统会自动运行,那么非主线程也是需要开发人员显式调用的,可以通过如下方法
NSRunLoop提供的方法:
- (void)run; // 默认模式
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
CFRunLoop提供的函数:
/// 默认模式
void CFRunLoopRun(void);
/// 在指定模式,指定时间,运行
CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);

当执行了上面的运行方法后,如果runloop所在的模式没有对应的事件源,即上面图中提到的input sources、timer sources,会直接退出当前runloop(注意:是当前)。另外注意的是,input sources里面的Selector Sources,它有一些特殊情况,上面也提到了。这些情况下runloop还是会直接退出。

网上有很多说到事件源包括了observe,其实是不包含的,即runloop是否退出与observe没有关系,observe只是监听runloop本身的状态而已。

  • 内部循环(略复杂)

这样看起来还是比较清晰的。

关于自动释放池提一下(下一篇会做详细说明):

  • 第1步的观察者(优先级较高)会创建自动释放池
  • 第6步的观察者,会销毁老的自动释放池,并创建新的自动释放池,对于一个runloop来说,此步骤会不断的循环
  • 第10步的观察者,销毁自动释放池

上面提到的自动释放池的处理当然是系统帮我们处理的,非主线程和主线程系统都帮我们做了处理。官方说到,如果你使用POSIX thread APIs创建线程,那就是另外一套内存回收系统了,是不会用autoreleasePool,系统当然也不会创建。

3. runloop退出

可以用以下方式退出runloop

  • 设置最大时间到期:推荐使用这种方式
  • modeItem(事件源)为空:但并不推荐这样退出,因为一些系统的Item我们并不知道
  • 调用CFRunLoopStop,退出runloop并将程序控制权交给调用者(如果runloop有嵌套,则只退出最内层runloop),一些情况下,CFRunLoopStop并不能真正的退出runloop,比如你使用下面的2种方法开启runloop:
- (void)run; // 默认模式
- (void)runUntilDate:(NSDate *)limitDate;

当执行NSRunLoop的run方法,一旦成功(默认模式下有事件源),那么run会不停的调用runMode:beforeDate:来运行runloop,那么即便CFRunLoopStop退出了一个runloop,很快会有另一个runloop执行。即:如果你想退出一个runloop,那么你就不该调用run方法来开启runloop

runUntilDate:与run一样不停的执行runMode:beforeDate:方法,CFRunLoopStop也是退不出来的,不同的是runUntilDate:自己有个期限,超过这个期限会自动退出

很明显,你会想到利用事件源为空来退出,这种方法上面已经说了,不推荐。。。

一个不想回答的问题:runloop本身的释放。有人会纠结这个问题,经过多方查问、资料、源码、测试加自身理解,得出:runloop退出后,是不会被释放的(或者说立即),它大概很可能是伴随着线程的释放而释放。。。。。。欢迎补充

Runloop嵌套

嵌套,刚接触时感觉很神奇,然而一入嵌套深似海。。。特别是约瑟夫环的问题(http://www.jianshu.com/p/3c62ac7d9285)。。。

在当前runloop的callout函数里面执行上runloop,例程代码如下:

/**
runloop嵌套测试,
*/
+ (void)nestTest {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSTimer *tickTimer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle1) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tickTimer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:2]];
NSLog(@"-end-");
});
} /**
不停的运行与退出最内层runloop
*/
+ (void)timerHandle1 {
NSLog(@"timer111-%@",[[NSRunLoop currentRunLoop] currentMode]);
// 防止多次添加timer,开发中应特别注意
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSTimer *tickTimer2 = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(timerHandle2) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:tickTimer2 forMode:UITrackingRunLoopMode];
});
[[NSRunLoop currentRunLoop] runMode:UITrackingRunLoopMode beforeDate:[NSDate distantFuture]];
} + (void)timerHandle2 {
NSLog(@"timer222-%@",[[NSRunLoop currentRunLoop] currentMode]);
CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]);
}

打印结果

例程中外层runloop运行在NSDefaultRunLoopMode模式下,然后在它的callout函数(定时器1)又执行runloop,运行在UITrackingRunLoopMode模式下,实现嵌套,然后在内层runloop的callout(timerHandle2),停止运行当前runloop,即停止内层runloop,这时又回到外层循环。外层runloop只运行2秒到期。-end-

上面嵌套是运行在不同模式下,当同一模式下的runloop出现嵌套时,苹果依然处理的很好。举例:

  1. 将t1(timer1)添加到r1(runloop1),并在NSDefaultRunLoopMode模式下运行
  2. 在t1的响应函数里,将t2添加到r2,r2在NSDefaultRunLoopMode模式下运行
  3. 此时很明显,r2处于嵌套内层,则只应该运行t2的响应函数
  4. 在t2的响应函数里,退出r2,此时回到r1
  5. 会运行t1与t2的响应函数

可能你会觉得很诧异,t2怎么也会运行呢????其实这很符合逻辑:

假设在第2步骤中,我们没有执行r2,即没有r2,那么t2还是加到了r1上。既然是加到了r1那执行就不难理解了。(是否感觉苹果很强大?)

注意:r1与r2代表的是同一runloop,只是调用栈不同,或者说嵌套层。如果把runloop理解为一个函数,那么就可以理解为函数r1调用了自身,那个"自身"称为r2。

参考:http://www.jianshu.com/p/4263188ed940

NSRunLoop原理详解——不再有盲点的更多相关文章

  1. WebActivator的实现原理详解

    WebActivator的实现原理详解 文章内容 上篇文章,我们分析如何动态注册HttpModule的实现,本篇我们来分析一下通过上篇代码原理实现的WebActivator类库,WebActivato ...

  2. I2C 基础原理详解

    今天来学习下I2C通信~ I2C(Inter-Intergrated Circuit)指的是 IC(Intergrated Circuit)之间的(Inter) 通信方式.如上图所以有很多的周边设备都 ...

  3. Zigbee组网原理详解

    Zigbee组网原理详解 来源:互联网 作者:佚名2015年08月13日 15:57   [导读] 组建一个完整的zigbee网状网络包括两个步骤:网络初始化.节点加入网络.其中节点加入网络又包括两个 ...

  4. 块级格式化上下文(block formatting context)、浮动和绝对定位的工作原理详解

    CSS的可视化格式模型中具有一个非常重要地位的概念——定位方案.定位方案用以控制元素的布局,在CSS2.1中,有三种定位方案——普通流.浮动和绝对定位: 普通流:元素按照先后位置自上而下布局,inli ...

  5. SSL/TLS 原理详解

    本文大部分整理自网络,相关文章请见文后参考. SSL/TLS作为一种互联网安全加密技术,原理较为复杂,枯燥而无味,我也是试图理解之后重新整理,尽量做到层次清晰.正文开始. 1. SSL/TLS概览 1 ...

  6. 锁之“轻量级锁”原理详解(Lightweight Locking)

    大家知道,Java的多线程安全是基于Lock机制实现的,而Lock的性能往往不如人意. 原因是,monitorenter与monitorexit这两个控制多线程同步的bytecode原语,是JVM依赖 ...

  7. [转]js中几种实用的跨域方法原理详解

    转自:js中几种实用的跨域方法原理详解 - 无双 - 博客园 // // 这里说的js跨域是指通过js在不同的域之间进行数据传输或通信,比如用ajax向一个不同的域请求数据,或者通过js获取页面中不同 ...

  8. 节点地址的函数list_entry()原理详解

    本节中,我们继续讲解,在linux2.4内核下,如果通过一些列函数从路径名找到目标节点. 3.3.1)接下来查看chached_lookup()的代码(namei.c) [path_walk()> ...

  9. Influxdb原理详解

    本文属于<InfluxDB系列教程>文章系列,该系列共包括以下 15 部分: InfluxDB学习之InfluxDB的安装和简介 InfluxDB学习之InfluxDB的基本概念 Infl ...

随机推荐

  1. leetcode--001 max point on a line

    package leetcode; import java.util.HashMap; class Point{ int x; int y; Point(){ x=0; y=0; } Point(in ...

  2. .NET运行机制

    .NET运行机制   .NET框架是一个多语言组件开发和执行环境,它提供了一个跨语言的统一编程环境..NET框架的目的是便于开发人员更容易地建立Web应用程序和Web服务,使得Internet上的各应 ...

  3. radioButton添加试题选项webview(二)

    由于项目里radioGroup里,4个选项里加载的是webview,而不是radiobutton本身自己可设置的text类型,并且每个webview都需要和radiobutton对齐,所以这个布局有点 ...

  4. Django中扩展Paginator实现分页

    Reference:https://my.oschina.net/kelvinfang/blog/134342 Django中已经实现了很多功能,基本上只要我们需要的功能,都能够找到相应的包.要在Dj ...

  5. python 爬取的数据要如何展现(可视化)?

    我是把数据放在 mongodb ,然后单独一个脚本作分析,导出 json ,用 c3.js 画图,然后随便写个很简单的页面就好了. 展示在这里: http://107.170.207.236/job_ ...

  6. Grunt构建工具能做哪些事?

    Grunt到底有什么作用?一般用来干嘛? 很多前端的工作,包括Less编译.javascript压缩.Css压缩等零零碎碎的工作, 都可以让Grunt来做. 实际上在项目开发中,一般是前端代码 与 后 ...

  7. js 如何判断鼠标点击事件还是js代码调用

    <html> <head> <meta http-equiv="Content-Type" content="text/html; char ...

  8. webx学习

    webx框架学习指南 http://openwebx.org/docs/Webx3_Guide_Book.html webx学习(一)——初识webx webx学习(二)——Webx Framewor ...

  9. Oracle 11gR2 RAC ohasd failed to start 解决方法

    rcrCRS-4124: Oracle High Availability Services startup failed. CRS-4000: Command Start failed, or co ...

  10. Java Swing jpanel paint方法执行两次的问题

    Java Swing jpanel paint方法执行两次的问题: 在其他环境下执行了两次,自己测试怎么都是执行了一次,记录一下这个问题:需要后继工作: 可能是进行各种参数设置的时候导致了paint方 ...