Runloop 和线程的 happy time

796 阅读8分钟

基本信息

我们知道,如果测试的时候使用 macOS 创建一个命令行项目,那么它执行完main 函数,程序也就结束了。但是在iOS测试项目中,执行完main函数以后,app 还会运行着,当用户触发点击事件等,app 也能响应。这是因为在 iOS 项目中,默认为主线程开启了 Runloop,Runloop 延长了线程的生命周期,也就能保证app一直运行,它能够让线程有事件的时候就去处理事件,没有的时候就去休眠。

Runloop 对象

Runloop 实际使用的时候有两套API

  • 基于 C 语言的 CFRunLoopRef;

  • OC 层面的 NSRunLoop (它是对 CFRunLoopRef 的封装);

由于 CFRunLoopRef 是开源的,所以有兴趣的可以看看 CFRunLoopRef 原代码

如何获取

获取当前线程的Runloop:

  • C 语言 API: CFRunLoopGetCurrent()

  • OC API: [NSRunLoop currentRunLoop]

获取主线程的Runloop:

  • C 语言 API: CFRunLoopGetMain()

  • OC API: [NSRunLoop mainRunLoop]

与线程的关系

  • 每条线程都有对应的唯一的一个 Runloop 对象;

  • Runloop 保存在一个全局的字典中,以线程为key,Runloop 对象为value;

这个在 Runloop 的源代码里面其实可以看到(目前查看的版本是 CF-1153.18)。

以获取当前线程的 Runloop 为例:

  • 从上面的源代码我们也可以看出,线程的 Runloop 对象默认是不开启的,只有主动去获取的时候才会创建;

  • 主线程默认开启 Runloop,主要是为了是延长 App 生命周期;

  • Runloop 会在线程结束的时候销毁。

Runloop 相关类

在 CoreFoundation 中有5个 Runloop 相关的类:

  • CFRunLoopRef : Runloop 对象

  • CFRunLoopModeRef : Runloop 内部运行的具体的Mode模式

  • CFRunLoopSourceRef : Mode 模式里的 source 事件(包含 source0, source1)

  • CFRunLoopTimerRef : Mode 模式包含的 timer 事件

  • CFRunLoopObserverRef : Mode 模式包含的对 observer 监听事件

CFRunLoopRef

typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort;			// used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;                // Runloop 对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;      // Runloop 里面的通用模式
    CFMutableSetRef _commonModeItems;  // Runloop 里面的通用模式item (可为 source / timer / observer)
    CFRunLoopModeRef _currentMode;     // Runloop 里面当前使用的模式
    CFMutableSetRef _modes;            // Runloop 里面的所有模式
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;    // 包含的 sources0 Set
    CFMutableSetRef _sources1;    // 包含的 sources1 Set
    CFMutableArrayRef _observers; // 包含的 observers Array
    CFMutableArrayRef _timers;    // 包含的 timers Array
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

其它三个的内部定义:

typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;

它们都是运行模式下对应的 item,能够在实际使用中处理不同的事件。

大概的关系图为:

一些要点

  • Runloop 对象的在运行过程中,一次只能指定一个 Mode 模式运行,如果要切换到其它Mode,需要先退出当前 Mode,再进入其它 Mode;

  • Mode 里面包含了相关的 sources0, sources1, observers, timers;

  • 如果 Runloop 的当前运行Mode模式里面 sources0/sources1/observers/timers 都为空,那么Runloop会直接退出;

  • 开发中常用的 Mode 模式包含 默认模式:NSDefaultRunLoopMode, 滚动模式:UITrackingRunLoopMode (其它一些系统级别的 Mode 基本用不上);

  • NSDefaultRunLoopMode:App 的默认模式,主线程默认就是这个运行模式;

  • UITrackingRunLoopMode:界面滚动相关, 比如 ScrollView 滚动切换到的模式。

运行逻辑

Runloop 运行时的状态

/* Run Loop Observer Activities */
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),            // 即将退出当前 Mode 的 Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

一个监听运行状态的例子

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建 Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                                       kCFRunLoopAllActivities,
                                                                       true,
                                                                       0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"kCFRunLoopEntry");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"kCFRunLoopBeforeSources");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"kCFRunLoopBeforeTimers");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"kCFRunLoopBeforeWaiting");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"kCFRunLoopAfterWaiting");
                break;
            case kCFRunLoopExit:
                NSLog(@"kCFRunLoopExit");
                break;
            default:
                break;
        }
    });
    
    // 添加到当前 Runloop
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    // C 语言的创建使用 create 或者 copy 的释放一下
    CFRelease(observer); 
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 测试点击事件
}

启动的时候可以看到大量的类似输出:

2021-01-18 22:34:35.854865+0800 Runloop[99050:2117959] kCFRunLoopBeforeTimers
2021-01-18 22:34:35.855008+0800 Runloop[99050:2117959] kCFRunLoopBeforeSources
2021-01-18 22:34:35.855454+0800 Runloop[99050:2117959] kCFRunLoopBeforeTimers
...
...
2021-01-18 22:34:37.333628+0800 Runloop[99050:2117959] kCFRunLoopAfterWaiting
2021-01-18 22:34:37.334021+0800 Runloop[99050:2117959] kCFRunLoopBeforeTimers
2021-01-18 22:34:37.334144+0800 Runloop[99050:2117959] kCFRunLoopBeforeSources
2021-01-18 22:34:37.334265+0800 Runloop[99050:2117959] kCFRunLoopBeforeWaiting

除去之前的输出,点击屏幕,查看输出:

2021-01-18 22:35:51.520240+0800 Runloop[99050:2117959] kCFRunLoopAfterWaiting
2021-01-18 22:35:51.520415+0800 Runloop[99050:2117959] kCFRunLoopBeforeTimers
2021-01-18 22:35:51.520574+0800 Runloop[99050:2117959] kCFRunLoopBeforeSources
2021-01-18 22:35:51.522203+0800 Runloop[99050:2117959] kCFRunLoopBeforeTimers
...
...
2021-01-18 22:35:51.524794+0800 Runloop[99050:2117959] kCFRunLoopBeforeTimers
2021-01-18 22:35:51.524902+0800 Runloop[99050:2117959] kCFRunLoopBeforeSources
2021-01-18 22:35:51.525001+0800 Runloop[99050:2117959] kCFRunLoopBeforeWaiting

之所以看不到 kCFRunLoopEntry 的输出,是因为App一启动的时候就已经执行了,这个自定义的 observer 是后面才加入主线程的Runloop中的。

查看Mode间的切换

添加一个TextView来滚动触发主线程 Mode间的切换:

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];

    // 创建 Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,
                                                                       kCFRunLoopAllActivities,
                                                                       true,
                                                                       0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry: {
                // 查看某个 Mode 的开发
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopEntry - %@", mode);
                CFRelease(mode);
                break;
            }
            case kCFRunLoopExit: {
                // 查看某个 Mode 的结束
                CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
                NSLog(@"kCFRunLoopExit - %@", mode);
                CFRelease(mode);
                break;
            }
            default:
                // 其它状态忽略
                break;
        }
    });

    // 添加到当前 Runloop, 记得标记为 kCFRunLoopCommonModes (UITrackingRunLoopMode 和 NSDefaultRunLoopMode 都标记为了 kCFRunLoopCommonModes)
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);

    CFRelease(observer); // C 语言的创建使用 create 或者 copy 的释放一下
}

需要注意的是 kCFRunLoopCommonModes 不是一个模式,它只是一个标记而已

滚动 textView 到停止滚动,输出如下:

2021-01-18 22:45:45.977951+0800 Runloop[99500:2129065] kCFRunLoopExit - kCFRunLoopDefaultMode
2021-01-18 22:45:45.978217+0800 Runloop[99500:2129065] kCFRunLoopEntry - UITrackingRunLoopMode
2021-01-18 22:45:46.984343+0800 Runloop[99500:2129065] kCFRunLoopExit - UITrackingRunLoopMode
2021-01-18 22:45:46.984766+0800 Runloop[99500:2129065] kCFRunLoopEntry - kCFRunLoopDefaultMode

可以看到Runloop 里面 Mode 之间的切换是先退出当前 Mode 的 Loop, 再进入其它 Mode 的 Loop。

Source0,Source1,Timers,Observers 对应的事件类型

Source0 :

  • 触摸事件处理
  • performSelector:onThread:

Source1:

  • 基于Port的线程间通信
  • 系统事件捕捉

Timers:

  • NSTimer
  • performSelector:withObject:afterDelay:

Observers:

  • 用于监听RunLoop的状态
  • UI刷新(BeforeWaiting)
  • Autorelease pool(BeforeWaiting)

(在实际的使用中,可以打断点,执行 bt 指令查看函数调用栈,然后查看具体的调用类型)

下面是实际使用中能够用到的添加上述事件到 Runloop 的接口:

    // 添加当前runloop的监听
    [[NSRunLoop currentRunLoop] addObserver: forKeyPath: options: context:];
    
    // 添加一个source1事件到当前runloop,事件使用的时候往往使用这个来进行线程的保活
    [[NSRunLoop currentRunLoop] addPort: forMode:];
    
    // 添加一个timer事件到runloop
    [[NSRunLoop currentRunLoop] addTimer: forMode:];

Runloop 在运行的时候,主要就是在运行状态的不断切换以及如果有上述事件到来以后的处理, 它的一个大致的的运行逻辑如下所示:

实际应用

NSTimer

测试代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    static int count = 0;
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:true block:^(NSTimer * _Nonnull timer) {
        NSLog(@"count = %d", ++count);
    }];
    
    //  需要添加到标记为通用模式的 NSRunLoopCommonModes
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

在例子中如果只是添加到 UITrackingRunLoopMode 或者 NSDefaultRunLoopMode 模式, 那么 count 的输出就只能在一种模式运行了,要想滚动情况和非滚动情况都执行 count 输出,需要添加到标记为通用模式的NSRunLoopCommonModes。

控制线程生命周期

下面演示一个会崩溃的例子:

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSThread *thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"task 1");
    }];
    // 开始执行
    [thread start];
    
    // 报错: target thread exited while waiting for the perform
    [self performSelector:@selector(task2) onThread:thread withObject:nil waitUntilDone:true];
}

- (void)task2 {
    NSLog(@"task 2");
}

@end

为什么会这样呢?这是因为 thread 对象执行完 task1 以后,由于这是一条非主线程,所以任务一执行,它就会退出了,相当于结束了生命周期。要想延长线程的生命周期,使其能够结束当前任务还不退出,那就需要主动把它添加到相应的Runloop中。

下面是一个封添线程到Runloop的例子:

// ----------- interface -----------------

#import <Foundation/Foundation.h>

typedef void (^ZZThreadTask)(void);

NS_ASSUME_NONNULL_BEGIN

@interface ZZThread : NSObject

// 在当前子线程执行一个任务
- (void)executeTask:(ZZThreadTask)task;

// 结束线程
- (void)stop;

@end

NS_ASSUME_NONNULL_END

// ----------- implementation -----------------

#import "ZZThread.h"

@interface ZZThread()

@property (strong, nonatomic) NSThread *realThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;

@end

@implementation ZZThread

- (instancetype)init {
    if (self = [super init]) {
        self.stopped = NO;
        
        __weak typeof(self) weakSelf = self;
        
        self.realThread = [[NSThread alloc] initWithBlock:^{
            // 添加一个 source1,否则mode里面没有item会直接退出 Runloop
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            while (weakSelf && !weakSelf.isStopped) {
                // 自己控制运行模式,不要使用 [[NSRunLoop currentRunLoop] run],否则会结束不了什么周期
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
        
        [self.realThread start];
    }
    return self;
}

- (void)executeTask:(ZZThreadTask)task {
    if (!self.realThread || !task) return;
    
    [self performSelector:@selector(__executeTask:) onThread:self.realThread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!self.realThread) return;
    
    // waitUntilDone:YES 很重要,需要等待线程真的结束
    [self performSelector:@selector(__stop) onThread:self.realThread withObject:nil waitUntilDone:YES];
}

- (void)dealloc {
    NSLog(@"%s", __func__);
    [self stop];
}

#pragma mark - private

- (void)__stop {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.realThread = nil;
}

- (void)__executeTask:(ZZThreadTask)task {
    task();
}

@end

测试代码:

@interface ViewController ()

@property (nonatomic, strong) ZZThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[ZZThread alloc] init];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self.thread executeTask:^{        NSLog(@"task - %@", [NSThread currentThread]);
    }];
}

- (void)dealloc {
    // 也可以主动在不想要 thread 以后,自己调用 [thread stop]
    [self.thread stop];
}

@end

可以看到如果不断的点击屏幕测试,输出如下:

2021-01-19 21:53:13.619390+0800 ThreadLive[13831:2488695] task - <RealThread: 0x6000026693c0>{number = 12, name = (null)}
2021-01-19 21:54:16.896227+0800 ThreadLive[13831:2488695] task - <RealThread: 0x6000026693c0>{number = 12, name = (null)}
2021-01-19 21:54:17.204199+0800 ThreadLive[13831:2488695] task - <RealThread: 0x6000026693c0>{number = 12, name = (null)}

可以看到目前子线程能够持续的处理事件,并且也能够在合适的时候结束自己的生命周期。