美文网首页
iOS RunLoop理解

iOS RunLoop理解

作者: 宙斯YY | 来源:发表于2017-11-16 15:13 被阅读25次

    RunLoop概念

    一个APP之所以能在程序运行起来不停止,就是RunLoop的原因,RunLoop就像一个死循环,等待处理外部手机操作,网络请求以及内部通讯等命令,其实RunLoop是管理线程的一种机制,这种机制不仅在iOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。

    RunLoop作用

    一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠。
    RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

    RunLoop和线程

    RunLoop是为了线程而生,没有线程,它就没有存在的必要。RunLoop是线程的基础架构部分。线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是RunLoop。

    RunLoop中API

    CocoaTouch和CoreFundation都提供了Runloop对象方便配置和管理线程的RunLoop。OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
    CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
    CocoaTouch层面提供的API比较简单:
    每个线程,包括程序的主线程都有与之相应的RunLoop对象。
    主线程的RunLoop默认是启动的,通过[NSRunLoop mainRunLoop]获得。子线程的RunLoop如果要启动需要手动调用。在任何一个CocoaTouch程序的线程中,都可以通过NSRunLoop *runloop = [NSRunLoop currentRunLoop]来获取到当前线程的RunLoop。开启子线程RunLoop,可以使用[[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
    那么,开启的RunLoop什么时候销毁呢?答案是当该线程销毁时,该线程的RunLoop肯定被销毁或者RunLoop的mode为空的时候销毁,那么怎么判断mode为空呢?答案是该mode中没有observer,source,timer事件就为空。

    CoreFundation层面提供的API相对更多一点:
    在 CoreFoundation 里面关于 RunLoop 有5个类:
    1.CFRunLoopRef
    它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。

    2.CFRunLoopModeRef
    一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。但是每次RunLoop运行时,只能指定其中一个Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

    3.CFRunLoopSourceRef
    处理事件源。Source有两个版本:Source0 和 Source1。分别处理不同事件,Source0处理外部交互事件,Source1处理内部通信等事件。

    4.CFRunLoopTimerRef
    处理Timer事件。

    5.CFRunLoopObserverRef
    观察者,监听RunLoop不同状态。

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    };
    
    -(void)observerRunLoop
    {
        //监听kCFRunLoopDefaultMode下的RunLoop状态
        CFRunLoopMode mode =kCFRunLoopDefaultMode;
        
        CFRunLoopObserverRef observer=CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 0, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    SDLog(@"RunLoop启动");
                    break;
                case kCFRunLoopBeforeWaiting:
                    SDLog(@"RunLoop即将休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    SDLog(@"RunLoop被唤醒");
                    break;
                case kCFRunLoopBeforeTimers:
                    SDLog(@"RunLoop即将处理Timers");
                    break;
                case kCFRunLoopBeforeSources:
                    SDLog(@"RunLoop即将处理Sources");
                    break;
                case kCFRunLoopExit:
                    SDLog(@"RunLoop退出");
                    break;
                    
                default:
                    break;
            }
        });
        
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, mode);
    }
    

    RunLoop关于mode的应用

    RunLoop包含5中mode(模式),每种模式接受不同的事件源。
    a. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
    b. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    c. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    d.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    e. kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

    每种mode中又包含三种item(soures,observer,timer),如果一个Mode中一个item都没有,则这个RunLoop会直接退出。我们的RunLoop要想工作,必须要让它存在一个Item(source,observer或者timer),主线程之所以能够一直存在,并且随时准备被唤醒就是因为系统为其添加了很多item。

    mode:最常见的两种模式,默认模式(空闲)NSDefaultRunLoopMode,UI模式UITrackingRunLoopMode,比如UI相关事件(滑动,点击等),就是主线程RunLoop在UITrackingRunLoopMode模式下进行监视处理的。

    1.例如:performSelector方法
    //performSelector默认是在当前RunLoop的默认模式下执行方法
    [self performSelector:@selector(test) withObject:self];
    //可以通过performSelector指定RunLoop模式的方式解决RunLoop问题
        [self performSelector:@selector(test) withObject:self afterDelay:3 inModes:@[NSRunLoopCommonModes]];
    
    2.例如:处理滑动时间和定时器冲突的问题

    主线程调用timer,添加到NSDefaultRunLoopMode的RunLoop中,此时滑动scrollview,那么timer将停止打印,若添加到UITrackingRunLoopMode的RunLoop中,滑动scrollview,那么timer可以打印,但是不滑动时不打印。原因就是滑动时主线程RunLoop在UITrackingRunLoopMode模式下运行,timer如果放到该模式下就能检测到,如果放到NSDefaultRunLoopMode模式下就检测不到。如果想在两种模式下都检测到,就都需要添加,当然,iOS为我们提供了复合的Mode-NSRunLoopCommonModes,包含上述两种模式。
    解决方法就是添加到NSRunLoopCommonModes。

    NSTimer * timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        }];
        
     [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
        
     [timer setFireDate:[NSDate distantPast]];
    

    还有种解决办法,就是子线程执行timer,或者使用dispatch_source_set_timer在全局并行队列执行。

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            
            timer=[NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
                
                
            }];
            
            [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
            
            [timer setFireDate:[NSDate distantPast]];
            
           //注意要开启子线程的RunLoop,因为子线程RunLoop默认关闭
    
            [[NSRunLoop currentRunLoop]runUntilDate:[NSDate distantFuture]];
        });
    
    timer=dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(timer, ^{
            
        });
        dispatch_resume(timer);
    
    3.开启常驻子线程

    NSRunLoop提供了添加item的API,也可以通过添加item让子线程RunLoop活下来。

    [NSRunLoop currentRunLoop]addTimer:(nonnull NSTimer *) forMode:(nonnull NSRunLoopMode)
    
    [[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
    
    [NSRunLoop currentRunLoop]addObserver:(nonnull NSObject *) forKeyPath:(nonnull NSString *) options:(NSKeyValueObservingOptions) context:(nullable void *):
    
    

    例如:让一个子线程处理处理完一个任务之后,再处理另一个任务。

    /*单纯使用线程间通讯是做不到的,因为子线程一旦执行完任务就销毁了啊,无法再被唤醒,除非使用该子线程常驻不被销毁。
    */
    [self performSelector:@selector(test) onThread:子线程 withObject:nil waitUntilDone:YES];
    
    /*这样就可以考虑在子线程中开启该子线程RunLoop,并让RunLoop做任务让该子线程保持存活,那么做什么任务呢,根据之前的知识,可以是source事件,可以是timer事件,也可以是observer,推荐使用基于端口的source0事件。
    */
        NSRunLoop *runloop=[NSRunLoop currentRunLoop];
        [runloop addPort:[NSPort port]forMode:NSDefaultRunLoopMode];
        [runloop run];
    

    RunLoop的内部逻辑

    RunLoop_1.png

    RunLoop循环内部会不断创建和销毁自动释放池处理一些垃圾数据(使用过的变量等)
    自动释放池第一次创建:当RunLoop启动时
    自动释放池最后一次销毁:当RunLoop销毁时
    自动释放池其他时间创建和销毁:当RunLoop即将进入休眠的时候,释放之前的自动释放池(回收数据),创建新的自动释放池。

    RunLoop在常用SDK中应用场景

    AFNetworking(相当于一个线程常驻的方式)
    这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 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 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 port 发送消息到 loop 内;但此处添加 port 只是为了让 RunLoop 不至于退出,并没有用于实际的发送消息。
    

    据说使用NSURLConnection的老前辈都需要通过RunLoop调试子线程的网络回调。

    PerformSelecter
    当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
    当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    AsyncDisplayKit
    ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

    参考文章:https://blog.ibireme.com/2015/05/18/runloop/

    相关文章

      网友评论

          本文标题:iOS RunLoop理解

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