CFRunLoop
- 这篇文章是在看了sunnyxx大神的线下分享后整理的学习笔记,感谢sunnyxx大神的分享,学习路上再接再厉。
- sunnyxx大神的自动算高工具UITableView-FDTemplateLayoutCell,里面有RunLoop的使用技巧。
https://github.com/forkingdog/UITableView-FDTemplateLayoutCell
概念
事件循环
每个线程都有一个RunLoop对象,但是只有主线程的RunLoop是开启的。子线程中的RunLoop默认是不被创建的,在子线程中当我们调用
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
获取RunLoop对象的时候,就会创建RunLoop一个线程可以开启多个RunLoop,只不过都是嵌套在最大的RunLoop中
作用
使程序一直运行并接收用户的输入
决定程序在何时处理哪些事件
调用解耦(主调方产生很多事件,不用等到被调方处理完事件之后,才能执行其他操作)
节省CPU时间(当程序启动后,什么都没有执行的话,就不用让CPU来消耗资源来执行,直接进入睡眠状态)
模拟RunLoop
int main(int argc, char * argv[]) {
while (程序在运行中) {
runloop睡觉呢
起床了,有事干了(唤醒runloop)
runloop干活中
}
return 0;
}
构成元素
每一个RunLoop都包含若干个CFRunLoopMode
在同一时间,只能在一种Mode下面执行
当需要切换Mode的时候,就必须退出当前的RunLoop。重新启动一个
系统默认的有以下5种模式
CFRunLoopDefaultMode: 这个是默认 Mode,也是空闲状态。主线程通常在这个 Mode 下运行的。
UITrackingRunLoopMode: ScrollView滚动时候的模式。
UIInitializationRunLoopMode: 在刚启动程序时进入的第一个 Mode,启动完成后就不再使用。
GSEventReceiveRunLoopMode: 接受系统事件的内部的Mode,这个Mode由GraphicsServices调用在CFRunLoopRunSpecific前面。通常用不到。
CFRunLoopCommonModes: 这是一个数组,包括了第1和第2种模式。
- CFRunLoopMode的应用举例
当我们在做图片轮播器的时候,如果使用的是kCFRunLoopDefaultMode那么当ScrollView滚动的时候,RunLoop模式就会切换为UITrackingRunLoopMode,这时候NSTimer就没法执行,这时候我们可以使用kCFRunLoopCommonModes,就可以解决这个问题。
CFRunLoopMode又包含若干个CFRunLoopSource\ CFRunLoopTimer\ CFRunLoopObserver
CFRunLoopSource
RunLoop的数据源抽象类(类似于OC中的protocol)
RunLoop定义了两个版本的source:Source0 和 Source1
Source0:处理的是App内部的事件、App自己负责管理,如按钮点击事件等。
Source1:由RunLoop和内核管理,Mach Port驱动,如CFMachPort、CFMessagePort
- CFRunLoopTimer的封装有(只是举例几个)
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval: (NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
CFRunLoopObserver
作用:告知外界RunLoop状态的更改
有以下状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 进入RunLoop开始跑了
kCFRunLoopEntry = (1UL << 0),
// 将要执行timer了
kCFRunLoopBeforeTimers = (1UL << 1),
// 将要执行Source了
kCFRunLoopBeforeSources = (1UL << 2),
// 将要进入睡眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出
kCFRunLoopExit = (1UL << 7),
// 全部的状态
kCFRunLoopAllActivities = 0x0FFFFFFFU
}
- CFRunLoopObserver的应用举例
- CFRunLoopObserver与Autorelease Pool
CFRunLoopObserver 监视到kCFRunLoopEntry(将要进入Loop)的时候,会调用_objc_autoreleasePoolPush() 创建自动释放池。
CFRunLoopObserver 监视到kCFRunLoopBeforeWaiting(将要进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;kCFRunLoopExit(即将退出Loop) 时会调用 _objc_autoreleasePoolPop() 来释放自动释放池。
- 重绘视图
苹果为了保证界面的流畅性,(1)不会重绘属性(frame等)没有改变的视图(2)只发送一次drawRect:消息。
当相关的视图对象接收到设置属性的消息的时候,就会将自己标记为要重绘。RunLoop会收集所有等待重绘制的视图,苹果会注册一个CFRunLoopObserver来监听kCFRunLoopBeforeWaiting事件,当事件触发的时候,就会对所有等待重绘的视图对象发送drawRect:消息。
RunLoop的挂起和唤醒
- 当RunLoop处于空闲状态或者点击了暂停的时候,RunLoop就被挂起,具体步骤
(1) 指定用于再次唤醒的端口(mach_port)
(2) 调用mach_msg
监听唤醒端口。内核调用mach_msg_trap
让RunLoop处于mach_msg_trap
状态,RunLoop就会挂起,等待激活。就像一段代码中有scanf函数,必须要接收一个输入一样,不输入就不会继续往下执行。这里要区别于sleep。或者像是Notification,当有post的时候,才会被唤醒。
(3)由另一线程(或者另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态就会被唤醒,RunLoop就继续工作
RunLoop的实现
// 底层的实现函数
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled){
// 配置RunLoop的Mode
SetupCFRunLoopMode()
// 通知 Observers 将要进入 Loop
__CFRunLoopDoObservers(kCFRunLoopEntry);
// 通过GCD设置RunLoop的超时时间
SetupThisRunLoopRunTimeoutTimer();
// RunLoop开始处理事件 do while 循环
do {
// 通知 Observers 将执行timer
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
// 通知 Observers 将执行Source0
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 执行blocks
__CFRunLoopDoBlocks();
// 执行Source0
__CFRunLoopDoSource0();
// 问 GCD 主线程有没有需要执行的东西
CheckIfExistMessagesInMainDispatchQueue();
// 通知 Observers 将进入睡眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
/* 指定 唤醒端口
监听 mach_msg 会停在这里
进入 mach_msg_trap 状态
睡眠中...
*/
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// 接收到 消息 通知Observers RunLoop被唤醒了
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 处理事件
if (wakeUpPort == timerPort) {
// 唤醒端口是 timerPort 执行timer回调 /* DOES CALLOUT */
__CFRunLoopDoTimers();
} else if (wakeUpPort == mainDispatchQueuePort) {
// 唤醒端口 执行mainQueue里面的调用
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else {
// 唤醒端口 执行Source1回调
__CFRunLoopDoSource1();
}
// 执行 blocks
__CFRunLoopDoBlocks();
// 当事件处理完了、被强制停止了、超时了、Mode是空的时候就会退出 循环
} while (!stop && isStopped !timeout && !ModeIsEmpty );
// 通知 Observers 将退出Loop
__CFRunLoopDoObservers(kCFRunLoopExit);
}
其中var wakeUpPort = SleepAndWaitForWakingUpPorts();
这句伪代码可以看作是RunLoop的核心。内部实现简化为这样:先调用__CFRunLoopServiceMachPort() ——> 里面会调用mach_msg()
函数 然后会卡在这里,等待接收消息来唤醒RunLoop。直到下面的某个条件被触发才被唤醒:
time_out 超时时间到了
有一个Source事件
timer的时间到了
RunLoop 调用mach_msg()
函数去接收消息,如果没有其他 mach_port
发送消息过来,内核就会将线程置于等待状态,直到接收到msg。就好比我们在一个函数中,调用了scanf()函数来接收输入一样,只有收到了输入信息,代码才能继续向下执行,否则会一直卡在那里。
- GCD 和 RunLoop
在RunLoop的内部实现中,用到了很多GCD的东西。比如刚刚开始run的时候,通过DISPATCH_SOURCE_TYPE_TIMER
该类型的dispatch_source
设置了RunLoop的超时时间。还可以在上面RunLoop实现的伪代码中看到__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
,只要是dispatch到main_queue的,CoreFoundation 都会调用这个函数,之后,libdispatch.dylib 就会执行回调。
RunLoop实践
-
AFNetworking中RunLoop的创建
在AFN中当使用 NSURLConnection 去执行网络操作的时候,会遇到还没有收到服务器的回调,线程就已经退出了。为了解决这一问题,作者使用到了RunLoop。下面是AFN中的一段代码:
+ (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;
}
上面这段代码在AFURLConnectionOperation.m
中的 162 行。
这是创建一个常驻服务线程的好方法。比如,当我们的程序要提供语音服务的时候,就可以创建一个专门为语音功能服务的线程,当需要语音服务的时候,这个线程就可以来执行。
- 一个TableView延迟加载图片的新思路
当cell上有需要从网络获取的图片的时候,我们滚动tableView,异步线程会去加载图片,加载完成后主线程就会设置cell的图片,这个时候就会出现卡的现象。一般的解决方案是调用tableView的代理方法,判断tableView是否正在滑动,如果在滑动,就不设置图片,等停止滑动后再去设置cell的图片。用Runloop能更简单的解决这个问题。我们可以根据RunLoop不同Mode下,执行不同的事件来解决这个问题思路如下:当设置图片的时候,让其在 CFRunLoopDefaultMode 下进行。当滚动tableView的时候,RunLoop是在 UITrackingRunLoopMode 这个Mode下,就不会设置图片,当停止的时候,就会设置图片。
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
- 让Crash的App回光返照
App崩溃的发生分两种情况:
(1) program received signal:SIGABRT SIGABRT
一般是过度release 或者 发送 unrecogized selector导致。
(2) EXC_BAD_ACCESS
是访问已被释放的内存导致,野指针错误。
由 SIGABRT 引起的Crash 是系统发这个signal给App,程序收到这个signal后,就会把主线程的RunLoop杀死,程序就Crash了 该例只针对 SIGABRT引起的Crash有效。
- Signal: 是Unix、类Unix等操作系统中进程间通讯的一种方式,用来通知一个事件发生。当一个singal发送给进程,操作系统就会中断进程的正常控制流程,如果在进程中定义了信号的处理函数,那么这个函数就会被执行,因此我们可以注册signal,并指定收到signal后要执行的函数
为了让App回光返照,我们需要来捕获 libsystem_sim_c.dylib
调用 abort() 函数发出的程序终止信号,然后让其执行我们定义的处理signal的方法。在方法中,我们需要开启一个RunLoop,保持主线程不退出。
// 创建RunLoop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
// 设置Mode
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
// 弹窗告知 程序挂了
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩溃了" message:@"崩溃信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alertView show];
while (1) {
for (NSString *mode in allModes) {
// 快速的切换 Mode 就能处理滚动、点击等事件
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
备注
有哪些地方理解的不对,希望大神们能够指出,感激不尽。