zoukankan      html  css  js  c++  java
  • iOS NSRunloop

    什么是Runloop

    Runloop即运行循环。为什么你的APP放在那里不去动它,在某个时间点去操作它,它还会给你反馈。就是因为Runloop的存在。
    总结一下,因为Runloop的存在,保证你的程序不会死。

    主要负责什么?
    1. 使程序一直运行并接受用户输入
    2. 决定程序在何时处理一些Event
    3. 调用解耦(Message Queue)
    4. 节省CPU时间(没事的时候闲着,有事的时候处理)
    谁依赖NSRunloop
    1. NSTimer
    2. UIEvent
    3. autorelease
    4. NSObject(NSDelaydPerforming)
    5. NSObject(NSThreadPerformAddtion)
    6. CADisplayLink
    7. CATransition
    8. CAAnimation
    9. dispatch_get_main_queue()
    10. AFNetworking(NSURLConnection)
    11. ...

    主线程几乎所有的函数都从以下的6个之1的调起

    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
    __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
    

    构成元素

     
    Snip20160907_437.png

    因为NSRunloop是对CFRunloop的封装,所以这里只看CFRunLoop就可以了。

    CFRunLoopTimer的封装

    系统提供的NSTimer、CADisplayLink、performSelector等都是对CFRunLoopTimer的封装。

    CFRunLoopSource

    Source是RunLoop的数据源抽象类(用OC的话来讲就是protocol)。
    RunLoop定义了两个版本的Source,分别是Source0和Source1。

    1. Source0:处理APP内部事件、APP自己负责管理(触发),如UIEvent、CFSocket
    2. Source1:由RunLoop和内核管理,Mach Port驱动,如CFMachPort、CFMessagePort
    CFRunLoopObserver

    观察者,向外部报告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
    };
    

    框架中很多机制都由CFRunLoopObserver触发,比如CAAnimation
    举例:

    self.navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>
    

    当程序执行完这行代码时,我们可以看到经历push动画之后,到达了一个新的界面。
    但其实并不是执行完这行代码就出现了Push的动画。
    其实,执行这段代码时不会立刻就掉push动画,而是要RunLoop循环一圈收集所有的Animation操作,汇集起来一起去调。

    CFRunLoopObserver与AutoreleasePool

    对象的释放并不是在{}括号结束。而是稍微延迟了一点。
    堆栈如下:

    _wrapRunLoopAutoreleasePoolHandler
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
    

    UIKit通过RunLoopOberser在RunLoop两次Sleep间对AutoreleasePool进行Pop和Push,将这次Loop产生的Autorelease对象释放。
    也就是RunLoop跑一圈没事了就睡,被唤醒了再跑下一圈,在两次sleep之间对自动释放池进行释放。

    CFRunLoopMode

    注意

    RunLoop在同一段时间只能且必须在一种特定Mode下Run。
    更换Mode时,需要停止当前Loop,然后重启新Mode。
    Mode是iOS滑动顺畅的关键。

    类型
    1. NSDefaultRunLoopMode
      默认状态(空闲状态),比如点击按钮都是这个状态
    2. UITrackingRunLoopMode
      滑动时的Mode。比如滑动UIScrollView时。
    3. UIInitializationRunLoopMode
      私有的,APP启动时。就是从iphone桌面点击APP的图标进入APP到第一个界面展示之前,在第一个界面显示出来后,UIInitializationRunLoopMode就被切换成了NSDefaultRunLoopMode。
    4. NSRunLoopCommonModes
      它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合。结构类似于一个数组。在这个mode下执行其实就是两个mode都能执行而已。
      典型的应用场景这样:当前界面有开启一个NSTimer,并且滑动UIScrollView。正常开启NSTimer后,滑动UIScrollView时它是不滑动的。解决办法就是把这个timer加入到当前的RunLoop,并把RunLoop的mode设置为NSRunLoopCommonModes。这样就可以保证不管你是NSDefaultRunLoopMode里跑,还是UITrackingRunLoopMode里跑,这个timer都可以执行。
    self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0625
                                                      target:self
                                                    selector:@selector(progressChange)
                                                    userInfo:nil
                                                     repeats:YES];
        
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
    

    当你开始滑动UIScrollView时,RunLoop的mode状态变化如下:

    NSDefaultRunLoopMode -> UITrackingRunLoopMode -> NSDefaultRunLoopMode
    

    开始滑动时,第一次mode的切换会把NSDefaultRunLoopMode停掉。然后开启新的UITrackingRunLoopMode。当滑动停止时,由UITrackingRunLoopMode切换回NSDefaultRunLoopMode,这时UITrackingRunLoopMode被停止,又切换回了老的NSDefaultRunLoopMode(这个老的NSDefaultRunLoopMode应该是重新开始的)。

    RunLoop和GCD的关系

    RunLoop和GCD的关系,准确来说是只要使用了dispatch_get_main_queue(),就与RunLoop有了关系。

    因为GCD中dispatch到main queue的block被分发到main RunLoop执行。

    RunLoop的挂起和唤醒

    我写了个demo,运行,然后点击debug栏的暂停,查看堆栈,如下:

     
    Snip20160907_438.png
    1. 指定用于唤醒的mach_port接口
    2. 调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap状态
    3. 由另一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活。

    RunLoop迭代执行顺序

    伪代码:

    SetupThisRunLoopRunTimeoutTimer();  //by GCD timer
    do{
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
    
        __CFRunLoopDoBlocks();
        __CFRunLoopDoSource0();
    
        CheckIfExistMessagesInMainDispatchQueue();    //GCD
    
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        var wakeUpPort = SleepAndWaitForWakingUpPorts();
        //mach_msg_trap
        //Zzz...
        //Received mach_msg , wake up
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        //Handle msgs
        if(wakeUpPort == timerPort){
            __CFRunLoopDoTimers();
        }else if(wakeUpPort == mainDispatchQueuePort) {
            //GCD
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
        }else {
              __CFRunLoopDoSource1();
        }
        __CFRunLoopDOBlocks();
    }while(!stop && !timeout);
    
    

    代码解读

    //首先do..while循环不能是一个死循环,所以在这里设置一个过期时间
    //这件事是GCD干的,用来检测do..while循环跑了多久
    SetupThisRunLoopRunTimeoutTimer();
    
    //开始跑循环
    do{
        //告诉observer我要跑timer了
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        //告诉observer我要跑source了
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
    
        __CFRunLoopDoBlocks();
        //程序跑到这里会查询Source0有什么消息
        __CFRunLoopDoSource0();
    
        //询问GCD你有没有存在主线程的东西需要我帮你调
        CheckIfExistMessagesInMainDispatchQueue();  //GCD
    
        //告诉observer我要睡了,RunLoop进入到挂起状态
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
    
        //进入trap状态,程序跑到这里就卡在这不动了,等待被某个Port唤醒
        var wakeUpPort = SleepAndWaitForWakingUpPorts();
    
        //被唤醒后,告诉observer我被唤醒了
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
    
        //假如是被timer唤醒的
        if(wakeUpPort == timerPort){
            //就去循环遍历和timer有关的回调
            __CFRunLoopDoTimers();
        }else if(wakeUpPort == mainDispatchQueuePort) {
            //如果是主线程的GCD把我唤醒的,那RunLoop就知道GCD要让它做事了,然后就取调GCD的这些事件
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
        }else {
              //如果都不是,就是Source1,Source1是基于Port事件的,比如网络某个端口来数据了,就会把RunLoop唤醒,去对来的数据进行处理
              __CFRunLoopDoSource1();
        }
        __CFRunLoopDoBlocks();
    }while(!stop && !timeout);
    //判断条件:有没有被外部干掉 && 到了过期时间
    //如果过期时间不手动进行设置的话,默认值是一个很大的值,可能是Int_Max
    

    AFNetworking是如何玩转RunLoop的

    + (void)networkRequestThreadEntryPoint:(id)_unuserd object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
    
            //为了不让runloop run起来没事干导致消失
            //所以给runloop加了一个NSMachPort,给它一个mode去监听
            //实际上port什么也没干,就是让runloop一直在等,目的就是让runloop一直活着
            //这是一个创建常驻服务线程的好方法
            NSRunloop *runloop = [NSRunLoop currentRunLoop];
            [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
            [runloop run];
        }
    }
    
    + (NSThread *)networkRequestThread {
        static NSThread *_networkReuqestThread = nil;
        static dispatch_once_t oncePredicate;
        dispatch_once(&oncePredicate, ^{
            _networkRequestThread = 
            [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
            [_networkRequestThread start];
        });
        return _networkRequestThread;
    }
    

    一个TableView延迟加载图片的新思路

    以前是怎么解决的?
    通过UITableView的代理方法,判断如果处于滑动状态就不去设置cell上的图片,如果没有处于滑动状态就取设置cell上的图片。

    而现在通过Runloop就有一个十分简便的方法。

    //在cell里面把设置图片的事情在NSDefaultRunloopMode里面去做。
    //当主线程的tableview不再滑动的时候就会去设置图片
    UIImage *dowloadImage = ...;
    [self.iconImageView performSelector:@selector(setImage:) withObject:dowloadImage afterDelay:0 inModes:@[NSDefaultRunloopMode]];
    

    这样去设置图片就简便了很多,不用再去判断tableview的代理方法。代码也会很清爽。



  • 相关阅读:
    ES6新特性
    ng-bind与ng-medol 区别
    验证输入两次密码是否一致angularjs
    最全的node.js安装步骤
    JAVA基础
    localStorage 个人使用总结
    mac中怎么安装python3
    macbook配置homebrew以及安装python3
    python之函数进阶
    mysql数据库入门
  • 原文地址:https://www.cnblogs.com/tangyuanby2/p/8028444.html
Copyright © 2011-2022 走看看