Runloop

一个线程一次只能执行一个任务,执行完成后线程就会退出。RunLoop 机制能让线程随时处理事件但并不退出。这里说的随时是指:程序需要运行时就保持程序的持续运行,不需要的时候就进入休眠状态。

RunLoop 的结构

和 RunLoop 相关的主要涉及五个类:

  • CFRunLoopRef:RunLoop对象
  • CFRunLoopModeRef:运行模式
  • CFRunLoopSourceRef:输入源/事件源
  • CFRunLoopTimerRef:定时源
  • CFRunLoopObserverRef:观察者

一个RunLoop 对象中可以包含多个 Mode,每个 Mode 又包含多个个 Source、Timer、Observer。

RunLoop 中的 Mode

关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。

总共是有五种Mode:

  • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行
  • UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响
  • UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,实际是kCFRunLoopDefaultModeUITrackingRunLoopMode的结合。

Runloop运行流程

基本概念:

  1. source0, 触摸事件,performSelector
  2. source1, 系统事件,基于 Port,
  3. timers, 定时器
  4. observers,监听器,用于监听runloop 状态,执行回调

Runloop 具体来说主要执行逻辑是这样的:

  1. 通知 Obersvers,RunLoop 已经启动
  2. 通知 Obersvers,即将处理 Timers
  3. 通知 Obersvers,即将处理 Sources
  4. 处理 Blocks (此处 blocks 为 CFRunLoopPerformBlock 调用的 block)
  5. 处理 Sources0(可能会再次处理 blocks)
  6. 如果存在 Source1 ,则跳转到第 8 步直接处理。
  7. 通知 Obersvers,开始休眠(开始等待消息唤醒)。
  8. 如果唤醒,通知 Obersvers,结束休眠,根据唤醒的消息类型可能会处理
    • GCDTimers
    • Timer
    • Sources
    • 处理GCD Async To Main Queue
  9. 处理 Blocks
  10. 根据前面执行结果判断接下来如何操作,可能会:
    • 回到 2
    • 到下一步11,结束 runloop
  11. 通知 Obersvers,RunLoop结束。

常驻线程

借助RunLoop可以实现线程后台常驻的功能:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(runOne) object:nil];
    [self.thread start];
}
- (void) runOne{
    NSLog(@"----任务1-----");
    // 下面两句代码可以实现线程保活
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"未开启RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(runTwo) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) runTwo{
    NSLog(@"----任务2------");
}

AutoreleasePool

应用程序一旦启动,主线程 RunLoop 里注册了两个 Observer。

一个 Observer 监听即将进入Loop事件,回调内会调用 objc_autoreleasePoolPush() 创建自动释放池,并保证创建释放池发生在其他所有回调之前。

_另外一个 Observer 监视了两个事件(RunLoop即将进入休眠和即将退出 RunLoop 事件) ,前者会调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;后者会调用 _objc_autoreleasePoolPop() 来释放自动释放池,并保证释放自动释放池事件发生在其它回调之后。

实现原理

  • 一个线程的自动释放池是一个指针堆栈
  • 每一个指针或者指向被释放的对象,或者是自动释放池的POOL_BOUNDARY,POOL_BOUNDARY 是自动释放池的边界。
  • 一个池子的 token 是指向池子 POOL_BOUNDARY 的指针。当池子被出栈的时候,每一个高于标准的对象都会被释放掉。
  • 堆栈被分成一个页面的双向链表。页面按照需要添加或者删除。
  • 本地线程存放着指向当前页的指针,在这里存放着新创建的自动释放的对象。

自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的,当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中,调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息.

使用场景

通常情况下,我们是不需要手动添加 autoreleasepool 的,使用线程自动维护的 autoreleasepool 就好了。仅在下列三种情况下需要我们手动添加 autoreleasepool:

  1. 如果你编写的程序不是基于 UI 框架的,比如说命令行工具;
  2. 如果你编写的循环中创建了大量的临时对象;
  3. 如果你创建了一个辅助线程。

例子:

int lagerNum = 1024 * 1024 * 2 ;
    for(int i = 0 ; i < lagerNum; i++)
    {
        @autoreleasepool{
            NSString *str = [NSString stringWithFormat:@"Hello"];
            str = [str uppercaseString];
            str = [NSString stringWithFormat:@"%@ - %@   %d",str, @"World!", i];
            // NSLog(@"%@", str);
        }
    }

在我的钢琴app:klaver中也加入了常驻线程来处理琴谱解析的问题,由于对于这个app琴谱解析是常用的功能,为了避免每次切换下一曲时重新创建线程的开销而使用常驻线程。

关于 performSelector

先记录一道题目:

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    NSLog(@"1");
    [self performSelector:@selector(_sel1) ];
    [self performSelector:@selector(_sel2) withObject: nil afterDelay:0];
    [self performSelectorOnMainThread: @selector(_sel3) withObject: nil waitUntilDone: false];
    NSLog(@"2");
});

-(void) _sel1 {
    NSLog(@"call _sel1 %@", [NSThread currentThread]);
}
-(void) _sel2 {
    NSLog(@"call _sel2 %@", [NSThread currentThread]);
}
-(void) _sel3 {
    sleep(6);
    NSLog(@"call _sel3 %@", [NSThread currentThread]);
}

运行结果如下:

2021-07-28 22:48:05.556735+0800 ForLife[5958:2029108] --------start
2021-07-28 22:48:05.557100+0800 ForLife[5958:2029108] call _sel1 <NSThread: 0x280a69b00>{number = 4, name = (null)}
2021-07-28 22:48:05.557384+0800 ForLife[5958:2029108] --------end
2021-07-28 22:48:11.576002+0800 ForLife[5958:2029084] call _sel3 <NSThread: 0x280a2c6c0>{number = 1, name = main}

问题:

  1. 为什么没打印 call _sel2,因为当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
  2. performSelector 内部机制,只是一个单纯的消息发送,就是对 objc_msgSend的封装,和时间没有一点关系,所以不需要添加到子线程的Runloop中也能执行
  3. performSelectorOnMainThread,该方法主要用来用主线程来修改页面UI的状态,当调用方主线程的时候,waitUntilDone 参数无效