GCD的内部的线程池相关问题
RunLoop与RunTime
什么是RunLoop?
RunLoop是通过内部维护的事件循环来对 事件/消息 进行管理的 对象
事件循环包括两点:
没有消息需要处理时,休眠以避免资源占用
有消息需要处理时,立刻被唤醒
RunLoop的关键点,就是用户态与内核态的切换
问:为什么iOS中main()函数可以保持一直运行状态。而不退出?
在main()函数内部,会调用UIApplicationMain()函数,在UIApplicationMain()函数内部会启动主线程的RunLoop,RunLoop通过内部维护的事件循环来对 事件/消息 进行管理,能做到有事做的时候去做事,没事做的时候,通过用户态到内核态的切换,进行休眠,从而避免资源浪费。
RunLoop如何维护事件循环机制呢?
RunLoop的数据结构
苹果提供了两套RunLoop:NSRunLoop和CFRunLoop
CFRunLoop是Core Foundation框架的,CF开头的框架是开源的
NSRunLoop是Foundation框架的
NSRunLoop是对CFRunLoop的封装,提供了面向对象的API
CFRunLoop
CFRunLoopModel
Source/Timer/Observer
CFRunLoopSource
Source0
需要手动唤醒线程
触摸事件处理
performSelector:onThread:
Source1
具备唤醒线程的能力
基于Port的线程间通信
系统事件捕捉
CFRunLoopTimer
基于事件的定时器
和NSTimer是 toll-free bridged(免费桥转换)的
CFRunLoopObserver
问:我们可以监听RunLoop的哪些时间点?
kCFRunLoopBeforeWaiting:waiting等待、休眠。是用户态到内核态的转换
kCFRunLoopAfterWaiting:是内核态到用户态的转换
线程和RunLoop是一对一的关系
RunLoop跟Model是一对多的关系
Model和Source/Timer/Observer也是一对多的关系
一个RunLoop包含若干个Mode,每个Mode又包含若干个source0、source1、observer、timer
RunLoop启动时只能选择其中一个Mode作为currentMode。
如果需要切换Mode,只能退出当前loop,重新选择一个Mode进入。
不同组的source0、source1、observer、timer能分隔开来,互不影响
如果Mode里没有任何source0、source1、observer、timer,RunLoop会立马退出
CFRunLoopCommonModes
CFRunLoopCommonModes并不是实际存在的一种Mode
commonMode是同步Source/Timer/Observer到多个Mode中的一种技术方案
问:点击App图标,从启动到杀死进程,中间经历了哪些?
从大的方面,经历了程序启动的三个方面:dyld、runtime、main函数
还可以从RunLoop的角度去分析
RunLoop和NSTimer
两种方法:
方法一:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
方法二:
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode);
RunLoop与多线程
线程是和RunLoop是一一对应的
自己手动创建的线程默认是没有RunLoop的
问:怎样实现一个常驻线程?
为当前线程开启一个RunLoop
向该RunLoop中添加一个Port或者Source维护RunLoop的事件循环
启动该RunLoop
#import "MCObject.h"
@implementation MCObject
static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;
+ (NSThread *)threadForDispatch{
if (thread == nil) {
@synchronized(self) {
if (thread == nil) {
// 线程的创建
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);
// 创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 如果可以运行
while (runAlways) {
@autoreleasepool {
// 令当前RunLoop运行在DefaultMode下面
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
}
}
// 某一时机 静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
@end
问:怎么保证子线程数据回来更新UI的时候,不打断用户的滑动操作?
网络数据请求通常在子线程中运行,以避免阻塞主线程。但是,由于只有主线程可以进行 UI 更新,所以在数据加载完成后需要回到主线程来更新 UI。如果在主线程中直接更新 UI,可能会导致 UI 布局计算和绘制操作打断正在进行的用户滑动操作。
平时App的主运行循环是在kCFRunLoopDefaultMode
下执行的,当用户触发滑动操作时,主运行循环会切换到UITrackingRunLoopMode
下执行,以保证滑动操作的流畅性。
滑动结束后,Runloop由UITrackingRunloopMode
又回到kCFRunLoopDefaultMode
下了。
所以,可以在将数据请求的结果回调切换到主线程中执行的时候,将其切换到NSDefaultRunLoopMode
,这样就不会在用户滑动的时候进行 UI 更新,从而保证滑动的流畅性。
即:将子线程数据给主线程刷新UI的时候,包装后提交到主线程的defaultModel下,这样两个model不会同时执行,也就不会打断用户的滑动操作。
代码示例:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 在这里下载数据...
NSData *data = ... // 下载的数据
// 回到主线程
dispatch_async(dispatch_get_main_queue(), ^{
[self.imageView performSelector:@selector(setImage:)
withObject:[UIImage imageWithData:data]
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
});
//或者
//[self performSelectorOnMainThread:@selector(updateUI) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
});