RunLoop

作者: Drmshow | 来源:发表于2015-05-26 22:00 被阅读24758次

    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);
    
    }
    
    }
    
    

    备注

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

    相关文章

      网友评论

      本文标题:RunLoop

      本文链接:https://www.haomeiwen.com/subject/ejyrqttx.html