RunLoop

96
Drmshow
2015.05.26 22:00* 字数 2141

CFRunLoop

概念

  • 事件循环

  • 每个线程都有一个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种模式

  1. CFRunLoopDefaultMode: 这个是默认 Mode,也是空闲状态。主线程通常在这个 Mode 下运行的。

  2. UITrackingRunLoopMode: ScrollView滚动时候的模式。

  3. UIInitializationRunLoopMode: 在刚启动程序时进入的第一个 Mode,启动完成后就不再使用。

  4. GSEventReceiveRunLoopMode: 接受系统事件的内部的Mode,这个Mode由GraphicsServices调用在CFRunLoopRunSpecific前面。通常用不到。

  5. CFRunLoopCommonModes: 这是一个数组,包括了第1和第2种模式。

  • CFRunLoopMode的应用举例

当我们在做图片轮播器的时候,如果使用的是kCFRunLoopDefaultMode那么当ScrollView滚动的时候,RunLoop模式就会切换为UITrackingRunLoopMode,这时候NSTimer就没法执行,这时候我们可以使用kCFRunLoopCommonModes,就可以解决这个问题。

  • CFRunLoopMode又包含若干个CFRunLoopSource\ CFRunLoopTimer\ CFRunLoopObserver

  • CFRunLoopSource

  • RunLoop的数据源抽象类(类似于OC中的protocol)

  • RunLoop定义了两个版本的source:Source0 和 Source1

  1. Source0:处理的是App内部的事件、App自己负责管理,如按钮点击事件等。

  2. 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的应用举例
  1. CFRunLoopObserver与Autorelease Pool

CFRunLoopObserver 监视到kCFRunLoopEntry(将要进入Loop)的时候,会调用_objc_autoreleasePoolPush() 创建自动释放池。

CFRunLoopObserver 监视到kCFRunLoopBeforeWaiting(将要进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;kCFRunLoopExit(即将退出Loop) 时会调用 _objc_autoreleasePoolPop() 来释放自动释放池。

  1. 重绘视图

苹果为了保证界面的流畅性,(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。直到下面的某个条件被触发才被唤醒:

  1. time_out 超时时间到了

  2. 有一个Source事件

  3. 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);

}

}

备注

有哪些地方理解的不对,希望大神们能够指出,感激不尽。

随笔