关于RunLoop

2023-11-20  本文已影响0人  蔚尼

今天学习runloop,开始啦~~

一.RunLoop的概念

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

1.什么是runloop?(面试题

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

2.关于用户态、内核态

比如在内核态会有一些指令、终端、关机开机的操作。假如每个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

如上图:

2. CFRunLoopSourceRef

CFRunLoopSourceRef 是事件产生的地方.

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

  1. NSDefaultRunLoopMode
    默认的mode,通常主线程是在这个mode下运行;
  1. UITrackingRunLoopMode
    界面跟踪的mode,用于scrollview追踪滑动,保证界面滑动时不受其他mode的影响;
  1. UIInitalizationRunLoopMode
    在app启动时第一个mode,启动完成后就不再使用
  1. GSEventReceiveRunLoopMode:
    接收系统内部的mode,通常用不到
  1. NSRunLoopCommonModes
    这是一个占位符mode,不是一个实际存在的mode
    是同步Source/Timer/Oberver到多个Mode的一种技术方案
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,怎么唤醒?(面试)
Source1事件、NSTimer事件、外部手动唤醒

五.RunLoop的底层实现

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/

上一篇下一篇

猜你喜欢

热点阅读