MJiOS底层笔记--Runloop

1,033 阅读4分钟

本文属笔记性质,主要针对自己理解不太透彻的地方进行记录。

推荐系统直接学习小码哥iOS底层原理班---MJ老师的课确实不错,强推一波。


什么是runloop

iOS系统在程序运行过程中循环做一些事情

应用

  1. 定时器(Timer)、PerformSelector
  2. GCD Async Main Queue
  3. 事件响应、手势识别、界面刷新
  4. 网络请求
  5. AutoreleasePool
  6. 其他

runloop的作用

  1. 保持程序的持续运行
int main(int argc, char * argv[]) {
   @autoreleasepool {
       return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
       
       return 0; 则程序会直接退出。
   }
}
  1. 处理App中的各种事件(比如触摸事件、定时器事件等)
  2. 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
  3. 其他

基本概念

  1. 每条线程都有唯一的一个与之对应的RunLoop对象
  2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  3. 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  4. RunLoop会在线程结束时销毁
  5. 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
  6. 任何位置都可以获取主线程RunLoop
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

CFRunLoopModeRef

基本知识

  1. CFRunLoopModeRef代表RunLoop的运行模式

  2. 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer

  3. RunLoop启动时只能选择其中一个Mode,作为currentMode

  4. 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入

不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响

  1. 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出

常见Mode

  1. kCFRunLoopDefaultMode(NSDefaultRunLoopMode):

App的默认Mode,通常主线程是在这个Mode下运行

  1. UITrackingRunLoopMode:

界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

  1. UITrackingCommonMode

会进行 _commonModes标记,只要_modes符合 _commonModes都会执行。

_commonModeItems代表着能在Common模式下工作的单元,比如timer。

Source0

  1. 触摸事件处理
  2. performSelector:onThread:

Source1

  1. 基于Port的线程间通信
  2. 系统事件捕捉 捕捉之后包装成Source0去处理

Timers

  1. NSTimer
  2. performSelector:withObject:afterDelay:

底层也是NSTimer

Observers

  1. 用于监听RunLoop的状态
  2. UI自动刷新(BeforeWaiting)

runloop休眠之前刷新

  1. Autorelease pool(BeforeWaiting)

清理内部需要释放的对象

  1. 自主监听runloop状态切换
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}


- (void)viewDidLoad {
    // 创建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 释放
    CFRelease(observer);
}

RunLoop的运行逻辑

  1. GCD只有在回到主队列时,才会依赖runloop
- (void)viewDidLoad {
    [super viewDidLoad];
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        // 处理一些子线程的逻辑
        
        // 回到主线程去刷新UI界面
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"11111111111");
        });
    });
}
  1. runloop的休眠是内核层级的休眠,而不是while循环这种死循环


控制线程生命周期(线程保活)

  1. 向自线程runloop中添加一个port,赋予其存在的意义。

线程不会死,也可以执行test中的代码。但是run方法里的end也不会打印,因为runloop已经休眠了。由于任务永远不会执行完毕,所以哪怕是VC释放了,线程也不会释放。


- (void)viewDidLoad {
    [super viewDidLoad];

    self.thread = [[MJThread alloc] initWithTarget:self selector:@sel(run) obj:nil];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

- (void)run {
    NSLog(@"%@----begin----", [NSThread currentThread]);
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"%@----end----", [NSThread currentThread]);
}

  1. run

NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)。

所以我们哪怕我们手动将runloop停止,也是做不到的。

  1. runMode

我们可以使用另外一个函数runMode来进行一次runloop循环

但有一个问题是,只要该runloop被唤醒一次,就会退出

[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//[NSDate distantFuture] 会设置一个用不超时的时间
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
  1. while+runMode

通过加标记的方式,让runloop每次处理完时间,都重新runMode一次

self.thread = [[MJThread alloc] initWithBlock:^{
    // 往RunLoop里面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    while (!weakSelf.isStoped) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
}];

解决NSTimer在滑动时停止工作的问题

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    // NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
    // NSRunLoopCommonModes并不是一个真的模式,它只是一个标记
    // timer能在_commonModes数组中存放的模式下工作
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

}

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:需要依赖定时器与runloop,所以在自线程中并不会起作用。

- (void)test
{
    NSLog(@"2");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"1");
        // 这句代码的本质是往Runloop中添加定时器
        [self performSelector:@selector(test) withObject:nil afterDelay:.0];
        NSLog(@"3");

    });
}



//打印
// 1
// 3