什么是Runloop
· 一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
· Runloop类似于一个while循环,循环执行代码,保持程序的持续运行。
· RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
· 在iOS的工程的main.m文件中我们可以看到这样的代码:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain
函数内部就启动了一个Runloop,使App一直运行,这个默认开启的Runloop默认和主线程关联起来。
· 新建一个工程,在storyboard上加上按钮,运行
结果如下:
从Xcode左上角看的出来程序一直在运行
当把代码改为:
int main(int argc, char * argv[]) {
@autoreleasepool {
// return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
return 0;
}
}
main
函数直接返回0,AppDelegate里面的方法没有执行,然后程序就就退出了。
再把代码修改如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"%@", @"这里会打印");
int result = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"%@", @"这里不会打印");
return result;
}
}
运行结果如下:
程序执行了UIApplicationMain
后开启了默认的Runloop,一直循环15行,所以16行代码永远没有执行。
Runloop可以看作下面的伪代码:
int main(int argc, char * argv[]) {
BOOL AppIsRunning = YES;
while (AppIsRunning) {
id whoWakesMe = SleepForWakingUp();
id event = GetEvent(whoWakesMe);
HandleEvent(event);
}
return 0;
}
Runloop有什么用处
1、使程序一直运行接受用户输入
2、决定程序在何时应该处理哪些Event
3、调用解耦(对于编程经验为0的完全没搞懂这个意思,解释为Message Queue)
4、节省CPU时间
<br />
Runloop的机制
(套用sunnnyxx 在视频中提供的资料)
Runloop事件队列
RunLoop的挂起与唤醒
从伪代码可以看出
- 制定用于唤醒的
mach_port
端口
- 调用
mach_msg
- 监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在
mach_msg_trap
- 由另外一个线程(或另一个进程中的某个线程)向内核发送这个端口的
msg
后,trap
状态被唤醒,RunLoop继续开始干活
<br />
Runloop对象
1.iOS中有2tAPI来访问和使用RunLoop
-Foundation 框架
NSRunLoop
-Core Foundation
CFRunLoopRef
2.NSRunLoop和CFRunLoopRef都代表着RunLoop对象
3.NSRunLoop是基于CFRunLoopRef的一层OC包装, 所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API (Core Foundation 层面)
a 主线程的runloop自动创建,子线程的runloop默认不创建(在子线程中调用NSRunLoop *runloop = [NSRunLoop currentRunLoop];获取RunLoop对象的时候,就会创建RunLoop);
b runloop退出的条件:app退出;线程关闭;设置最大时间到期;modeItem为空;
c 同一时间一个runloop只能在一个mode,切换mode只能退出runloop,再重进指定mode(隔离modeItems使之互不干扰);
d 一个item可以加到不同mode;一个mode被标记到commonModes里(这样runloop不用切换mode)。
<br />Source是RunLoop的数据源抽象类(protocol)
RunLoop定义了两个Version的Source:
1、Source0:处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket
2、Source1:由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
如有需要,可从中选择一种来实现自己的Source
上一条基本不会发生
<br />RunLoopTimer的封装
// 创建但是不会加入当前 Runloop
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
// 创建但是加入当前 Runloop 的 NSDefaultRunLoopMode 并执行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;
<br />CFRunLoopObserver
向外部报告RunLoop当前状态的更改,框架中很多机制都由RunLoopObserver触发,如CAAnimation
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
UIKit通过RunLoopObserver在RunLoop两次Sleep
间对AutoreleasePool进行Pop和Push,将这次Loop中产生的Autorelease
对象释放
Runloop的寄生于线程:一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops;
自动释放池寄生于Runloop:程序启动后,主线程注册了两个Observer监听runloop的进出与睡觉。一个最高优先级OB监测Entry状态;一个最低优先级OB监听BeforeWaiting状态和Exit状态。
线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁)
<br />CFRunLoopMode
- RunLoop在同一段时间只能且必须在一种特定Mode下Run
- 更换Mode时,需要停止当前Loop,然后重启新Loop
- Mode是iOS App滑动顺畅的关键
- 可以定制自己的Mode
// 默认状态、空闲状态
NSDefaultRunLoopMode
// 滑动ScrollView时
UITrackingRunLoopMode
// 私有,App启动时
UIInitializationRunLoopMode
// Mode集合,可以理解为 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 的集合
NSRunLoopCommonModes
Runloop与GCD任务:
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调里执行这个 block。Runloop只处理主线程的block,dispatch 到其他线程仍然是由 libDispatch 处理的。
关于网络请求
iOS 中,关于网络请求的接口自下至上有如下几层:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
1.CFSocket 是最底层的接口,只负责 socket 通信。
2.CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
3.NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
4.NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。
下面主要介绍下 NSURLConnection 的工作过程。
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
Runloop实验
实验一
- (IBAction)buttonDidClick:(id)sender {
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
输出结果
实验二
把代码改成如下,输入结果一样
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
如果把[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
屏蔽,会发现没有打印东西,因为timerWithTimeInterval
这个方法只是创建了并没有加入Runloop
实验三 有scrollView的情况下使用Timer
在实验二的基础上,在vc中加一个textView,run起来,模拟器界面如下:
点击按钮,然后滚动scrollView,在停止滚动,打印结果
可以看的出来滚动的时间段,timer并没有效果,那是因为滚动的时候主线程Runloop已经切换mode为UITrackingRunLoopMode,Runloop只能指定一个mode,而timer只是加在NSDefaultRunLoopMode,所以发生滚动的时候,Runloop并不会响应timer;当松开手的时候Runloop切换回NSDefaultRunLoopMode,timer就重新起作用。
当我们把timer的mode修改为NSRunLoopCommonModes,此时滚动scrollView的同时也能响应timer:
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerTest) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
<br />
实验四 CFRunLoopSourseRef的实验
我们在button的响应注释,然后打个断点,run后点击button会发现如下类似这种UIEvent是属于Souce0
<br />
实验五 CFRunLoopObserverRef的实验
- (void)createObserver {
// 创建监听者对象
// rl: RunLoop
// observer: 监听者对象
// mode: Runloop所在的mode
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"observer--------%lu", activity);
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
CFRelease(observer);
}
根据CFRunLoopActivity枚举,我们可以看出Runloop的状态变化
1:即将进入Runloop-> 2:即将处理NSTimer-> 4:即将处理Souce0 -> 32:即将进入休眠 -> 64:从休眠仲唤醒
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
实验更新
代码:
- (IBAction)buttonDidClick:(id)sender {
NSLog(@"%s", __func__);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_myThread = [NSThread currentThread];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%@", @"+++++");
});
}
- (void)timerTest {
NSLog(@"%s", __func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(myThreadTest) onThread:_myThread withObject:nil waitUntilDone:NO];
}
- (void)myThreadTest {
NSLog(@"%s", __FUNCTION__);
}
点击按钮后打印出+++++
,然后点击屏幕空白处- (void)myThreadTest
并没有触发。
这是因为_myThread中的Runloop只run了一次就退出了,从而子线程没有监听到屏幕的点击事件。只run一次的原因首先看这张图
代码中只是让子线程的运行循环run了一次,并没有加入实质的source、port、Observer或者timer,Runloop直接跑一次直接退出了,导致点击时间没有Runloop来响应。
要响应- (void)myThreadTest
必须要子线程的Runloop保持驻留状态,给Runloop添加一个port让其保持驻留,此时我们点击button之后再点击屏幕空白处可以看到打印出来的日志,可以看的出来点击事件已经起效了,并且+++++
也没有打印出来,那是因为子线程的运行循环已经驻留,循环外面的代码就执行不到。
- (IBAction)buttonDidClick:(id)sender {
NSLog(@"%s", __func__);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_myThread = [NSThread currentThread];
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"++++");
});
}
Runloop使用
AFNetworking中RunLoop的创建
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
利用Runloop有话UITableView
因为UITableView滚动的时候主线程Runloop的mode切换为UITrackingRunLoopMode
,当停止滚动的时候会切回NSDefaultRunLoopMode
,从而可以减轻UITableView的卡顿。
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
参考资料:
http://blog.ibireme.com/2015/05/18/runloop/
http://www.jianshu.com/p/37ab0397fec7
https://yun.baidu.com/share/link?shareid=2268593032&uk=2885973690