RunLoop相关

166 阅读14分钟

一、什么是RunLoop

RunLoop从字面来说是跑圈。下面是苹果官方文档关于RunLoop的一段说明。

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

这段话翻译如下:

RunLoop是与线程关联的基本基础结构的一部分。RunLoop是一个事件循环,用于安排工作和协调传入事件的接收。RunLoop的目的是在有工作的时候保持线程忙碌,没有工作的时候让线程进入睡眠状态。

简单的说:RunLoop是一种高级的循环机制,让程序持续运行并处理程序中的各种事件,让线程在有工作的时候忙碌起来,不需要的时候让线程休眠。

二、RunLoop与线程

RunLoop和线程有着密不可分的关系。通常情况下线程的作用是用来执行一个或多个特定的任务,在线程执行完成后就会退出不再执行任务,RunLoop这样的循环机制让线程能够不断地执行任务并且不退出。

Run loop management is not entirely automatic. You must still design your thread’s code to start the run loop at appropriate times and respond to incoming events. Both Cocoa and Core Foundation provide run loop objects to help you configure and manage your thread’s run loop. Your application does not need to create these objects explicitly; each thread, including the application’s main thread, has an associated run loop object. Only secondary threads need to run their run loop explicitly, however. The app frameworks automatically set up and run the run loop on the main thread as part of the application startup process.

这段话翻译如下:

RunLoop管理不是完全自动的。你仍需设计线程代码,以便在适当时机启动RunLoop并响应传入的事件。Cocoa和Core Foundation都提供了RunLoop对象,以帮助你配置和管理线程的Runloop。你的应用不需要显式的创建这些对象;每个线程,包括应用的主线程,都有一个关联的RunLoop对象。但是,只有辅助线程需要显式的运行它们的RunLoop。作为应用程序启动过程的一部分,应用架构自动配置并运行了主线程的RunLoop。

从上面这段话,我们获取到了以下几个信息:
1、RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象;
2、不需要手动创建RunLoop对象,通过系统提供的方法可以获取RunLoop对象;
3、主线程的RunLoop在APP启动时创建,子线程的RunLoop在第一次获取时创建;
RunLoop和线程的关系如下图所示:

4.jpg

从上图可以看出,RunLoop在线程中不断检测,通过input source和timer source接收事件,然后通知线程进行处理事件。
除了处理输入的资源,运行循环还会生成有关其行为的通知。注册RunLoop的观察者可以接收这些通知,并利用它们在线程上进行额外的处理。你可以使用Core Foundation在你的线程上安装RunLoop观察者。

三、RunLoop源码

下面是RunLoop结构体的源码:

struct __CFRunLoop {
    CFRuntimeBase _base;
    // 获取mode列表的锁
    pthread_mutex_t _lock;
    // 唤醒端口
    __CFPort _wakeUpPort;
    Boolean _unused;
    // 重置RunLoop数据
    volatile _per_run_data *_perRunData;
    // RunLoop所对应的线程
    pthread_t _pthread;
    uint32_t _winthread;
    // 标记为Common的mode的集合
    CFMutableSetRef _commonModes;
    // CommonMode的Item集合,包含了所有添加到RunLoop中的输入源和定时器
    CFMutableSetRef _commonModeItems;
    // 当前Mode,mode的结构体类型是CFRunLoopModeRef
    CFRunLoopModeRef _currentMode;
    // 存储的是RunLoop中的所有模式
    CFMutableSetRef _modes;
    // _block_item链表表头指针
    struct _block_item *_blocks_head;
    // _block_item链表表尾指针
    struct _block_item *_blocks_tail;
    // 运行时间点
    CFAbsoluteTime _runTime;
    // 睡眠时间点
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
}

从RunLoop的源码可以看出,一个RunLoop对象包含一个线程(_thread),若干个mode(_modes)和若干个commonMode(_commonModes)

四、CFRunLoopModeRef源码

下面是CFRunLoopModeRef源码

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    // 锁,必须在RunLoop加锁后,才能加锁
    pthread_mutex_t _lock;
    // mode的名称
    CFStringRef _name;
    // Mode是否停止
    Boolean _stopped;
    char _padding[3];
    // sources0事件
    CFMutableSetRef _sources0;
    // sources1事件
    CFMutableSetRef _sources1;
    // observers事件
    CFMutableArrayRef _observers;
    // timers事件
    CFMutableArrayRef _timers;
    // 字典, key是mach_port_t,value是CFRunLoopSourceRef
    CFMutableDictionaryRef _portToV1SourceMap;
    // 保存所有需要监听的port,比如_wakeUpPort,_timePort都保存在这个数组中
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    // GCD定时器
    dispatch_source_t _timerSource;
    // GCD队列
    dispatch_queue_t _queue;
    // 当定时器触发时设置为true
    Boolean _timerFired;
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    // MK_TIMER的Port
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline;
    uint64_t _timerHardDeadline;
}

从CFRunLoopMode的源码能够看出,一个CFRunLoopMode对象有唯一一个name,若干个sources0事件,若干个sources1事件,若干个timer事件,若干个observer事件和若干个port。
RunLoop总是在某个特定的CFRunLoopMode下运行,这个特定的Mode就是_currentMode。
根据CFRunLoop结构体源码可以知道一个RunLoop对象包含若干个mode,于是形成了如下所示的结构。

4.webp 苹果提供了5个CFRunLoopMode,分别是NSDefaultRunLoopMode、NSConnectionReplyMode、NSModalPanelRunLoopMode、NSEventTrackingRunLoopMode、NSRunLoopCommonModes。
在iOS中公开给开发者调用的只有NSDefaultRunLoopMode和NSRunLoopCommonModes。 1、NSDefaultRunLoopMode
默认模式是用于大多数操作的模式,大部分时候使用此模式来启动RunLoop并配置输入源。
2、NSConnectionReplyMode
Cocoa将此模式与NSConnection对象结合使用以检测响应,开发者几乎不需要自己使用次模式。 3、NSModalPanelRunLoopMode
Cocoa使用此模式处理模态面板的事件,模态面板是一种用户界面元素,显示在应用程序的其它部分之上,并阻止用户与应用程序其它部分进行交互,直到用户关闭该面板。在iOS中,可以使用UIAlertController来创建模态面板。 4、NSEventTrackingRunLoopMode(UITrackingRunLoopMode)
Cocoa使用此模式来处理用户界面的事件跟踪,如触摸事件等。这种模式通常在需要处理连续的用户输入时使用,确保这些输入能够被流畅地处理而不受其它事件的干扰。
5、NSRunLoopCommonModes(kCFRunLoopCommonModes)
NSRunLoopCommonModes是NSDefaultRunLoopMode和NSEventTrackingRunLoopMode(UITrackingRunLoopMode)集合,在这种模式下,RunLoop分别注册了NSDefaultRunLoopMode和NSEventTrackingRunLoopMode(UITrackingRunLoopMode)。也可以调用CFRunLoopAddCommonMode()方法将自定义Mode添加到NSRunLoopCommonModes(kCFRunLoopCommonModes)集合中。

五、CFRunLoopSourceRef-事件源

下面是CFRunLoopSource的源码

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    // 用于标记Signaled状态,Source0只有在被标记为Signaled状态,才会被处理。
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;
    CFMutableBagRef _runLoops;
    // 联合体
    union {
        CFRunLoopSourceContext version0;
        CFRunLoopSourceContext1 version1;
    } _context;
};

根据苹果官方的定义CFRunLoopSource是输入源的抽象,分为source0和source1两个版本。

source0是APP内部事件,只包含一个函数指针回调,并不能主动触发事件。使用时,开发者需要先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
source1包含一个mach_port和一个函数回调指针。source1是基于port的,通过读取某个port上内核消息队列上的消息来决定执行的任务,然后再分发到source0中处理的。source1只供系统使用,并不对开发者开放。

source0的消息不是其它进程或者内核直接发送的,不能主动唤醒RunLoop,一般是APP内部的事件,比如:hitTest:withEvent:的处理,performSelector的事件。
source1是基于mach_port的,来自系统内核或者其它进程或线程的事件,可以主动唤醒休眠中的RunLoop。mach_port是进程间相互发送消息的一种机制,source1基本是系统事件,比如:屏幕点击、网络数据的传输都会触发source1。
举个例子,一个APP在前台静止,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
手指触摸到硬件(屏幕),屏幕触摸的事件会被IOKit包装成IOHDEvent,通过mach_port传给SpringBoard.APP,再由SpringBoard.APP传递给正在活跃的APP,IOHDEvent先通知source1,source1唤醒RunLoop,然后将事件分发给source0,由source0来处理。

六、CFRunLoopTimerRef-Timer事件

CFRunLoopTimer是定时器,下面是CFRunLoopTimer的源码:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    // timer对应的RunLoop
    CFRunLoopRef _rlModes;
    // 下一次触发的时间
    CFAbsoluteTime _nextFireDate;
    // 定时器的间隔
    CFTimeInterval _interval;
    // 定时器允许的误差
    CFTimeInterval _tolerance;
    uint64_t _fireTSR;
    // 优先级
    CFIndex _order;
    // 任务回调
    CFRunLoopTimerCallBack _callout;
    // 上下文
    CFRunLoopTimerContext _context;
};

从上面的代码可以看出,timer是依赖于RunLoop的,而且有函数指针回调,那么便可以在设定的时间点抛出回调执行任务。苹果的官方文档也提到CFRunLoopTimer和NSTimer是toll-free bridged的,这意味着两者之间可以互相转换。

七、CFRunLoopObserverRef-观察者

CFRunLoopObserver是观察者,可以监测RunLoop的各种状态变化。
下面是CFRunLoopObserver的源码:

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    // 对应的RunLoop对象
    CFRunLoopRef _runLoop;
    // 当前的观察的RunLoop个数
    CFIndex _rlCount;
    // RunLoop的状态
    CFOptionFlags _activities;
    CFIndex _order;
    // 回调
    CFRunLoopObserverCallBack _callout;
    // 上下文
    CFRunLoopObserverContext _context;
};

RunLoop的source事件源监测是否有需要执行的任务,observer监测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 // 所有RunLoop活动
}

八、RunLoop执行事件的顺序和逻辑

RunLoop运行代码在__CFRunLoopRun方法中,这个方法的源码很长,实际上该方法内部就是一个do-while循环。当调用该方法时,线程就会一直留在这个循环里面,直到超时或者手动被停止,该方法才会返回。在这个循环里面,线程在空闲的时候处于休眠状态,在有事情要处理的时候,处理事件。
该方法是整个RunLoop运行的核心方法。苹果官方文档对于RunLoop处理各类事件的流程有着详细的描述。
1、通知观察者RunLoop已经启动。
2、通知观察者定时器即将触发。
3、通知观察者任何不基于端口的输入源都将触发(即将处理source0)。
4、触发所有准备触发的非基于端口的输入源(处理source0)。
5、如果基于端口的输入源已经准备好并等待启动,则立即处理事件,并进入步骤9(处理source1)。
6、通知观察者线程即将进入休眠状态。
7、使线程进入休眠状态,直到发生以下事件之一:

  • 某一事件到达基于端口的源(source1);
  • 定时器触发;
  • RunLoop设置的超时时间已经到时;
  • RunLoop被显示的唤醒;

8、通知观察者线程即将被唤醒;
9、处理未处理的事件:

  • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop,进入步骤2;
  • 如果输入源启动,传递相应的事件;
  • 如果RunLoop被显式地唤醒而且还没有超时,重启RunLoop,进入步骤2;

10、通知观察者RunLoop退出。 整个流程如下图所示: 4.webp

九、RunLoop的应用

(一)卡顿检测

1.1 卡顿原因

iOS开发中,UIKit是非线程安全的,因此一切与UI相关的操作都必须在主线程执行,系统会每16.6ms将UI的变化重新绘制渲染到屏幕上。如果UI刷新的时间间隔小于16ms,那么用户是不会感到卡顿的。但是如果在主线程进行了一些耗时操作,阻碍了UI的刷新,那么就会产生卡顿,甚至卡死。主线程对于任务的处理是基于RunLoop机制。
在日常开发中,UIEvent事件、Timer事件、dispatch主线程任务都是在RunLoop的循环机制的驱动下完成的。一旦我们在主线程中的任何一个环节进行了一个耗时操作或者因为锁的使用不当造成了与其它线程的死锁,主线程就会因为无法执行Core-Animation的回调而造成界面无法刷新。用户的交互也依赖于UIEvent的传递和响应,该流程也必须在主线程中完成。所以,主线程的阻塞会导致UI和交互的双双阻塞,这也是导致卡死和卡顿的根本原因。

1.2 检测卡顿的原理

卡顿的根本原因在于主线程RunLoop阻塞,我们要通过技术手段监测主线程RunLoop的运行状态。
为了能够实时获取主线程RunLoop的状态,要对主线程注册观察者获取RunLoop的运行状态。在触发观察者回调时,利用signal机制将其运行状态传递给另一个正在监听的子线程(后面称之为监听线程)。通过监听线程,我们可以完整的了解到主线程RunLoop循环的周期目前处于哪个阶段、耗时了多久等等。
RunLoop的运行逻辑中,RunLoop调用方法主要在两个状态区间:

  • kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间
  • kCFRunLoopAfterWaiting之后

根据这些必要的信息,就可以采取对应的策略进行异常的捕获和处理。

1.3 检测卡顿的方案

目前大多数APM工具都是采用监听RunLoop的方式进行卡顿的捕获,这也是性能和准确性表现最好的一种方案。
卡顿监控的特点在于主线程的阻塞是暂时的、能够恢复的,因此我们要获取卡顿持续的时间,用来评估卡顿问题的严重性,我们预先设定一个卡顿时间的阈值T,当主线程阻塞的时间超过该阈值,则会触发全线程的抓栈。

static int kTimeout = 1000; // 单次定时器触发时间,1000ms
static int kTimeoutCount = 3; // 定时器触发次数,总时间为timeout * timeoutCount
@interface PerformanceMonitor()
@property (nonatomic, assign) int timeoutCount;
@property (nonatomic, assign) CFRunLoopObserverRef observer; // RunLoop观察者
@property (nonatomic, strong) dispatch_semaphore_t semaphore;
@property (nonatomic, assign) CFRunLoopActivity activity; // RunLoop的状态值
@end

@implementation PerformanceMonitor
+ (instancetype)sharedInstance {
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

// RunLoop观察者的回调方法
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    // C语言函数中不能使用self
    PerformanceMonitor *monitor = (__bridge PerformanceMonitor *)info;
    monitor.activity = activity;
    dispatch_semaphore_t semaphore = monitor.semaphore;
    dispatch_semaphore_siganl(semaphore);
}

- (void)stop {
    if (!_oberver) {
        return;
    }
    // 移除观察者
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    CFRelease(_observer);
    _observer = NULL
}

- (void)start {
    if (_observer) {
        return;
    }
    // 创建信号
    _semaphore = dispatch_semaphore_create(0);
    // 创建RunLoop观察者上下文,把Self作为上下文信息传进去,以便在回调用使用(C语言函数中不能使用self)
    CFRunLoopObserverContext context = {0, (__bridge void*)self, NULL, NULL, NULL};
    // 创建RunLoop观察者
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
    // 将观察者添加到主线程RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    __weak typeof(self) weakSelf = self;
    // 在子线程监控时长
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        while(YES) {
            // dispatch_semaphore_wait方法如果超时,则会返回一个不等于0的整数,如果收到dispatch_semaphore_signal就会返回0
            long st = dispatch_semaphore_wait(weakSelf.smeaphore, dispatch_time(DISPATCH_TIME_NOW, kTimeout * NSEC_PER_MSEC));
            if (st != 0) { // 信号超时,说明RunLoop状态在超时时间内没有改变
                if (!weakSelf.observer) {
                    weakSelf.timeoutCount = 0;
                    weakSelf.semaphore = 0;
                    weakSelf.activity = 0;
                    return;
                }
                
                // 查看RunLoop卡在哪个状态,如果是kCFRunLoopBeforeSources或者kCFRunLoopAfterWaiting状态,说明产生了卡顿
                if (weakSelf.activity == kCFRunLoopBeforeSources || weakSelf.activity == kCFRunLoopAfterWaiting) {
                    weakSelf.timeout++;
                    if (weakSelf.timeout < kTimeoutCount) {
                        continue;
                    }
                    // 抓栈
                }
            }
            // 状态改变,超时次数重置为0
            weakSelf.timeoutCount = 0;
        }
    });
}

(二)常驻线程

有时候我们需要创建一个线程在后台一直做一些任务,但常规的线程在任务完成后就会立即销毁,因此需要常驻线程让线程一直都存在。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run {
    NSRunLoop *currentRl = [NSRunLoop currentRunLoop];
    [currentRl addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [currentRl run];
}

- (void)run2 {
    NSLog(@"常驻线程");
}

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

上面通过给RunLoop添加一个Port实现了RunLoop和线程的常驻。

十、参考资料