Loading

iOS 底层原理|RunLoop 详解

一、RunLoop 简介

RunLoop 是与线程关联的基本基础结构的一部分。一个 RunLoop 是一个事件处理循环,你用它来安排工作,并协调接收传入的事件。RunLoop 的目的是在有工作要做时让线程忙,而在没有工作时让线程进入睡眠状态。直到用户关闭程序为止。

  • RunLoop,顾名思义就是运行循环,实际上就是一个 do..while..,下面是简单描述 RunLoop 逻辑的伪代码:
int main(int argc,char * argv[]){
    bool running = 0;
    do {
        // 休眠等待任务
        // 有任务,执行任务,做完继续休眠等待
    }while(0 == running);
    return 0;
}
  • RunLoop 基本功能特点就是保持程序的持续运行;程序中事件处理(比如触摸事件、定时器事件等);节省 CPU 资源,提高程序的性能等等。

二、RunLoop 探究

1. RunLoop 对象

iOS 中有两套 API 来使用 RunLoop。一套是 FoundationNSRunLoop;另一套就是 Core FoundationCFRunLoopRefNSRunLoopCFRunLoopRef 都代表着 RunLoop 对象,但 NSRunLoop 是基于 CFRunLoopRef 的一层 OC 封装。

CF 开源源码下载地址

1.1 RunLoop 与线程关系

  • 每条线程都会有唯一的一个与之对应的 RunLoop 对象;
  • RunLoop 保存在一个全局的字典里,线程为 Key,RunLoop 为 Value;
  • 线程刚创建时候并没有 RunLoop 对象,RunLoop 会在第一次获取它时候创建。
    就是说当你获取当前线程的 RunLoop 时候,发现还没有创建 RunLoop,他就会自动创建一个当前线程的 RunLoop);
  • RunLoop 会在线程结束时候销毁;
  • 主线程的 RunLoop 在 main 中自动获取(创建),子线程默认不开启 RunLoop。

1.2 获取 RunLoop 对象的方式

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

2. RunLoop 相关的类

Core Foundation 中关于 RunLoop 的5个类:

类名 介绍
CFRunLoopRef RunLoop 对象
CFRunLoopModeRef RunLoop 运行模式
CFRunLoopSourceRef input sources
CFRunLoopTimerRef Timer sources
CFRunLoopObserverRef 监听 RunLoop 状态改变

他们之间的大致关系就是:创建一个 RunLoop(CFRunLoopRef) 之后,有默认的运行模式 mode(CFRunLoopModeRef),也可以为RunLoop 指定运行模式,RunLoop 启动必须得有运行模式,而且在运行模式中必须还有 Timer sources(CFRunLoopTimerRef) 或是 input sources(CFRunLoopSourceRef) 事件其中之一,否则 RunLoop 就会退出。监听(CFRunLoopObserverRef) RunLoop 的 mode 变化就需要

1.1 CFRunLoopRef

CFRunLoopRef 底层结构体:

typedef struct __CFRunLoop * CFRunLoopRef;
struct __CFRunLoop {
    pthread_t _pthread; // 线程
    CFMutableSetRef _commonModes; // 常用模式
    CFMutableSetRef _commonModeItems; 
    CFRunLoopModeRef _currentMode; // 当前模式
    CFMutableSetRef _modes; // CFRunLoopModeRef 类型的 mode 的集合(无序)
    ...
};

1.2 CFRunLoopModeRef

CFRunLoopModeRef 底层结构体:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
    CFStringRef _name; // 模式名称
    CFMutableSetRef _sources0; // CFRunLoopSourceRef 输入源0 集合
    CFMutableSetRef _sources1; // CFRunLoopSourceRef 输入源1 集合
    CFMutableArrayRef _observers; // 监听事件
    CFMutableArrayRef _timers; // CFRunLoopTimerRef 计时器源 集合
    ...
}
  • _sources0:触摸事件处理;
  • _sources1:基于 Port 的线程间通信,系统事件捕捉;
  • _timers: 本质就是 NSTimer
  • _observers:用于监听 RunLoop 的状态,比如监听到将要休眠,休眠之前刷新 UI、休眠之前 Autorelease pool 清理释放对象等。

CFRunLoopModeRef 代表 RunLoop 的运行模式, 一个 RunLoop 包含若干个 mode,每个 mode 又包含若干个 _sources0_sources1_observers_timers,这么多 mode 中,启动 RunLoop 时只能选择其中一个 Mode 作为 currentMode。如果要切换 mode,必须退出当前 RunLoop,然后重新选择 mode 后再启动。
这里要注意的是如果 RunLoop 中 _sources0_sources1_observers_timers 都为空那么 RunLoop 就会停止运行。

CFRunLoopModeRef 的所有 mode 类型:

mode Name Description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默认模式是用于大多数操作的模式。大多数时候,您应该使用此模式来启动运行循环并配置输入源
Connection NSConnectionReplyMode (Cocoa) Cocoa 使用这种模式结合 NSConnection 对象监控回复,这个 mode 用的少
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa 使用这种模式来识别事件用于模态面板
Event tracking NSEventTrackingRunLoopMode (Cocoa) 在界面滚动时 Cocoa 使用这个模式来限制传入的事件循环和其他类型的用户界面跟踪回路。
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 这是一个可配置的常用模式。将一个输入源与这种模式还将它与每个模式的集团。Cocoa 应用程序,这个集合包括违约,模态和事件跟踪默认模式。包括最初只是默认模式的 Core Foundation。你可以添加自定义模式使用 CFRunLoopAddCommonMode 函数集。

CFRunLoopModeRef 常用两个 mode:

  • kCFRunLoopDefaultModeNSDefaultRunLoopMode): App 的默认 Mode,通常主线程是在这个 Mode 下运行。
  • UITrackingRunLoopMode 界面跟踪 mode,用于 UIScrollView 追踪触摸滑动,再界面滚动时候切换到该模式下,保证界面滑动时不受其他 mode 影响。

1.3 CFRunLoopObserverRef

CFRunLoopObserverRef 底层结构体主要成员变量:

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    CFOptionFlags _activities; // RunLoop 监听的活动状态
    ...
};

/* RunLoop 监听的活动状态 */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),  // 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),         // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 包含上面所有状态 
};

3. RunLoop 运行逻辑

我们想研究底层逻辑,我们就需要知道 RunLoop 是在源码中那个函数开始的。我们可以在 ViewController 下面使用屏幕点击事件监听的方法:

然后在命令窗口敲 bt 指令,我们就能得到底层调用的信息,发现 RunLoop 的入口函数是叫 CFRunLoopRunSpecific

我们从 CoreFoundation 源码中找到 CFRunLoopRunSpecific 方法,由于代码过于抽象,我们从源码剥离出来关键代码:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    ...
    // 通知所有的 Observer,程序要进入 RunLoop 了
	if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    // 具体要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
	// 通知所有的 Observers,程序要退出 RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    ...
    return result;
}

我们从上面代码可以知道,RunLoop 具体处理事情的方法是 __CFRunLoopRun,我们从源码剥离出来关键代码(剥离出来关键代码我觉得是一个加深理解的方式,剥离完之后在回头看 RunLoop 运行逻辑图,会非常清晰。大家可以自己下载源码,去尝试一遍):

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    int32_t retVal = 0;
    do {
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        // 通知所有的 Observer,即将处理 Timers
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知所有的 Observer,即将处理 Sources
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 处理 Blocks
	    __CFRunLoopDoBlocks(rl, rlm);
        // 处理 Sources0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            // 处理 Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            msg = (mach_msg_header_t *)msg_buffer;
            // 判断有无 Sources1 前面我们也讲了 Sources1 跟端口相关
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有 Sources1,就跳转
                goto handle_msg;
            }
        }
        didDispatchPortLastTime = false;
        // 通知所有的 Observer,即将休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 开始休眠
        __CFRunLoopSetSleeping(rl);
        do {
             // 等待别的消息唤醒别的线程,如果没唤醒会一直堵塞在这里,如果唤醒就往下走
            // CPU 不会给休眠线程分配资源,阻塞在这里,就不会继续往下走了
            // __CFRunLoopServiceMachPort 里的 mach_msg 是内核层面的 API,没有消息就让线程休眠,有消息就唤醒线程,这里的休眠是完全不做事情,一行汇编都不会执行
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        // 停止休眠
        __CFRunLoopUnsetSleeping(rl);
        // 通知所有的 Observer,结束休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
        handle_msg:
            // 被 Timer 唤醒
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                CFRUNLOOP_WAKEUP_FOR_TIMER();
                // 处理 Timer
                if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                    __CFArmNextTimerInMode(rlm, rl);
                }
            }
            // 同上
            else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
                CFRUNLOOP_WAKEUP_FOR_TIMER();
                if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
                    __CFArmNextTimerInMode(rlm, rl);
                }
            }
            // 被 GCD 唤醒
            else if (livePort == dispatchPort) {
                // 处理 GCD 相关的事情
                // GCD 有自己的处理逻辑,很多东西是不依赖 RunLoop,只有从子线程回到主线程会使用,比如子线程事情做完回到主线程刷新 UI。
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } else { // 被 Source1 唤醒
                // 处理 Source1 
                sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
                if (NULL != reply) {
                    (void)mach_msg(reply, MACH_SEND_MSG, reply->msgh_size, 0, MACH_PORT_NULL, 0, MACH_PORT_NULL);
                    CFAllocatorDeallocate(kCFAllocatorSystemDefault, reply);
                }
            }
            // 处理 Blocks
            __CFRunLoopDoBlocks(rl, rlm);
        // 设置返回值
        if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
            } else if (timeout_context->termTSR < mach_absolute_time()) {
                retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) { // 停止 RunLoop,退出 while
                __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {  // 停止 RunLoop,退出 while
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        }  
    } while (0 == retVal);
    return retVal;
}

三、RunLoop 的应用

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

在 iOS 开发中,有时会有一些花费时间较长的操作阻塞主线程,我们通常为了防止界面卡顿,将其放入子线程中运行。如果我们经常在一个子线程中执行任务,频繁的创建和销毁线程就会造成资源浪费,这时候就要用到 RunLoop 来使线程长时间存活了,我们称之为线程保活或者永驻线程。但是线程一般一次只能执行一个任务,执行完成后线程就会退出:
EasyThread.hEasyThread.m

// EasyThread.h
@interface EasyThread : NSThread
@end
// EasyThread.m
@implementation EasyThread

- (void)dealloc {
    NSLog(@"%s", __func__);
}

@end

ViewController.m

- (void)viewDidLoad {
    [super viewDidLoad];
    EasyThread *thread = [[EasyThread alloc] initWithTarget:self selector:@selector(doSomething) object:NULL];
    [thread start];
}

- (void)doSomething {
    NSLog(@"do");
}

打印日志:

2021-04-19 08:15:56.994035+0800 StudyIOSApp[65423:10339378] do
2021-04-19 08:15:56.994341+0800 StudyIOSApp[65423:10339378] -[EasyThread dealloc]

上面可以看出正常使用线程,当事情做完线程就会销毁线程,我们如何通过 RunLoop 进行线程保活?我们现在上面 doSomething 方法中添加 RunLoop 方式:

 - (void)doSomething {
    NSLog(@"%s", __func__);
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    // run是RunLoop的启动方法
    [[NSRunLoop currentRunLoop] run];
    // 用于查看方法是否执行完毕
    NSLog(@"ok");
}

项目添加上述代码,发现不会执行到 NSLog(@"ok");,RunLoop 一直会卡在上面部分。
但是为什么执行了 run 方法以后就会一直卡在这里呢,关于 run 方法的官方资料:

方法名 介绍
run 无条件
runUntilDate 设定时间限制
runMode:beforeDate: 在特定模式下

对于三种方法介绍总结如下:

  • 第一种:无条件进入是最简单的做法,但也最不推荐。这会使线程进入死循环,从而不利于控制 RunLoop,结束 RunLoop 的唯一方式是 kill 它;
  • 第二种:如果我们设置了超时时间,那么 RunLoop 会在处理完事件或超时后结束,此时我们可以选择重新开启 RunLoop。这种方式要优于前一种;
  • 第三种:这是相对来说最优秀的方式,相比于第二种启动方式,我们可以指定 RunLoop 以哪种模式运行。

这时候我们就可以设计一下:

// 线程保活
__weak typeof(self)weakSelf = self;
self.thread = [[NSThread alloc] initWithBlock:^{
    // 通过添加 Source1 保持 RunLoop 运行
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    // 通过 while 循环来线程保活
    while(weakSelf && weakSelf.isStoped) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
}];

如果想关闭销毁线程我们可以用 Core Foundation 的框架CFRunLoopStop(CFRunLoopGetCurrent()); 这个方法。了解了这些之后,我们可以做关于线程保活的封装:
QZHThread.h

@interface QZHThread : NSObject
// 运行
- (void)run;
// 线程执行任务
- (void)executeTask:(void (^)(void))task;
// 停止
-(void)stop;
@end

QZHThread.m

@interface QZHThread()

// 一条线程
@property(nonatomic ,strong) NSThread *thread;
// 是否停止 RunLoop
@property(nonatomic ,assign) BOOL    isStoped;

@end

@implementation QZHThread

#pragma mark - --------- 公开的方法  ---------
- (instancetype)init
{
    self = [super init];
    if (self) {
        self.isStoped = NO;
        // 线程保活
        __weak typeof(self)weakSelf = self;
        self.thread = [[NSThread alloc] initWithBlock:^{
            // 通过添加 Source1 保持 RunLoop 运行
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            // 通过 while 循环来线程保活
            while(weakSelf && !weakSelf.isStoped) {
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }];
    }
    return self;
}

// 开启一条线程
- (void)run {
    if (!self.thread) return;
    [self.thread start];
}

// 线程执行任务
- (void)executeTask:(void (^)(void))task {
    if (!task) return;
    if (!self.thread) return;
    // 在当前线程执行任务
    [self performSelector:@selector(__executeTask:) onThread:self.thread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!self.thread) return;
    [self performSelector:@selector(__stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:YES];
}

#pragma mark - --------- 私有方法  ---------
// 销毁析构方法
- (void)dealloc {
    NSLog(@"%s",__func__);
    [self stop];
}

// 停止RunLoop
- (void)__stopRunLoop {
    self.isStoped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
}

// 执行任务
- (void)__executeTask:(void (^)(void))task {
    task();
}

@end

ViewController.m 中使用:

@implementation ViewController

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

- (void)doSomething {
    NSLog(@"%@", [NSThread currentThread]);
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    __weak typeof(self) weakSelf = self;
    [self.thread executeTask:^{
        [weakSelf doSomething];
    }];
}

@end

点击屏幕打印结果:

2021-04-19 09:06:20.156588+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474ec0>{number = 7, name = (null)}
2021-04-19 09:06:22.003031+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474ec0>{number = 7, name = (null)}
2021-04-19 09:06:22.371167+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474ec0>{number = 7, name = (null)}
2021-04-19 09:06:22.726126+0800 StudyIOSApp[67303:10393065] <NSThread: 0x600000474ec0>{number = 7, name = (null)}

2. 处理 NSTimer 在滑动时失效的问题

一般情况下,正在使用的定时器默认会处于 NSDefaultRunLoopMode 模式;但是在滑动页面时候,正在使用的定时器会处于 UITrackingRunLoopMode 模式下,这样定时器就停止了,停止滚动后,定时器又继续运行。我们如何去解决呢?我们把 timer 添加到 NSRunLoopCommonModes 模式,你可以理解为默认的 NSRunLoopCommonModes 模式等效于 NSDefaultRunLoopModeUITrackingRunLoopMode 结合。

NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

3. 利用 RunLoop 监控应用卡顿

如果 RunLoop 的线程进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者线程唤醒后接收消息时间过长而无法进入下一步,就可以认为是线程受阻了。如果这个线程是主线程的话,表现出来的就是出现了卡顿。所以我们可以通过监听线程停留 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 两种状态时长,去判断是否卡顿。大体思路如下:

  • 创建一个 CFRunLoopObserverContext 观察者;
  • 将创建好的观察者 Observer 添加到主线程 RunLoop 的 NSRunLoopCommonModes 模式下观察;
  • 每当监听到 Observer 通知,使信号量的值 + 1;
  • 创建一个持续的子线程使用信号量专门用来监控主线程的 RunLoop 状态,设置信号量的等待时间;
  • 如过等待时间内子线程还没有被唤醒,则认为发生了卡顿。

有兴趣可以参考一下博客,去实现功能类封装 《ios 利用RunLoop的原理去监控卡顿》

4. 等等

参考文档

  1. 《Threading Programming Guide》苹果官方
posted @ 2021-04-19 15:01  QiuZH's  阅读(873)  评论(0编辑  收藏  举报