iOS - RunLoop

image.png

基本认识

顾名思义,在程序运行的过程中循环做一些事情。


image

在开发的过程中,我们接触到的 NSTimer 相关、GCD Async Main Queue、事件响应、手势识别、界面刷新、网络请求和自动释放池都是基于 RunLoop 实现。

项目的主程序入口 main 函数会返回一个 UIApplicationMain,在这个过程中就会开启一个 RunLoop 对象,这个对象就会循环处理一些事情,当我们点击一个可以交互的 UI 控件的时候,程序会做出响应,这都是 RunLoop 的功劳。

所以说 RunLoop 可以保持程序的正常运行,能响应各种事件,并节省 CPU 资源,提高程序性能:没有事件的时候待命,有事件的时候处理事情。

RunLoop 对象

iOS 中有 2 套 API 访问和使用 RunLoop,分别是 Foundation 中的 NSRunLoopCore Foudation 中的 CFRunLoopRef前者是后者的 Objective-C 封装。并且 CFRunLoopRef 是开源的,开源地址在。下面就是获取当前的 RunLoop 对象:

[NSRunLoop currentRunLoop];
CFRunLoopGetCurrent();

RunLoop 和线程

每条线程都有一个唯一的一个与之对应的 RunLoop 对象,并且 RunLoop 保存在一个全局的 Dictionary 中,线程为 key,RunLoop 为 value。
刚创建的线程是没有 RunLoop 对象的,RunLoop 会在第一次获取它的时候创建。RunLoop 会随着线程的结束销毁,主线程比较特殊,会自动创建并获取 RunLoop。

在源码中,CFRunLoopGetCurrent 的实现为:

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

我们看到最终调用的是 _CFRunLoopGet0 方法,该方法中有:

CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); // 先从字典中查找是否有对应的 RunLoop 
__CFUnlock(&loopsLock);
if (!loop) {
    CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 没查找到,创建
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
}
if (!loop) {
    CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); // 将新创建的 RunLoop 保存到全局的字典当中
    loop = newLoop;
}

这验证了 RunLoop 会存在一个全局的字典当中这一说法。

pthreadPointer(t) 为线程。

RunLoop 相关的类

在 Core Foundation 中和 RunLoop 相关的有 5 个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopOberverRef

CFRunLoop 的底层结构为:

typedef struct __CFRunLoop* CFRunLoopRef;
struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
}

从结构体可以看出,一个 RunLoop 可以装着多个 mode,但实际只有一个 mode 是 currentMode。

__CFRunLoopMode 的结构为:

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; // 对应 CFRunLoopSourceRef 对象
    CFMutableSetRef _sources1; // 对应 CFRunLoopSourceRef 对象
    CFMutableArrayRef _observers; // 对应 CFRunLoopOberverRef 对象 
    CFMutableArrayRef _timers; // 对应 CFRunLoopTimerRef 对象
    CFMutableDictionaryRef _portToV1SourceMap;
    ...
};

所以,总体关系如下:


image

RunLoop 启东时只能选一个 Mode 作为 Current Mode,若要切换 Mode,只能退出当前 RunLoop,重新选择一个 Mode 再进入。
这样做的好处是:不同组的 source0、source1、timer、observer 相互隔离,互不影响。

如果 Mode 中没有任何 source0、source1、timer、observer 则 RunLoop 会立即退出。

常见的 Mode

  • kCFRunLoopDefaultMode (NSDefaultRunLoopMode),主线程是在这个 Mode 下执行的。

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

其他的 Mode 开发中不常用。

并且需要注意的是,主线程切换 Mode 并不会导致程序退出,切换 Mode 的操作还是在事件循环中进行的,并不会打破事件循环。

那么创建一个 RunLoop 并选择一个 Mode 后,最终处理的就是 Mode 下的 source0、source1、timer、observer。

source0

一般指触摸事件处理,我们新建一个空白程序,在初始界面添加触摸方法,并在注释位置加断点:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"%s", __func__); // 加断点
    
}

进入调试环境后借助 bt 命令打印函数调用栈:

image.png

在上图 #9 的位置看到了 source0 的影子。

performSelector: onThread: 系列方法也是 source0 的一个范畴。

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

如点击事件,一开始是由 source1 捕捉,然后分发给 source0 处理。

Timers

就是我们熟知的 NSTimer,另,performSelector: withObject: afterDelay: 也属于 Timer 范畴。

obervers
  • 用于监听 RunLoop 的状态;
  • UI 刷新;
  • Autorelease pool;

如对 UI 控件进行颜色设置的时候,并不会立即生效,监听器会在 RunLoop 休眠之前进行 UI 刷新。自动释放池同理。

有时候,我们也会手动添加 observer,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
};

我们写个例子来监听这些状态:

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

observeRunLoopActicities 为 C 语言函数:

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;
    }
}

触摸了一下屏幕发现打印:


image.png

在触摸函数调用之前,RunLoop 的状态为 kCFRunLoopBeforeSources 即即将处理 source。
我们将触摸函数改为:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    [NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"This is a timer");
    }];
    
}

增加了一个定时器,运行并触摸屏幕打印结果为:


image

在处理定时器之前,RunLoop 的状态为 kCFRunLoopAfterWaiting 即唤醒状态。

RunLoop 的运行逻辑

  • 首先,通知 Observers 进入 Loop;

  • 进入 Loop 后,再次通知 Observers,即将处理 Timers;

  • 通知 Observers 即将处理 Sources;

    • 处理 blocks;
    • 处理 Source0,并且可能会再次处理 blocks;
  • 如果没有 Source1,通知 Observers 进入休眠状态;

  • 如果有 Source1,通知 Observers 结束休眠,处理消息事件;
    a. 处理 Timer;
    b. 处理 GCD 的队列;
    c. 处理 Source1;

  • 处理 blocks;

  • 根据前面的执行结果,决定如何操作:

    • 可能不退出 RunLoop 继续从处理 Timer 开始;
    • 若退出 RunLoop,会通知 Observers 退出 Loop;
  • 通知 Observers 退出 Loop;

执行逻辑源码解读

CFRunLoop.c 中,SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) 为整个 RunLoop 的入口。

去除细节和加锁代码,为:

SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {         

    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); // 通知 Observers 进入 Loop
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode); // 主要的运行逻辑

    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); // 通知 Observers 退出 Loop

    return result;
}

__CFRunLoopRun 中有非常复杂的逻辑:

static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    ...
    ...
    int32_t retVal = 0; 
    do {
        ...
        // 通知 Observers 即将处理 Timbers
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知 Observers 即将处理 Sources
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 处理 blocks
        __CFRunLoopDoBlocks(rl, rlm);
        // 处理 Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            __CFRunLoopDoBlocks(rl, rlm); //再次处理 blocks
          }
        ...
    
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
            msg = (mach_msg_header_t *)msg_buffer;
            // 判断有没有 Source1
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
                // 如果有 Source1,跳转到 handle_msg
                goto handle_msg;
            }
        }
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    __CFRunLoopSetSleeping(rl); // 即将休眠
        ...
        do {
            ...
            // 等待别的消息唤醒当前线程
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        } while (1);
        ...

        __CFRunLoopSetIgnoreWakeUps(rl);
        __CFRunLoopUnsetSleeping(rl);
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting))         __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting); // 通知 Observers 结束休眠
    handle_msg:
        ...
        ...
        // if - else if - ... - else 的部分是判断如何醒来的
        if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) { // 被 Timers 唤醒
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())){
                    __CFArmNextTimerInMode(rlm, rl);
            }
        }
        else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) { // 被 Timers 唤醒
            CFRUNLOOP_WAKEUP_FOR_TIMER();
            
        else if (livePort == dispatchPort) { // 被 GCD 唤醒
            ...
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); // 处理 GCD 相关
            ...
        } else { // 其余都被认定是 Source1 唤醒
            ...
              sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply); // 处理 Source1
              ...
    } while (0 == retVal); // 整个 do-while 就是循环处理事件的部分
    ...
    ...
}

需要注意的是,在通知线程进入休眠的状态时候并非传统意义上的阻塞,而是真正的进入了休眠状态,也就是内核层面的休眠。

内核层面的 API 和操作系统打交道,并不开放,应用层面的 API 是开放的,可以进行 UI、网络层等编程。

RunLoop 的实际应用

控制线程周期

最典型的开源框架 AFNetworking 就是用了 RunLoop 的技术来控制子线程的生命周期:创建线程后,一直让线程处于内存中不销毁,在某一刻需要执行任务的时候就让子线程处理,在某一刻销毁子线程的话就停止 RunLoop。

假如,在控制器中有这样一个需求,启动控制器的时候就开启子线程,并进行线程保活在点击停止按钮的时候,就终止线程的 RunLoop,那么实现为:

#import "ViewController.h"
#import "VThread.h"

@interface ViewController ()

@property (nonatomic, strong) VThread* thread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.stopped = NO;
    __weak typeof(self) weakSelf = self;
    self.thread = [[VThread alloc] initWithBlock:^{
        NSLog(@"%s %@", __func__, [NSThread currentThread]);
        
        // 向 RunLoop 中添加 Observers、Timers、Sources
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode: NSDefaultRunLoopMode];
        
        while (weakSelf && !weakSelf.isStopped) {
            [[NSRunLoop currentRunLoop] runMode: NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; // 永不超时,RunLoop 永远执行
        }
        
        NSLog(@"==== end ====");
    }];
    
}
- (IBAction)stopButtonDidClick:(id)sender {
    
    
    if (!self.thread) return;
    // 子线程执行终止 RunLoop
    // YES 标识表示等待 stopRunLoop 执行完再继续走下面的逻辑
    [self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone: YES];
}

- (void)dealloc {
    
    self.thread = nil;
    [self stopRunLoop];
}

// 终止子线程的 RunLoop
- (void)stopRunLoop {
    
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    // 清空线程
    self.thread = nil;

}
@end

NSTimer 失效问题

NSTimer 在默认情况是 NSDefaultRunLoopMode 模式的,那么在复杂的 UI 控制其中,在滑动 UIScrollView 及其子类的时候模式会切换为 UITrackingRunLoopMode 模式,造成只能在NSDefaultRunLoopMode 模式下工作的 Timer 的停止工作,进而失效。

NSTimer 的 scheduled.... 系列方法都是设置的默认模式,所以不建议使用。

那么解决办法就是:

NSTimer* timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        // ....
    }];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

kCFRunLoopCommonModes 并不是一个真正的全新的模式,仅仅作为标记的作用,标记着任何模式下都是通用的、可行的。
在底层结构中,CFRunLoop 的结构体中:

struct __CFRunLoop {
    pthread_t _pthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
}

_commonModes 包装的就是 kCFRunLoopCommonModes 和 UITrackingRunLoopMode。

其他应用

  • 监控应用卡顿
  • 性能优化

这里的卡顿检测主要是针对在主线程执行了耗时的操作所造成的,这样可以通过 RunLoop 来检测卡顿:添加 Observer 到主线程 RunLoop 中,通过监听 RunLoop 状态的切换的耗时,达到监控卡顿的目的。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335