iOS底层-Runloop(一)

先说说我是咋理解runloop的吧?然后在专业的讲讲

本人理解:

  1. 从C语言的角度来看,整个app就是调用了一个main函数,只是这个main函数一直不返回值.
  2. main函数内部:
    1. 常识: 执行任务需要开启一个线程,因为任务必须在线程下执行
    2. 一般的main函数执行流程:开启一个线程->执行main函数->添加许多任务(即写了很多代码)->任务完成(代码执行完毕)->返回main函数值程序结束->线程关闭
  3. runloop啥作用?
    1. Runloop就是一个死循环,把这个死循环放在上面开启的线程中,那么线程就永远不会结束.因此main永远不返回.但是这个死循环比较牛逼,下面讲讲他的牛逼之处:
    2. 一般死循环特点:
      • 程序一旦进入死循环,一直循环执行,永不停歇,并且执行速度非常快
      • 只执行这段代码,其他任何代码都不会执行,也就是说,只专注在这跑圈,不管外界发生任何事
  4. Runloop特点:
    1. 程序一旦进入runloop,当外界没事的时候,他就慢慢跑圈,(至于多慢我就不知道了),甚至休眠,一旦有事,他就快速跑到该事件,去执行该事件,执行完毕后再接着休眠

RunLoop的传说

  1. 什么是runloop?
    1. 字面理解:运行循环/跑圈
    2. 在程序运行过程中循环做一些事情
  2. 基本作用
    1. 保持程序的持续运行
      • 打开app它一直不死,就是因为runloop
    2. 处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)
    3. 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
      • 能让我们的主线程,有事情就做事情,没事情就休息,比如点击事件:点击前主线程在休息,当点击事件处理完毕后,让主线程再次休息…
  3. 如果没有runloop

     int main(int argc, char * argv[]) {
         NSLog(@"execute main function");
         return 0;
     }
    
    1. 第3行后程序就结束了
    2. 如果有了runloop大致运行代码逻辑如下:

       int main(int argc, const char * argv[]) {
           @autoreleasepool {
               int retval = 0;
               do{
                   //睡眠中等待消息
                   int message = sleep_and_wait();
                   //处理消息
                   retval = process_message(message);
                          
               }while(0== retval);
           }
           return 0;
       }
      
  4. 由于main函数里面( UIApplicationMain())启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
  5. main函数中的RunLoop

     int main(int argc, char * argv[]) {
         @autoreleasepool {
             return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
         }
     }
    
    1. 代码的UIApplicationMain函数内部就启动了一个RunLoop
    2. 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
    3. 这个默认启动的RunLoop是跟主线程相关联的

既然runloop那么牛逼,那么我们就来认识他一下:

RunLoop

RunLoop对象

  1. 相关框架
    1. iOS中有2套API来访问和使用RunLoop:
      1. Foundation(OC):NSRunLoop
      2. Core Foundation(C):CFRunLoopRef
        1. 该框架有个特点:所有的类都已CF开头,以Ref(reference:引用)结尾
    2. NSRunLoop和CFRunLoopRef都代表着RunLoop对象
    3. NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)
    4. CFRunLoopRef是开源的
      1. https://opensource.apple.com/tarballs/CF/
  2. RunLoop与线程关系
    1. 上面说到,runloop通常是用来卡住线程的,那么他们必然有关系啦
      • 每条线程都有唯一的一个与之对应的RunLoop对象
      • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
      • 程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
      • RunLoop在第一次获取时创建,在线程结束时销毁
      • 主线程的RunLoop已经自动创建好了,并且主动启动了,子线程的RunLoop需要主动创建,然后手动启动
  3. 获得RunLoop对象
    1. Foundation

       [NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象     
       [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象  
      
    2. Core Foundation

       CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象      
       CFRunLoopGetMain(); // 获得主线程的RunLoop对象 
      
  4. 举例使用

     - (void)viewDidLoad {
         [super viewDidLoad];
         //当前线程就是主线程,打印地址一样
         NSLog(@"%p--%p",[NSRunLoop mainRunLoop],[NSRunLoop currentRunLoop]);
         //创建一个线程
         NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
         [thread start]; 
     }
     -(void)run{
         /*
          [NSRunLoop currentRunLoop]:这就是在子线程中创建了一个runloop,不是通过alloc/init,而且该创建是懒加载的
          */
         NSLog(@"%p",[NSRunLoop currentRunLoop]);
     }
    

RunLoop的相关类

  1. Core Foundation中关于RunLoop的5个类
    1. CFRunLoopRef
    2. CFRunLoopModeRef
    3. CFRunLoopSourceRef
    4. CFRunLoopTimerRef
    5. CFRunLoopObserverRef
  2. 查看源码
    1. CFRunLoopRef的本质

       typedef struct __CFRunLoop * CFRunLoopRef;
       struct __CFRunLoop {
           pthread_t _pthread;
           CFMutableSetRef _commonModes;
           CFMutableSetRef _commonModeItems;
           CFRunLoopModeRef _currentMode; //当前模式
           CFMutableSetRef _modes; //集合内部装的是CFRunLoopModeRef类型
           ...
       };
      
      1. 一个Runloop内部有一堆mode,但是仅仅有一个是当前mode
    2. CFRunLoopModeRef的本质

       typedef struct __CFRunLoopMode *CFRunLoopModeRef;
       struct __CFRunLoopMode {
           CFStringRef _name;
           CFMutableSetRef _sources0; //集合内部装的是CFRunLoopSourceRef类型
           CFMutableSetRef _sources1; //集合内部装的是CFRunLoopSourceRef类型
           CFMutableArrayRef _observers; //集合内部装的是CFRunLoopObserverRef类型
      
           CFMutableArrayRef _timers;  //集合内部装的是CFRunLoopTimerRef类型
           ...
       };
      
    3. 这几个类的关系图如下:

      图二

      1. 分析:
        1. RunLoop就像一个汽车,要跑起来,那么就要装一些必备的东西
        2. RunLoop里面有很多Mode对象->每个Mode对象里面又有三种集合分别存放:Source/Observer/Timer对象->有了这些Mode以及子内容runloop才能一直跑圈/休息/有事件触发
      2. 注意:runloop中的Mode中必须有source或者timer才能一直运行,否则跑一圈就停了

CFRunLoopModeRef

  1. CFRunLoopModeRef代表RunLoop的运行模式
  2. 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
  3. 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode [NSRunLoop currentRunLoop] runMode: beforeDate:
  4. 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
    1. 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
  5. 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

  6. 系统默认注册了5个Mode:(前两种最重要)

    1. kCFRunLoopDefaultMode: App的默认Mode,通常主线程是在这个Mode下运行
    2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

      1. 分析:当我们滚动图片时就会切换到这个模式,跟踪你的滚动,一旦停止滚动就会切换到Default模式
      2. 举例:当我们滚动的时候,其他事件没反应,比如定时器,当滚动的时候定时器没反应,因为它在Default模式中
    3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
    4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
    5. kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

CFRunLoopTimerRef

  1. CFRunLoopTimerRef是基于时间的触发器,基本上说的就是NSTimer
  2. 它受runloop的Mode影响
  3. GCD的定时器不受runLoop的Mode影响
    1. GCD定时器实现不一样
  4. 代码举例

     - (void)viewDidLoad {
         [super viewDidLoad];
         //向sb中拖入一个textView
     //情况一:没有滚动textView时,1秒打印一次,一旦滚动textView,定时器无用了,停止滚动,定时器有起作用了
     //    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
         //上面一段代码做了很多事情,等价代码如下:
         //timer添加到defaultMode里面去,mode再添加到runloop中去,runloop在启动时候再指定这个mode->拿出timer来用
         NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
         //也就说只能在Defaultmode下好使
     //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
            
         //情况二:如果想要一滚动timer就有用,一停止滚动timer就失效,那么模式就用UITrackingRunLoopMode
     //     [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
         //情况三:滚动不影响定时器
         //定时器只会跑在标记为CommonModes的模式下(或者用GCD定时器)
           [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
         //通过打印runloop,我们发现kCFRunLoopDefaultMode/UITrackingRunLoopMode这两种模式都被标记为了commonmodes标签了,因此就可以做到了
         NSLog(@"%@",[NSRunLoop currentRunLoop]);  
     }
     - (void)test {
         NSLog(@"----");
     }
    

CFRunLoopSourceRef

  1. 系统定的我们用不着
  2. CFRunLoopSourceRef是事件源(输入源)
  3. 我们的一些触发事件,都是由source触发的
    1. 以前的分法
      1. Port-Based Sources
      2. Custom Input Sources
      3. Cocoa Perform Selector Sources(处理performselector调用)
    2. 根据函数调用栈分法
      1. Source0:非基于Port
      2. Source1:基于Port的,通过内核和其他线程通信,接收/分发系统事件

CFRunLoopObserverRef

  1. CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
  2. 可以监听的时间点有以下几个:

     /* Run Loop Observer Activities */
     typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
         kCFRunLoopEntry = (1UL << 0),//即将进入loop,左移动0->2的0次方= 1
         kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer …2
         kCFRunLoopBeforeSources = (1UL << 2),//即将处理source...4
         kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠 …32
         kCFRunLoopAfterWaiting = (1UL << 6),//刚刚从休眠中唤醒...64
         kCFRunLoopExit = (1UL << 7),//即将推出loop 128
         kCFRunLoopAllActivities = 0x0FFFFFFFU
     };
    
  3. 代码举例(自己监听runloop的状态)
    1. 可以监听触摸、滚动,runloop的状态改变
     // 创建observer
     CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
         switch (activity) {
             case kCFRunLoopEntry:
             {
                 CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                 NSLog(@"====runloop的当前模式:%@",mode);
                 CFRelease(mode);
             }
                 break;
             case kCFRunLoopExit:
             {
                 CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                 NSLog(@"====runloop的当前模式:%@",mode);
                 CFRelease(mode);
             }
                 break;
             default:
                 break;
         }
    //        NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
           
     });
     //添加观察者到当前线程的RunLoop的当前Mode中,监听runloop的状态
     CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
     // 释放Observer
     CFRelease(observer);
     /*
      //需要自己管理内存
      CF的内存管理(Core Foundation)
      1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
      * 比如CFRunLoopObserverCreate
      2.release函数:CFRelease(对象);
     */
    

RunLoop的逻辑流程图

  1. runloop一旦启动,在某种模式下运行时,没有消息时等待状态,一旦有消息就立刻进行处理,那么处理的是什么东西呢?
  2. 处理的就是当前模式下的source0、source1、observers、timers这些东西,那这些东西是什么呢?
    1. Source0
      1. 触摸事件处理
      2. performSelector:onThread:
    2. Source1
      1. 基于Port的线程间通信
      2. 系统事件捕捉
    3. Timers
      1. NSTimer
      2. performSelector:withObject:afterDelay:
    4. Observers
      1. 用于监听RunLoop的状态
      2. UI刷新(BeforeWaiting)
      3. Autorelease pool(BeforeWaiting)
  3. 举例使用
    1. 在控制器中实现触摸方法,然后打断点

       -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
           NSLog(@"=====%s",__func__);
       }
      
    2. 在控制台输入bt,打印所有的函数调用栈:

       * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
       * frame #0: 0x000000010ee02b3d RunloopTest`-[ViewController touchesBegan:withEvent:](self=0x00007f910750d9f0, _cmd="touchesBegan:withEvent:", touches=1 element, event=0x0000600000cdc7e0) at ViewController.m:22:5
       frame #1: 0x0000000112f9ba09 UIKitCore`forwardTouchMethod + 353
       frame #2: 0x0000000112f9b897 UIKitCore`-[UIResponder touchesBegan:withEvent:] + 49
       frame #3: 0x0000000112faac48 UIKitCore`-[UIWindow _sendTouchesForEvent:] + 1869
       frame #4: 0x0000000112fac5d2 UIKitCore`-[UIWindow sendEvent:] + 4079
       frame #5: 0x0000000112f8ad16 UIKitCore`-[UIApplication sendEvent:] + 356
       frame #6: 0x000000011305b293 UIKitCore`__dispatchPreprocessedEventFromEventQueue + 3232
       frame #7: 0x000000011305dbb9 UIKitCore`__handleEventQueueInternal + 5911
       //处理source0的C函数
       frame #8: 0x000000010f767be1 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
       //处理source0事件
       frame #9: 0x000000010f767463 CoreFoundation`__CFRunLoopDoSources0 + 243
       //创建主运行循环
       frame #10: 0x000000010f761b1f CoreFoundation`__CFRunLoopRun + 1231
       frame #11: 0x000000010f761302 CoreFoundation`CFRunLoopRunSpecific + 626
       frame #12: 0x00000001186f22fe GraphicsServices`GSEventRunModal + 65
       //UIApplicationMain函数
       frame #13: 0x0000000112f70ba2 UIKitCore`UIApplicationMain + 140
       //调用main函数
       frame #14: 0x000000010ee02bf0 RunloopTest`main(argc=1, argv=0x00007ffee0dfcf80) at main.m:14:16
       //dyld系统函数调用:开启线程start,加载framework,动态静态链接库,bundle,启动图片,Info.plist,pch等
       frame #15: 0x0000000111ae1541 libdyld.dylib`start + 1
      
      1. 从函数调用栈可以看出,runloop处理的是source0,因此触摸事件是属于source0
  4. 源码分析:
    1. 下载runloop源码,然后找到CFRunLoop.c
    2. 从上面的函数调用栈可以看出,UIApplicationMain调用的第一个函数是CFRunLoopRunSpecific,在源码中定位到改函数,就是Runloop的入口
    3. 将源码简化如下:

       SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {
          
           //通知observer,进入loop
           if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
           //具体要做的事情
           result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
                  
           //通知observer,退出Runloop
           if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
                  
           return result;
       }
              
              
       /* rl, rlm are locked on entrance and exit */
       static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
                  
           int32_t retVal = 0;
           do {
               //通知observer:即将处理timers
               if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
                      
               //通知observer:即将处理sources
               if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
                      
               //处理blocks
               __CFRunLoopDoBlocks(rl, rlm);
                      
               //处理sources0
               Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
               //处理blocks
               if (sourceHandledThisLoop) {
                   __CFRunLoopDoBlocks(rl, rlm);
               }
              
               //判断有无source1:port属于sorce1
               if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                   //有source1,跳转到handle_msg,处理source1
                   goto handle_msg;
               }
                      
               //通知observer:即将休眠
               if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
               //休眠
               __CFRunLoopSetSleeping(rl);
                      
               //等待别的消息来唤醒
               __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
                      
               //唤醒
               // user callouts now OK again
               __CFRunLoopUnsetSleeping(rl);
               //通知observer:结束休眠
               if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
                      
           handle_msg:
               //被timer唤醒
               if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
                   CFRUNLOOP_WAKEUP_FOR_TIMER();
                   //处理timers
                   __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
               }
               //被GCD唤醒
               else if (livePort == dispatchPort) {
                   //处理GCD
                   __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                          
               } else { //source1
                   //处理source1
                   __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
               }
               //处理blocks
               __CFRunLoopDoBlocks(rl, rlm);
                      
               //设置返回值retVal,是否需要继续循环
               if (sourceHandledThisLoop && stopAfterHandle) {
                   retVal = kCFRunLoopRunHandledSource;
               } else if (timeout_context->termTSR < mach_absolute_time()) {
                   retVal = kCFRunLoopRunTimedOut;
               } else if (__CFRunLoopIsStopped(rl)) {
                   __CFRunLoopUnsetStopped(rl);
                   retVal = kCFRunLoopRunStopped;
               } else if (rlm->_stopped) {
                   rlm->_stopped = false;
                   retVal = kCFRunLoopRunStopped;
               } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                   retVal = kCFRunLoopRunFinished;
               }
           } while (0 == retVal);
                  
           return retVal;
       }
      
  5. 简化成流程图如下:

    图3

    1. 其中的处理blocks是什么呢? 如下:

       //runloop运行我们添加一些block的
       CFRunLoopPerformBlock(CFRunLoopGetMain(), kCFRunLoopDefaultMode, ^{
                  
       });
      
    2. 注意:一般情况下GCD是不依赖于Runloop的,GCD有自己的逻辑处理,跟Runloop无关,但是有一种情况,GCD需要交给Runloop来处理

      1. 线程间通信:子线程回到主线程刷新UI

          dispatch_async(dispatch_get_global_queue(0, 0), ^{
                   
             //子线程处理事情。。。
                        
                        
             //回到主线程刷新UI
             dispatch_async(dispatch_get_main_queue(), ^{
                            
             });
         });
        
Table of Contents