美文网首页
关于RunLoop

关于RunLoop

作者: 蔚尼 | 来源:发表于2023-11-20 20:35 被阅读0次

    今天学习runloop,开始啦~~

    一.RunLoop的概念

    一般来讲一个线程只能执行一次任务,执行完后就会退出。Runloop就可以让线程能随时处理事件但不退出。

    1.什么是runloop?(面试题

    • runloop是通过内部维护的事件循环来对事件、消息进行管理的一个对象
    • 没有消息处理时,用户态--》内核态。休眠以避免资源占用。
    • 有消息时,内核态--》用户态。立刻被唤醒。

    2.关于用户态、内核态

    • 应用程序一般是运行在用户态上面的,开发中绝大多数的api都是在用户层面的。
    • 需要使用到操作系统、底层内部的指令,就发生了系统调用。有的系统调用会触发空间的切换。在内核态上。
    • 之所以区分用户态和内核态,实际上是对计算机的一些资源调度、资源管理进行统一。这样就可以合理的进行资源调度,避免异常。

    比如在内核态会有一些指令、终端、关机开机的操作。假如每个app都可以进行开机关机的操作,这个场景导致的效果是无法想象的。

    3.main()函数为什么不会退出?(面试题

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    

    如上,main函数里面调用了UIApplicationMain,UIApplicationMain里面会启动线程的runloop。
    main函数会一直处于“接受消息->处理->等待” 的循环中,直到这个循环结束。达到runloop可以做到有事情的时候做事情,没有事情的时候从用户态转换为内核态,避免资源浪费。

    main()函数状态切换

    4.Runloop对象

    关于runloop,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。

    1. CFRunLoopRef是在 CoreFoundation 框架内(这个框架是开源的http://opensource.apple.com/tarballs/CF/)的,它提供了纯 C 函数的 API。
    2. NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API。

    CFRunLoopRef

    获得主线程的runloop

        CFRunLoopRef mainRef = CFRunLoopGetMain();
    

    获得当前的runloop对象

       CFRunLoopRef currentRef = CFRunLoopGetCurrent();
    

    NSRunLoop

    获得主线程的runloop

        NSRunLoop * mainRunloop = [NSRunLoop mainRunLoop];
    

    获得当前的runloop对象

        NSRunLoop * currentRunloop = [NSRunLoop currentRunLoop];
    

    runloop的OC和C的API互相转换:

        NSRunLoop * mainRunloop = [NSRunLoop mainRunLoop];
        NSLog(@"%p-----%p",mainRunloop.getCFRunLoop,mainRunloop);
    

    二. RunLoop与线程的关系

    1. 每个线程都有唯一的一个与之对应的Runloop对象。(其关系是保存在一个全局的 Dictionary 里。)

    2. 主线程的Runloop已经创建好了,子线程的Runloop需要主动创建

    3. 苹果不允许直接创建 RunLoop,可以通过CFRunLoopGetMain() 和 CFRunLoopGetCurrent()第一次获取的时候创建,在线程结束时销毁

    4. 只能在一个线程的内部获取其 RunLoop(主线程除外)

    如下

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
         //获取子线程的runloop
        [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
    }
    -(void)run{
        
        //获得子线程对应的runloop|currentRunloop
        //该方法本身是懒加载的,如果是第一次调用那么会创建当前线程对应的runloop并保存,以后则直接获取。
        //创建
        NSRunLoop * newThreadRunloop = [NSRunLoop currentRunLoop];
    
        //开启runloop(该runloop开启后马上退出了,因为runloop需要一个mode才能运行)
        [newThreadRunloop run];
        
    }
    

    三.RunLoop相关类/数据结构/对外的接口

    CoreFoundation 里面关于 RunLoop 有5个类:

    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef

    1.CFRunLoopRef

    RunLoop.png

    如上图:

    • RunLoop和Mode关系:一对多
      Mode和 Source/Timer/Observer关系:一对多

    • 每次runloop启动的时候,只能指定一个mode。即currentMode。

    • 如果要切换mode,只能退出loop,再重新指定一个mode进入。
      (这么做是为了分开不同的mode里面的source/timer/observer),让其互不影响。

    2. CFRunLoopSourceRef

    CFRunLoopSourceRef 是事件产生的地方.

    • source0
      需要手动唤醒线程。先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
    • source1
      具备唤醒线程的能力。包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。

    3. CFRunLoopTimerRef

    CFRunLoopTimerRef 是基于时间的触发器。和 NSTimer 是toll-free bridged(免费桥接) 的, 可以混用。

    4. CFRunLoopObserverRef

    CFRunLoopObserverRef 是观察者。
    每个 Observer 都包含了一个回调(函数指针),当 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
    };
    

    如下:为当前runloop的状态添加监听

    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        //01 创建观察者
        /*
         第一个参数:allocator,用于分配存储空间的;用默认的CFAllocatorGetDefault
         第二个参数:要监听的状态
         第三个参数:是否要持续监听
         第四个参数:和优先级相关的;传0
         第五个参数:当runloop状态改变的时候会调用这个block块
         */
        CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
           
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"runloop启动");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"runloop即将处理timer事件");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"runloop即将处理sourece事件");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"runloop即将进入休眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"runloop休眠结束");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"runloop退出");
                    break;
                default:
                    break;
            }
            
        });
        
        //02 监听runloop的状态
        /*
         第一个参数:runloop对象
         第二个参数:监听者
         第三个参数:runloop在哪种运行模式下的状态
         kCFRunLoopDefaultMode == NSDefaultRunLoopMode
         kCFRunLoopCommonModes == NSRunLoopCommonModes
         
         */
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
        
      
    }
    

    三. RunLoop的Mode

    • 系统默认注册了5个mode
    1. NSDefaultRunLoopMode
      默认的mode,通常主线程是在这个mode下运行;
    1. UITrackingRunLoopMode
      界面跟踪的mode,用于scrollview追踪滑动,保证界面滑动时不受其他mode的影响;
    1. UIInitalizationRunLoopMode
      在app启动时第一个mode,启动完成后就不再使用
    1. GSEventReceiveRunLoopMode:
      接收系统内部的mode,通常用不到
    1. NSRunLoopCommonModes
      这是一个占位符mode,不是一个实际存在的mode
      是同步Source/Timer/Oberver到多个Mode的一种技术方案
    • CFRunLoopMode 和 CFRunLoop 的结构大致如下:
    struct __CFRunLoopMode {
        CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
        CFMutableSetRef _sources0;    // Set
        CFMutableSetRef _sources1;    // Set
        CFMutableArrayRef _observers; // Array
        CFMutableArrayRef _timers;    // Array
        ...
    };
     
    struct __CFRunLoop {
        CFMutableSetRef _commonModes;     // Set<NSString *>
        CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
        CFRunLoopModeRef _currentMode;    // 当前的 RunloopMode
        CFMutableSetRef _modes;           // Set< CFRunLoopMode *>
        ...
    };
    

    上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

    上面的common modes有两种:kCFRunLoopDefaultMode和UITrackingRunLoopMode

         common modes = <CFBasicHash 0x604000445430 [0x10589f960]>{type = mutable set, count = 2,
         entries =>
         0 : <CFString 0x106c0f060 [0x10589f960]>{contents = "UITrackingRunLoopMode"}
         2 : <CFString 0x105875790 [0x10589f960]>{contents = "kCFRunLoopDefaultMode"}
         }
    
         */
    

    RunLoop需要选择一个Mode才可以运行起来。
    1)Runloop下面有很多个mode,运行的时候需要选择其中一个mode。
    2)然后判断这个mode的item是否为空。Mode里面有一些Source、timer、Observer。判断有Source或者Observer,或者两者都有,说明这个mode不为空。则可以运行这个runloop了。

    1. RunLoop的运行模式和NSTimer

    1.1 NSTimer创建定时器方法1--需要指定mode

    下面的mode,如果指定为NSDefaultRunLoopMode,则默认情况下定时器可以运行。
    但是界面滑动的时候定时器也要可以操作,就需要指定为TrackingRunLoopMode。
    想要默认和滑动模式下都可以运行定时器,那么就需要指定模式为NSRunLoopCommonModes。

       //01创建定时器对象
        NSTimer * timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
        //02 把定时器对象添加到runloop中,并指定运行模式为默认。
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
     
    

    1.2 NSTimer创建定时器方法2--mode为kCFRunLoopDefaultMode

    scheduledTimerWithTimeInterval这个方法不需要手动启动runloop,会自动设置运行模式为kCFRunLoopDefaultMode。

    如果需要让定时器在UITrackingRunLoopMode也运行,添加即可。

         //该方法会自动将创建定时器对象添加到当前的runloop中
        //运行模式为默认
       NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
        
        //把当前的定时器也可以在滚动下运行,添加缺少的tracking模式即可
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    

    1.3 子线程添加timer--需要手动开启runloop

    子线程里面没有runloop,所以使用NSTimer在子线程添加定时器是没有用的。需要开启runloop。

    -(void)timer3{
        
        [self performSelectorInBackground:@selector(addTimerForthread) withObject:nil];
    }
    -(void)addTimerForthread{
    
        //这个方法里面指定模式为默认模式
        NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run1) userInfo:nil repeats:YES];
    
        //子线程直接添加timer不会执行,需要手动添加runloop
        //注意,要在指定运行 模式之后再开启runloop,runloop才能执行
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"thread:%@",[NSThread currentThread]);
    }
    

    2. RunLoop的运行模式和GCD中的定时器--不会受到影响

    上面记录到,NSTimer中的定时器工作会受到runloop运行模式的影响,而GCD中的定时器不会受到影响

    @interface ViewController ()
    
    @property(nonatomic,strong)dispatch_source_t timer;
    
    @end
    -(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
        
        //NSTimer中的定时器工作会受到runloop运行模式的影响
        //GCD中的定时器不会受到影响
        
        //01 创建定时器对象
        //队列(GCD)决定代码块在哪个线程中执行(主队列--主线程|非主队列--子线程中)
    //    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
        dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0,dispatch_get_global_queue(0, 0));
    
        //02 设置定时器(开始时间|调用时间|误差)
        dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);//2:2秒执行一次;0:0秒误差
        //03 事件回调
        dispatch_source_set_event_handler(timer, ^{
            NSLog(@"GCD Thread:%@",[NSThread currentThread]);
        });
        //04 起点定时器
        dispatch_resume(timer);
        
        _timer  = timer;
    }
    
    

    四.RunLoop的实现机制

    RunLoop的实现机制
    • 即将进入runloop;(通知observer,对应kCFRunLoopEntry)
    • 将要处理Timer/Source0事件(通知observer,对应kCFRunLoopBeforeTimers、kCFRunLoopBeforeSources)
    • 处理Timer、Source0事件
    • 如果有Source1事件要处理,处理Source1事件
    • 没有Source1事件,线程将要休眠(通知observer,对应kCFRunLoopBeforeWaiting)
    • 线程进入休眠
    • 线程收到消息,被唤醒(通知observer,对应kCFRunLoopAfterWaiting),去处理收到的消息
    • 即将推出RunLoop的时候(通知observer,对应kCFRunLoopExit)

    问:处于一个休眠的runloop,怎么唤醒?(面试)
    Source1事件、NSTimer事件、外部手动唤醒

    五.RunLoop的底层实现

    • RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。
    • mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。
    • 当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到核心态;核心态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:
    RunLoop的底层实现

    -----未完待续-----

    六.苹果用 RunLoop 实现的功能

    概念---
    与多线程---
    相关类/数据结构/对外的接口---
    与NSTimer---
    CFRunLoopObserverRef---

    内部逻辑/事件循环机制---
    底层实现---

    source0如何手动唤醒线程

    问:处于一个休眠的runloop,怎么唤醒?(面试)
    Source1事件、NSTimer事件、外部手动唤醒???

    看看放哪里

    苹果用runloop实现的功能
    应用:
    [self.imageVie performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3 inModes:@[NSRunLoopCommonModes]];

    AFNet
    Async

    常驻线程

    面试问题汇总:

    https://blog.ibireme.com/2015/05/18/runloop/

    相关文章

      网友评论

          本文标题:关于RunLoop

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