OC-Runloop

RunLoop 是什么

runloop 就是一个运行循环,目的是让程序运行起来不会直接结束,能在有任务的时候处理任务,没有任务的时候等待处理任务。

iOS 中有两套API 可以访问 runloop

  • Foundation : NSRunLoop
  • Core Foundation : CFRunLoopRef

参考

RunLoop 与线程的关系

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

获取 RunLoop 对象

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

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

可以从 CFRunLoopGetCurrent() 源码中看到相关获取逻辑,代码简化如下


CFRunLoopRef CFRunLoopGetCurrent(void) {
    // 返回根据当前线程取到的runloop
    return _CFRunLoopGet0(pthread_self());
}

// 核心逻辑如下,已简化代码
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    
    // 1.判断如果没有runloop字典
    if (!__CFRunLoops) {
        // 2.创建一个字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 3.根据主线程创建runloop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(_CFMainPThread);
        // 4.在dict中将主线程指针和当前创建的runloop以 key:value 形式保存
        CFDictionarySetValue(dict, pthreadPointer(_CFMainPThread), mainLoop);
        // 5.dict 赋值给 __CFRunLoops(代码经简化,源码并非如此简单)
        __CFRunLoops = dict;
    }
    
    // 6. 从 __CFRunLoops 中根据参数t指针获取runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) {
        // 7. 如果没有就创建一个新loop对象
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        // 8. __CFRunLoops 中添加参数t的指针和新loop对象保存
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        // 9. 新runloop 对象就是要返回的loop对象
        loop = newLoop;
    }
    return loop;
}

runloop 内部数据结构

runloop 内部核心数据

struct __CFRunLoop {
    pthread_t _pthread;                 // 对应的线程
    CFMutableSetRef _commonModes;       // 通用Mode
    CFMutableSetRef _commonModeItems;   // 通用mode的items
    CFRunLoopModeRef _currentMode;      // 当前mode
    CFMutableSetRef _modes;             // 所有的mode
};

从数据结构中可以看到,实际就是 runloop 中包含自己的 thread 和 mode, CFRunLoopModeRef 类型结构如下

struct __CFRunLoopMode {
    CFStringRef _name;
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
};

可以看到 runloop 的内部结构基本如图

runloop结构

runloop 的运行模式 CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer
  • RunLoop启动时只能选择其中一个Mode,作为currentMode
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入
    • 不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出
  • 常见的2种Mode
    • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

Runloop 的运行逻辑

说白了 runloop 就是在整循环中一直检查当前运行的 mode 内部有没有需要处理的事件,有就处理,没有就休眠

runloop运行逻辑

Runloop 的 mode 内部主要有下面4类事件要处理

  • Source0

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

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

    • NSTimer
    • performSelector:withObject:afterDelay:
  • Observers

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

一次循环处理事件的逻辑如下

一次runloop的循环逻辑

一个简单的线程保活

@interface PermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@end

@implementation PermenantThread
#pragma mark - public methods
- (instancetype)init
{
    if (self = [super init]) {
        self.innerThread = [[NSThread alloc] initWithBlock:^{
            
            // 创建上下文(要初始化一下结构体)
            CFRunLoopSourceContext context = {0};
            // 创建source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            // 往Runloop中添加source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            // 销毁source
            CFRelease(source);
            
            // 启动
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
            
//            while (weakSelf && !weakSelf.isStopped) {
//                // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
//                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
//            }
            
            NSLog(@"end----");
        }];
        
        [self.innerThread start];
    }
    return self;
}

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

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

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

#pragma mark - private methods
- (void)__stop
{
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

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

@end

面试题

  • 讲讲 Runloop,项目中有用到吗?
1. 用到过
2. 线程保活,对于频繁需要在子线程做的操作,使用 runloo 进行线程保活 -- 适用于一个串行线程,非并发的情况。如需要一个后台线程持续做否些计算,网络请求。
3. 如 AFNetworking 2.0版本的时候就是后台有一个常驻线程

1. 实例
    1. 控制线程声明周期(线程保活)
    2. 解决NSTimer在滑动时停止工作的问题
    3. 监控应用卡顿
    4. 性能优化
  • runloop 内部实现逻辑?
1. runloop 的创建逻辑,从内部的 dict 中获取,没有就新建,并保存,目的是一条线程只对应一个runloop
2. runloop 的运行逻辑,本质上就是一个 while() 循环。每次进入循环的时候处理如上图事件。没有事件的时候进入休眠(通过调用内核接口实现)。
  • runloop 和线程的关系?
是一一对应的关系。
其实保存在 __CFRunLoops 全局 dict 中,以线程指针为key,线程对应的runloop地址为value
  • timer 和 线程的关系?
  • 程序中添加每3秒响应一次的NSTimer,当拖动 tableView 时候 timer 可能无法响应怎么解决?
将 timer 添加到 commonMode中,这样就可以处理普通模式和tracking 模式下的事件了
  • runloop 是怎么影响用户操作的,具体流程是什么样的?
由 source1 捕捉用户的触摸事件, 由 source0 处理用户的触摸事件。 V158T13
  • 说说 runloop 的几种状态?
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
  • runloop 的 mode 作用是什么?
不同 mode 将各自的 source、timer、observer 隔离开,这样同一时间只能执行一个 mode 下的 source、timer、observer,这样在某种模式下就会比较流畅。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349