关于RunLoop
今天学习runloop,开始啦~~
一.RunLoop的概念
一般来讲一个线程只能执行一次任务,执行完后就会退出。Runloop就可以让线程能随时处理事件但不退出。
1.什么是runloop?(面试题
)
- runloop是通过内部维护的
事件循环
来对事件、消息
进行管理的一个对象
。- 没有消息处理时,用户态--》内核态。休眠以避免资源占用。
- 有消息时,内核态--》用户态。立刻被唤醒。
2.关于用户态、内核态
- 应用程序一般是运行在用户态上面的,开发中绝大多数的api都是在用户层面的。
- 需要使用到操作系统、底层内部的指令,就发生了系统调用。有的系统调用会触发空间的切换。在内核态上。
- 之所以区分用户态和内核态,实际上是对计算机的一些资源调度、资源管理进行统一。这样就可以合理的进行资源调度,避免异常。
比如在内核态会有一些指令、终端、关机开机的操作。假如每个app都可以进行开机关机的操作,这个场景导致的效果是无法想象的。
3.main()函数为什么不会退出?(面试题
)
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
main()函数状态切换如上,main函数里面调用了UIApplicationMain,UIApplicationMain里面会启动线程的runloop。
main函数会一直处于“接受消息->处理->等待” 的循环中,直到这个循环结束。达到runloop可以做到有事情的时候做事情,没有事情的时候从用户态转换为内核态,避免资源浪费。
4.Runloop对象
关于runloop,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
- CFRunLoopRef是在 CoreFoundation 框架内(这个框架是开源的http://opensource.apple.com/tarballs/CF/)的,它提供了纯 C 函数的 API。
- NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API。
CFRunLoopRef
获得主线程的runloop
CFRunLoopRef mainRef = CFRunLoopGetMain();
获得当前的runloop对象
CFRunLoopRef currentRef = CFRunLoopGetCurrent();
NSRunLoop
获得主线程的runloop
NSRunLoop * mainRunloop = [NSRunLoop mainRunLoop];
获得当前的runloop对象
NSRunLoop * currentRunloop = [NSRunLoop currentRunLoop];
runloop的OC和C的API互相转换:
NSRunLoop * mainRunloop = [NSRunLoop mainRunLoop];
NSLog(@"%p-----%p",mainRunloop.getCFRunLoop,mainRunloop);
二. RunLoop与线程的关系
-
每个线程都有唯一的一个与之对应的Runloop对象。(其关系是保存在一个全局的 Dictionary 里。)
-
主线程的Runloop已经创建好了,子线程的Runloop需要主动创建
-
苹果不允许直接创建 RunLoop,可以通过CFRunLoopGetMain() 和 CFRunLoopGetCurrent()第一次获取的时候创建,在线程结束时销毁
-
只能在一个线程的内部获取其 RunLoop(主线程除外)
如下
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//获取子线程的runloop
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}
-(void)run{
//获得子线程对应的runloop|currentRunloop
//该方法本身是懒加载的,如果是第一次调用那么会创建当前线程对应的runloop并保存,以后则直接获取。
//创建
NSRunLoop * newThreadRunloop = [NSRunLoop currentRunLoop];
//开启runloop(该runloop开启后马上退出了,因为runloop需要一个mode才能运行)
[newThreadRunloop run];
}
三.RunLoop相关类/数据结构/对外的接口
CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
1.CFRunLoopRef
RunLoop.png如上图:
-
RunLoop和Mode关系:一对多
Mode和 Source/Timer/Observer关系:一对多 -
每次runloop启动的时候,只能指定一个mode。即currentMode。
-
如果要切换mode,只能退出loop,再重新指定一个mode进入。
(这么做是为了分开不同的mode里面的source/timer/observer),让其互不影响。
2. CFRunLoopSourceRef
CFRunLoopSourceRef 是事件产生的地方.
- source0
需要手动唤醒线程。先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。 - source1
具备唤醒线程的能力
。包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
3. CFRunLoopTimerRef
CFRunLoopTimerRef 是基于时间的触发器。和 NSTimer 是toll-free bridged(免费桥接) 的, 可以混用。
4. CFRunLoopObserverRef
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
};
如下:为当前runloop的状态添加监听
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//01 创建观察者
/*
第一个参数:allocator,用于分配存储空间的;用默认的CFAllocatorGetDefault
第二个参数:要监听的状态
第三个参数:是否要持续监听
第四个参数:和优先级相关的;传0
第五个参数:当runloop状态改变的时候会调用这个block块
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop启动");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop即将处理timer事件");
break;
case kCFRunLoopBeforeSources:
NSLog(@"runloop即将处理sourece事件");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"runloop即将进入休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"runloop休眠结束");
break;
case kCFRunLoopExit:
NSLog(@"runloop退出");
break;
default:
break;
}
});
//02 监听runloop的状态
/*
第一个参数:runloop对象
第二个参数:监听者
第三个参数:runloop在哪种运行模式下的状态
kCFRunLoopDefaultMode == NSDefaultRunLoopMode
kCFRunLoopCommonModes == NSRunLoopCommonModes
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
三. RunLoop的Mode
- 系统默认注册了5个mode
- NSDefaultRunLoopMode
默认的mode,通常主线程是在这个mode下运行;
- UITrackingRunLoopMode
界面跟踪的mode,用于scrollview追踪滑动,保证界面滑动时不受其他mode的影响;
- UIInitalizationRunLoopMode
在app启动时第一个mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode:
接收系统内部的mode,通常用不到
- NSRunLoopCommonModes
这是一个占位符mode,不是一个实际存在
的mode
是同步Source/Timer/Oberver到多个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<NSString *>
CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
CFRunLoopModeRef _currentMode; // 当前的 RunloopMode
CFMutableSetRef _modes; // Set< CFRunLoopMode *>
...
};
上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
上面的common modes有两种:kCFRunLoopDefaultMode和UITrackingRunLoopMode
common modes = <CFBasicHash 0x604000445430 [0x10589f960]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x106c0f060 [0x10589f960]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x105875790 [0x10589f960]>{contents = "kCFRunLoopDefaultMode"}
}
*/
RunLoop需要选择一个Mode才可以运行起来。
1)Runloop下面有很多个mode,运行的时候需要选择其中一个mode。
2)然后判断这个mode的item是否为空。Mode里面有一些Source、timer、Observer。判断有Source或者Observer,或者两者都有,说明这个mode不为空。则可以运行这个runloop了。
1. RunLoop的运行模式和NSTimer
1.1 NSTimer创建定时器方法1--需要指定mode
下面的mode,如果指定为NSDefaultRunLoopMode,则默认情况下定时器可以运行。
但是界面滑动的时候定时器也要可以操作,就需要指定为TrackingRunLoopMode。
想要默认和滑动模式下都可以运行定时器,那么就需要指定模式为NSRunLoopCommonModes。
//01创建定时器对象
NSTimer * timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
//02 把定时器对象添加到runloop中,并指定运行模式为默认。
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
1.2 NSTimer创建定时器方法2--mode为kCFRunLoopDefaultMode
scheduledTimerWithTimeInterval这个方法不需要手动启动runloop,会自动设置运行模式为kCFRunLoopDefaultMode。
如果需要让定时器在UITrackingRunLoopMode也运行,添加即可。
//该方法会自动将创建定时器对象添加到当前的runloop中
//运行模式为默认
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
//把当前的定时器也可以在滚动下运行,添加缺少的tracking模式即可
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
1.3 子线程添加timer--需要手动开启runloop
子线程里面没有runloop,所以使用NSTimer在子线程添加定时器是没有用的。需要开启runloop。
-(void)timer3{
[self performSelectorInBackground:@selector(addTimerForthread) withObject:nil];
}
-(void)addTimerForthread{
//这个方法里面指定模式为默认模式
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run1) userInfo:nil repeats:YES];
//子线程直接添加timer不会执行,需要手动添加runloop
//注意,要在指定运行 模式之后再开启runloop,runloop才能执行
[[NSRunLoop currentRunLoop] run];
NSLog(@"thread:%@",[NSThread currentThread]);
}
2. RunLoop的运行模式和GCD中的定时器--不会受到影响
上面记录到,NSTimer中的定时器工作会受到runloop运行模式的影响,而GCD中的定时器不会受到影响
。
@interface ViewController ()
@property(nonatomic,strong)dispatch_source_t timer;
@end
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
//NSTimer中的定时器工作会受到runloop运行模式的影响
//GCD中的定时器不会受到影响
//01 创建定时器对象
//队列(GCD)决定代码块在哪个线程中执行(主队列--主线程|非主队列--子线程中)
// dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,dispatch_get_global_queue(0, 0));
//02 设置定时器(开始时间|调用时间|误差)
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);//2:2秒执行一次;0:0秒误差
//03 事件回调
dispatch_source_set_event_handler(timer, ^{
NSLog(@"GCD Thread:%@",[NSThread currentThread]);
});
//04 起点定时器
dispatch_resume(timer);
_timer = timer;
}
四.RunLoop的实现机制
RunLoop的实现机制- 即将进入runloop;(通知observer,对应kCFRunLoopEntry)
- 将要处理Timer/Source0事件(通知observer,对应kCFRunLoopBeforeTimers、kCFRunLoopBeforeSources)
- 处理Timer、Source0事件
- 如果有Source1事件要处理,处理Source1事件
- 没有Source1事件,线程将要休眠(通知observer,对应kCFRunLoopBeforeWaiting)
- 线程进入休眠
- 线程收到消息,被唤醒(通知observer,对应kCFRunLoopAfterWaiting),去处理收到的消息
- 即将推出RunLoop的时候(通知observer,对应kCFRunLoopExit)
问:处于一个休眠的runloop,怎么唤醒?(
面试
)
Source1事件、NSTimer事件、外部手动唤醒
五.RunLoop的底层实现
- RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。
- mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。
- 当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到核心态;核心态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:
-----未完待续-----
六.苹果用 RunLoop 实现的功能
概念---
与多线程---
相关类/数据结构/对外的接口---
与NSTimer---
CFRunLoopObserverRef---
内部逻辑/事件循环机制---
底层实现---
source0如何手动唤醒线程
问:处于一个休眠的runloop,怎么唤醒?(
面试
)
Source1事件、NSTimer事件、外部手动唤醒???
看看放哪里
苹果用runloop实现的功能
应用:
[self.imageVie performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3 inModes:@[NSRunLoopCommonModes]];
AFNet
Async
常驻线程
面试问题汇总: