RunLoop

作者: 越天高 | 来源:发表于2020-10-30 09:15 被阅读0次

    01基本认识

    什么是RunLoop
    顾名思义
    运行循环
    在程序运行过程中循环做一些事情
    当我一启动IOS程序的时候,他内部就会创建一个RunLoop对象,循环做一些事情

    • 应用范畴
      定时器(Timer)、PerformSelector
      GCD Async Main Queue
      事件响应、手势识别、界面刷新
      网络请求
      AutoreleasePool

    如果没有RunLoop程序执行完main函数就会退出,
    为了保证程序一直在运行之后,他就引入了RunLoop,当我们执行main函数调用UIApplicationMain的时候,就会创建RunLoop对象。
    他内部大概是这个样的

     int retVal = 0;
        do {
            //睡眠中等待消息
            int message = sleep_and_wait();
            //处理消息
            retVal = process_message(message)
        } while (retVal == 0);
    

    当执行完main之后,程序不会马上退出,而是保持运行状态
    RunLoop的基本作用
    保持程序的持续运行
    处理App中的各种事件(比如触摸事件、定时器事件等)
    节省CPU资源,提高程序性能:该做事时做事,该休息时休息

    02获取RunLoop对象

    IOS没有RunLoop会马上退出,执行main返回0,程序立即退出。
    RunLoop有两个,一个是OC的一个是C语言的

        NSRunLoop *runloop = [NSRunLoop currentRunLoop];
        CFRunLoopRef runloopR = CFRunLoopGetCurrent();
    

    iOS中有2套API来访问和使用RunLoop
    Foundation:NSRunLoop
    Core Foundation:CFRunLoopRef
    NSRunLoop和CFRunLoopRef都代表着RunLoop对象
    NSRunLoop是基于CFRunLoopRef的一层OC包装
    CFRunLoopRef是开源的
    https://opensource.apple.com/tarballs/CF/

    Runloop与线程的关系
    1.每条线程都有唯一的一个与之对应的RunLoop对象
    2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
    3. 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
    4.RunLoop会在线程结束时销毁
    5. 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

        NSLog(@"%p--%p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
        0x600001e6cc00--0x600001e6cc00
        NSLog(@"%p--%p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
     0x600000660000--0x600000660000
    

    打印结果不一样是因为NSRunLoop是对CFRunLoop的包装,他里面存放着CFRunLoop,一个线程对应唯一一个runloop
    通过查看源码,我们可以看出当我们执行CFRunLoopGetCurrent-> CFRunLoopGet0->里面有个CFGetDictionaryValue取RunLoop,根据一个thread的key,如果一开始runloop对象不存在,他就创建一个runloop,然后把它添加到字典之中

    03CFRunLoopModeRef

    如果要寻做一些事,仅仅有这个RunLoop对象是不行,他还要以来其他的对象

    Core Foundation中关于RunLoop的5个类
    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef
    

    CFRunLoop的结构


    CFRunLoop

    可以看出,他和线程是一一对应的关系,他有一个集合存放着他的mode,这里面只有一个mode是currentMode,
    mode的结构


    mode

    source0和source1里面装的CFRunLoopSourceRef这种对象
    observers里面装的是CFRunLoopObserverRef这种对象

    内存结构
    • CFRunLoopModeRef代表RunLoop的运行模式
    • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
    • RunLoop启动时只能选择其中一个Mode,作为currentMode
    • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
    • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
    • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
    • 常见的2种Mode
      kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
      UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

    04CFRunLoopModeRef的成员

    通过控制台bt可以打印函数调用栈

     Source0
        触摸事件处理
        performSelector:onThread:
        
        Source1
        基于Port的线程间通信
        系统事件捕捉,比如点击屏幕产生的事件就是他捕捉到的,然后再交给source0处理
        
        Timers
        NSTimer
        performSelector:withObject:afterDelay:
        
        Observers
        用于监听RunLoop的状态
        UI刷新(BeforeWaiting),当前听器间听到runloop要睡觉了,他就会进行一次UI页面刷新
    比如设置颜色,当你值完代码,他先保留住,一旦监听到睡觉,就是刷新
        Autorelease pool(BeforeWaiting)//他在什么时候释放对象,一旦监听到要睡觉了,就清理一下自动释放的对象
    

    05CFRunLoopObserverRef

    Observer

    CFRunLoopObserverRef

    监听runloop的状态Observer,没有OC的API我们只能通过CF的函数形式添加Observer
    手动创建一个Observer监听runloop的状态

    void observerCall(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
    {
        typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
            kCFRunLoopEntry = (1UL << 0),
            kCFRunLoopBeforeTimers = (1UL << 1),
            kCFRunLoopBeforeSources = (1UL << 2),
            kCFRunLoopBeforeWaiting = (1UL << 5),
            kCFRunLoopAfterWaiting = (1UL << 6),
            kCFRunLoopExit = (1UL << 7),
            kCFRunLoopAllActivities = 0x0FFFFFFFU
        };
        //监听状态
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry");
                break;
                
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers");
                    
                break;
    
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources");
                break;
    
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting");
                break;
    
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit");
                break;
            default:
                break;
        }
    }
    - (void)viewDidLoad
    {
        [super viewDidLoad];
        
        //创建一个observer
        CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observerCall, NULL);
        //添加一个observer
        //kCFRunLoopCommonModes
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
        CFRelease(observer);
        
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        NSLog(@"------");
    }
    

    监听模式的改变,可以拖一个scrollView拖动一下改变的runloop的mode

      //监听状态
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry---%@",CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit---%@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));
                break;
            default:
                break;
        }
    //log
     kCFRunLoopExit---kCFRunLoopDefaultMode
    2020-10-26 21:56:57.104392+0800 RuntimeIsa[1573:89315] kCFRunLoopEntry---UITrackingRunLoopMode
    2020-10-26 21:56:57.315033+0800 RuntimeIsa[1573:89315] kCFRunLoopExit---UITrackingRunLoopMode
    2020-10-26 21:56:57.315199+0800 RuntimeIsa[1573:89315] kCFRunLoopEntry---kCFRunLoopDefaultMode
    

    06答疑

    循环做把mode里面source拿出来处理,
    source里面的东西是不固定的

    07执行流程图

    在IOS我们表面上能看到的Runloop就是applicationMain
    要想看到他具体做了什么事情,就要分析源码,但是源码太过于抽象,纯c。可以先看下总结图片。他大概就是不断的切换mode,然后处理不同模式喜爱的source0 ,source1,timer等
    handleport就是source1 可能是别的线程发过来的信息


    Runloop工作流程
    运行逻辑

    08-源码分析

    通过打断点,在控制台输入bt指令,来打印省略的步骤可以查看到他调用了一个CFRunLoopSpecific的函数->CFRunloopRun这个函数里面就是他在做的处理,就是一个do-while循环

    __CFRunLoopRef  rl,  CFRunLoopModeRef rum
    int32_t retVal = 0;
    
    do
    {
        //通知Obervers,即将处理Timers
    __CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeTimers);
        //通知Obervers,即将处理Sources
    __CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeSources);  
    //处理Blocks
    __CFRunLoppDoBlocks(rl,rlm)
    
    //处理source0
    if (__CFRunloopDoSource0(rl,rlm,stopAfterHandle))
    {
    //处理block;
    }
    
    }while(0 == retVal);
    
    //判断有无source1,如果有就跳到,handle_msg
    
    //通知observer即将进入休眠
    __CFRunLoopDoObservers(rl,rlm,kCFRunLoopBeforeSleeping);
    __CFRunLoopSetSleeping(rl);
    
    //等待别的消息来唤醒当前线程
    __CFRunLoopServiceMachPort
    
    handle_msg:
    if(被time唤醒)
    {
    //处理timers
    }else if(被GCD唤醒)
    {
    //处理GCD
    }else
    {//被source1唤醒,
    //处理source1
    }
    //处理block
    
    //设置返回值
    
    如果返回值retVal还是 == 0;
    那么就会回到开头从新循环
    
    

    09调用细节

    我们平时所做的UI界面刷新,timer定时器的监听,自动释放池都是疣runloop来做控制的,它主要做这几件事件,1 doObservers, 2 doBlocks 3. doSource03. doSource01 4doTimers5,处理gcd 6 休眠,这些函数里面还会调用一个__CFRUNLOOP_IS_CALLING_TO开头的函数

    • GCD有自己的逻辑,他很多东西不依赖RunLoop,他有一种情况会交给runloop处理
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
            //处理子线程的逻辑
            
            dispatch_async(dispatch_get_main_queue(), ^{
               //回到住线程刷新界面
            });
            
        });
    

    01、通知Observers:进入Loop
    02、通知Observers:即将处理Timers
    03、通知Observers:即将处理Sources
    04、处理Blocks
    05、处理Source0(可能会再次处理Blocks)
    06、如果存在Source1,就跳转到第8步
    07、通知Observers:开始休眠(等待消息唤醒)
    08、通知Observers:结束休眠(被某个消息唤醒)
    01> 处理Timer
    02> 处理GCD Async To Main Queue
    03> 处理Source1
    09、处理Blocks
    10、根据前面的执行结果,决定如何操作
    01> 回到第02步
    02> 退出Loop
    11、通知Observers:退出Loop

    10-休眠的细节

    开始睡觉意味着当前线程不做事情了,相当于阻塞了,这个阻塞和我们所写的while(1)(并没有休眠,线程还是在做事情)这种不一样,他是线程休息了什么代码也不执行了
    通过调用mach_msg来调用内核层面的API实现线程的休眠的目的
    当我们在用户态执行了mach_msg函数之后,他会进入内核态,调用内核态的mach_msg的实现,来达到休眠的目的。没消息就让线程休眠,有消息就唤醒线程。回到用户态处理消息,通过状态切换真正达到了让线程休息


    状态切换

    runloop是怎么响应用户的操作的?
    首先它是由source1来把用户事件捕捉,点击。source1会把这个事件,包装成事件队列,放到事件队列里面去。事件队列又是在source0里面处理的

    说说runloopd的几种状态
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities =
    runloop的mode作用是什么?
    常见的2种Mode
    kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
    UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    mode可以将不同的source0/source1/t/o隔离开来。他们之间相互隔离不受影响

    11NSTimer失效

    runloop实际开发中的应用,
    控制线程生命周期(线程保活),有时候我们不希望子线程运行完就销毁,
    解决NSTimer在滑动时停止工作的问题
    监控应用卡顿
    性能优化

    默认下的NSTimer我们一拖拽UI控件,他就会停止运行,因为我们在拖拽的时候,runloop切换了mode,定时器是在默认模式下工作的

    //用这个方法创建的定时器是加到默认模式下的
        [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer)
        {
            NSLog(@"%i", ++count);
        }];
    

    相关文章

      网友评论

          本文标题:RunLoop

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