8.RunLoop
什么是RunLoop?
RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
runloop循环不是单纯的for...while循环,而是发生一个用户态到内核态切换,以及内核态到用户态的一个切换
它维护的事件循环,可以用来不断的处理消息或者事件,对他们进行管理
当没有消息进行处理时,会从用户态经过系统调用进入到内核态,由此可以用来当前进程/线程的休眠,会把控制权交给内核态,避免资源占用
当有消息需要处理时,会发生从内核态到用户态的切换,当前的用户线程会被唤醒
所以说,状态切换才是Runloop的关键
关于用户态和内核态
何时切换:
为什么区分状态:
什么是事件循环?(总结):
入口函数:
在我们程序中,默认是从主函数进行程序启动
按说在main函数里,顺着执行体代码,进行依次执行,最后main函数就会退出,我们的程序也会随之退出
main函数为何能保持不退出?
在main函数中,会调用UIApplicationMain函数,
在内部会启动主线程的Runloop,可以不断的接收消息,比如点击屏幕事件,滑动列表以及处理网络请求的返回等
接收消息后对事件进行处理,处理完之后,就会继续等待
Runloop是对事件循环的一种维护机制,可以做到在有事做的时候做事,没有事情的时候会通过用户态发生到内核态的切换,避免资源占用,当前线程处于休眠的状态。
RunLoop的数据结构
在 OC 中实际提供了两个 RunLoop 的
一个是 NSRunLoop
一个是 CFRunLoop
NSRunLoop 是对 CFRunLoop 的封装,提供了一些面向对象的 API
NSRunLoop 是位于 Foundation 当中的,CFRunLoop 位于 CoreFoundation 当中的
RunLoop 的数据结构主要有三个
struct __CFRunLoop {
CFRuntimeBase _base;
...
/*C级别的一个线程对象 , RunLoop和线程是一一对应的关系*/
pthread_t _pthread;
/*数据结构:NSMutableSet
RunLoop当前所处的模式mode,其实是CFRunLoopMode的数据结构,
*/
CFRunLoopModeRef _currentMode;
/*多个mode的集合,从数据结构看出,
RunLoop和它的mode是一对多的关系
*/
CFMutableSetRef _modes;
/*也是一个集合里面都是字符串,
有别于_modes里面的元素CFRunLoopModeRef
*/
CFMutableSetRef _commonModes;
/*
也是一个集合,包含多个Observer(观察者)、Timer、Source。
我们可以为RunLoop添加Observer(观察者),包括Timer、Source都可以提交到某一个RunLoop对应的
某一个_currentMode上面。
*/
CFMutableSetRef _commonModeItems;
};
CFRunLoopMode具体的成员变量
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例如 @"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // 结构
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
CFRunLoopSource
在 CF 框架当中官方名称叫 CFRunLoopSource ,有两种 source0 和 source1
唤醒线程就是从内核态切换到用户态
和平时所使用的 NSTimer 是具备免费桥转换的
观测时间点
可以通过注册一些 Observer 来实现对 RunLoop 的一些相关时间点的监测或者观察
问:我们可以监听 RunLoop 哪些时间点?
一个 RunLoop 可以对应多个 mode,每个 mode 当中又可以有多个 Sources1,Timers,Observers
当 RunLoop 运行在某一个 mode 上的时候,比如说运行在 mode1 上面,这个时候如果 mode2 当中某一个 timer 事件或者 Observe 事件回调了,这个时候是没有办法接收对应 mode2 当中所回调过来的事件,这就 RunLoop 有多个 mode 的原因,实际上起到的就是一个屏蔽的效果,当运行到 mode1 上时,只能接收处理 mode1 当中的 Sources1,Timers,Observers
思考:一个 Timer 要想同时加入到两个 mode 里面,需要怎么做?如果说这个 Timer 既想要它在 mode1 上可以正常运行,在每一个事件回调中做正常的处理,在 mode2 上也需要做相应的处理和事件的回调接收
系统提供了添加到两个 mode 的机制的,这样可以保证当 RunLoop 发生 mode 切换的时候也可以让对应的 Timer 等事件正常处理接收
这实际上就涉及到了 CommonMode(CommonMode 的特殊性):
CommonMode 本身是有它的特殊性的,CommonMode 并不是一个实际存在的模式,在 OC 当中经常会通过 NSRunLoopCommonModes 字符串常量来表达 CommonMode
CommonMode 本身和 defaultMode 是有区分的,调用一个线程运行在 CommonMode 上面和运行在具体的 defaultMode 上是有区别的
这里有个概念叫 “CommonModes”:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
总结:
什么是RunLoop?它是怎么做到有事做事,没事休息的?
1、RunLoop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。
2、程序运行会调用main函数,在main函数里面调用UIApplicationMain,UIApplicationMain函数会启动主线程的runloop。
3、runloop运行后,会调用系统方法mach_msg(),会使得程序从用户态变成核心态,此时线程处于休眠状态。
4、当有外界条件变化(Source/Timer/Observer),mach_msg会使得程序从核心态变成用户态,此时线程处于活跃状态。
RunLoop与线程是怎么样的关系
1、RunLoop与线程是一一对应的关系。
2、一个线程默认是没有runloop的(主线程除外),我们需要为它手动创建。
#import "MCObject.h"
@implementation MCObject
//定义两个k静态全局变量
// 自定义线程
static NSThread *thread = nil;
// 标记当前线程是否要继续事件循环
static BOOL runAlways = YES;
+ (NSThread *)threadForDispatch{
if (thread == nil) {
@synchronized(self) {
if (thread == nil) {//采用线程安全的方式去创建thread,入口方法为runRequest
// 线程的创建
thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
[thread setName:@"com.imooc.thread"];
// 启动
[thread start];
}
}
}
return thread;
}
+ (void)runRequest
{
// 创建一个Source
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 为thread线程创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
//while循环维持RunLoop的事件循环
// 如果可以运行
while (runAlways) {
//确保每次yRunLoop运行一圈的时候能够对内存进行释放
@autoreleasepool {
/* 令当前RunLoop运行在DefaultMode下面,注意这个运行的mode和上面添加资源的mode必须是同一个mode
否则把事件源添加到另一个mode上,而运行的defaultMode下,是无法维持运行的
函数内部会调用mach_msg,发生由用户态到核心态的切换,当前线程就会休眠,就停在里面,不是死循环
1.0e10是让循环运行到指定时间退出,这个代表很久远的时间
true代表资源被处理后是否马上返回
*/
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
}
}
// 如果Runloop的mode中没有对应的事件源可以处理,runloop就会自动退出
// 所以我们在某一时机 将source移除,静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出并s释放掉
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
@end
在用户进行滑动的过程中,当前的RunLoop运行在UITrackingRunLoopMode模式下,
而我们一般对网络请求是放在子线程中,子线程返回给主线程的数据要抛给主线程用来更新UI,
可以把这部分逻辑包装起来,提交到主线程的default模式下,这样的话,当用户滑动时,default模式下的任务不会执行,
当用户手停止时,mode就切换到了default模式下,就会处理子线程的数据了,这样就不会打断用户的滑动操作了
1、把子线程抛给主线程进行UI更新的逻辑,可以包装起来,提交到主线程的NSDefaultRunLoopMode模式下面。
2、因为用户滑动操作是在UITrackingRunLoopMode模式下进行的。
//参考代码事件
[self.tableView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];