iOS 底层探索之Runloop

本篇是探索底层Runloop,目的是能够深入理解Runloop是干什么用的?什么时候用?怎么用?

1、什么是runloop?

runloop是一个循环,它在持续不断的跑圈,iOS应用程序刚打开时,就创建了一个主线程,并默认创建了Runloop保持主线程的持续运行。

我们到官方文档搜索一下Runloop,如图所示

image.png

发现找不到Runloop,再尝试搜索thread,发现在线程介绍里面竟然出现了Runloop字样,如图
image.png

如此可见,Runloop和线程之间有着不清不楚的关系。

再来看一下CFRunloop源码中的CFRunLoopRun函数

void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

我们可以看到,Runloop本质是一个do...while循环。
结合官方文档提供的运行循环结构看一下,Runloop是如何执行的。


image.png

从上图中我们可以看出,Runloop就是依附在线程上的循环,通过输入源(Input sources)和定时源(Timer sources)接收事件,然后交给线程去处理事件。

所以什么是Runloop?
Runloop就是一个循环,为了线程而生,它的本质是一个do...while循环

2、Runloop的作用

  • 1、保持程序持续的运行
    一般情况下,线程在执行完任务就会退出,如果我们不希望线程退出,还想让它执行更多的任务,就需要用到Runloop了。
  • 2、接收并处理App中的各种事件
    Runloop在循环时,通过输入源(input source)和定时源(timer source)接收App事件(触摸事件、UI刷新时间、定时器、performSelector)。
  • 3、提升性能
    在线程不工作时休眠,节省CPU资源。

以上就是Runloop的作用,这里只是概括一下,后面会有具体到用法的Runloop应用。

3、Runloop和线程的关系

接下来我们结合Runloop源码看看它和线程之间的关系,找到_CFRunLoopGet0这个函数,它的作用是获取runloop对象

//CFRunloop.c
//这个类是获取Runloop对象的
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (!__CFRunLoops) {//CFRunloops是存放runloop和线程对应关系的字典
        //创建存放runloop和线程的字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //获取主线程runloop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //将主线程和主线程对应的Runloop对象mainLoop添加到字典中
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //获取子线程对应的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
        //依据线程t创建Runloop对象
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //绑定线程和runloop到字典中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
    }
}

上述代码中有一段CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);,意思是以线程为key,runloop为value,将runloop存储到一个全局的字典中。
至此我们得出了第一个结论:线程和Runloop是一一对应的关系。

再看这段获取主线程runloop的代码CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());,意思是创建一个主线程的Runloop。
至此得出第二个结论:Runloop是以线程为参数创建的,并保存在全局的字典里。

再看后半段代码

//获取子线程对应的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    if (!loop) {
        //依据线程t创建Runloop对象
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
        
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //绑定线程和runloop到字典中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
    }

这段代码的意思是先从__CFRunLoops字典中获取Runloop对象,若没有,则以线程为参数创建一个,并存储到__CFRunLoops字典里,至此我们知道了第三个结论:主线程的Runloop由系统自动创建,子线程的Runloop需要在子线程里手动获取Runloop时创建。

综上我们知道了Runloop和线程的关系:
1、线程和Runloop是一一对应的关系。
2、Runloop是以线程为参数创建的,并保存到全局的字典里
3、主线程的Runloop由系统自动创建,子线程的Runloop需要在子线程里手动获取Runloop时创建。
4、Runloop在第一次获取时创建,在线程销毁时随之销毁。

4、Runloop的五个对象

1.__CFRunLoop * CFRunLoopRef;
2.__CFRunLoopSource * CFRunLoopSourceRef;
3.__CFRunLoopObserver * CFRunLoopObserverRef;
4.__CFRunLoopTimer * CFRunLoopTimerRef;
5.CFRunloopModeRef(为什么这个这么写呢,因为Runloop并没有暴露RunloopMode这个对象)

下面逐一讲一下Runloop这几个对象的含义和它们之间的关系,如图


上图就是Runloop对象、Mode、Source、Observer、Timer之间的关系。
一个Runloop包含若干个CFRunloopModeRef(运行模式),一个CFRunloopModeRef又包含若干个CFRunLoopSourceRef(输入源)/CFRunLoopTimerRef(定时源)/CFRunLoopObserverRef(观察源),但是Runloop同一时间只能指定一个CFRunloopModeRef(运行模式),如果要切换CFRunloopModeRef,需要先退出Runloop,再指定一个CFRunloopModeRef(运行模式)进入。

为什么是这样的结构?
答:这样做主要是为了分离Source/Timer/Observer,让其互不影响。

4.1、CFRunLoopRef(Runloop对象)

CFRunLoopRef 是 Core Foundation 框架下 RunLoop 对象类,可通过如下方式获取

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

也可以使用Foundation框架中的NSRunloop获取封装过的Runloop,NSRunloop是对CFRunLoopRef的封装

// 获得当前线程的 RunLoop 对象
[NSRunLoop currentRunLoop]; 
// 获得主线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; 
4.2、CFRunLoopSourceRef(输入源)

先看一下源码

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    union {
        //对应Source0
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        //对应Source1
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

从源码看出Source分为个版本

  • 1、Source0:这种版本的Source不能主动触发事件,得是用户或开发者手动触发,比如触摸事件和performSelector等App内部事件(UIEvent),需要先进行CFRunLoopSourceSignal标记,再通过CFRunLoopWakeUp唤醒Runloop处理事件。
  • 2、Source1:基于Port,用于通过内核和线程之间通信的,这种Source可以主动唤醒Runloop线程,一会儿用例子看一下。

先看一个触摸事件触发Source0的例子,创建个工程,上面放一个按钮,在点击事件的回调中打断点,如图


image.png

在左侧栏目中是触发的方法,如图


image.png

我们可以看到,用户的触摸事件,果然触发的是Source0。

下面是Source0的使用例子
要创建一个Source0输入源,需要执行六步走,
1、创建Context上下文
2、创建Source0输入源对象
3、获取Runloop
4、绑定Runloop、Source0和mode
5、标记执行信号CFRunloopSourceSignal
6、唤醒CFRunLoopWakeUp

void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"准备代发");
}

void perform(void *info){
    NSLog(@"执行吧,骚年");
}

void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
    NSLog(@"取消了,终止了!!!!");
}
- (void)source0Demo{
    //1、创建Context上下文
    CFRunLoopSourceContext context = {
        0,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        NULL,
        schedule,
        cancel,
        perform,
    };
    /**
     2、创建CFRunLoopSourceRef
     参数一:传递NULL或kCFAllocatorDefault以使用当前默认分配器。
     参数二:优先级索引,指示处理运行循环源的顺序。这里我传0为了的就是自主回调
     参数三:为运行输入源保存上下文信息的结构
     */
    CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
    //3、获取Runloop
    CFRunLoopRef rlp = CFRunLoopGetCurrent();
    //4、绑定Source、Runloop、Mode,此时我们的source就进入待绪状态
    CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
    //5、 一个执行信号
    CFRunLoopSourceSignal(source0);
    //6、 唤醒 run loop 防止沉睡状态
    CFRunLoopWakeUp(rlp);
    // 取消 移除
//    CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
    CFRelease(rlp);
}

从代码中可以看出,若要让Runloop执行Source0的事件,需要先发出一个执行信号CFRunLoopSourceSignal,再调用CFRunLoopWakeUp唤醒Runloop执行任务。

控制台打印效果如下


image.png

下面是Source1使用Port进行线程间通讯的例子
创建

@interface ViewController ()<NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;//主线程Port
@property (nonatomic, strong) NSPort* mainThreadPort;//子线程Port
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    [self portCommunicateTest];
}
- (void)portCommunicateTest{
    self.mainThreadPort = [NSPort port];
    self.mainThreadPort.delegate = self;
    // port - source1 -- runloop
    //port是操作Source1的,所以同样依赖于runloop
    [[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];

    //创建子线程
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        //实例化子线程对应的Port
        self.subThreadPort = [NSPort port];
        self.subThreadPort.delegate = self;
        
        [[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    });
}

//NSPort的代理方法,线程间通信的回调
- (void)handlePortMessage:(id)message {
   NSLog(@"当前线程是 == %@", [NSThread currentThread]); // 3 1
    NSLog(@"传来的消息内容 = %@", [[NSString alloc] initWithData:[message valueForKey:@"components"][0] encoding:NSUTF8StringEncoding]);
    sleep(1);
    if (![[NSThread currentThread] isMainThread]) {
        //像子线程的Port发送消息
        NSMutableArray* components = [NSMutableArray array];
        NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
        [components addObject:data];

        [self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
    }
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    //注意,component必须以NSData的形式传递
    NSMutableArray* components = [NSMutableArray array];
    NSData* data = [@"Hello" dataUsingEncoding:NSUTF8StringEncoding];
    [components addObject:data];
    
    [self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}

点击屏幕后,打印结果如下

image.png

在回调消息处打断点,发现如图所示
image.png

看到左侧显示的输入源是Source1,至此使用NSPort进行线程间通信的例子执行完毕,真可爱,线程还能这么玩。

4.3、CFRunloopModeRef

Runloop有五种运行模式

    1. UIInitializationRunLoopMode :在App刚启动进入的运行模式,启动完成后会切换到kCFRunLoopDefaultMode,从此不再使用。
    1. kCFRunLoopDefaultMode: 默认运行模式,在主线程运行在这个模式下。
    1. UITrackingRunLoopMode:界面跟踪模式,当见面滚动时,会切换到这个运行模式下,保证不收其他模式的影响。
    1. GSEventReceiveRunLoopMode: 接受系统事件的内部运行模式。
    1. kCFRunLoopCommonModes: 占位模式,通常用来标记kCFRunLoopDefaultModeUITrackingRunLoopMode,如果NSTimer加入这个模式,将不受运行模式切换的影响。
4.4、CFRunloopTimerRef

CFRunloopTimerRef是一个时间触发器,它包含一个时间长度和一个回调(函数回调),在加入Runloop时,Runloop会注册一个时间点,经过时间长度后,Runloop会被唤醒执行回调。Timer的底层就是一个CFRunloopTimerRef,它受Mode切换的影响。如果把Timer加入到kCFRunLoopCommonModes就不会受切换影响了,像下面这样

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0f repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"log NSTimer runloop");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.5、CFRunloopObserverRef

CFRunloopObserverRef是一个观察者,用来监控Runloop的状态的,它分为以下状态

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop 1
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer 2
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 32
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒 64
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop 128
};

举个例子

- (void)obseverDemo{
    //1、创建观察者上下文
    CFRunLoopObserverContext context = {
        0,
        ((__bridge void *)self),
        NULL,
        NULL,
        NULL
    };
    //2、获取当前Runloop对象
    CFRunLoopRef rlp = CFRunLoopGetCurrent();
    //3、创建观察者CFRunLoopObserverRef
    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, runLoopObserverCallBack, &context);
    //4、将观察者observer和runloop对象、Mode关联起来
    CFRunLoopAddObserver(rlp, observerRef, kCFRunLoopDefaultMode);
}

void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    switch (activity) {
    case kCFRunLoopEntry:
        NSLog(@"RunLoop进入");
        break;
    case kCFRunLoopBeforeTimers:
        NSLog(@"RunLoop要处理Timers了");
        break;
    case kCFRunLoopBeforeSources:
        NSLog(@"RunLoop要处理Sources了");
        break;
    case kCFRunLoopBeforeWaiting:
        NSLog(@"RunLoop要休息了");
        break;
    case kCFRunLoopAfterWaiting:
        NSLog(@"RunLoop醒来了");
        break;
    case kCFRunLoopExit:
        NSLog(@"RunLoop退出了");
        break;
        
    default:
        break;
    }
}

在我们滚动视图时,控制台打印如下


image.png

由此可见,官方说的对☺,确实可以通过CFRunloopObserverRef监听Runloop的状态。

Runtime的应用有很多,之前在项目中应用的比较深的是将对CPU压力比较大的UI任务拆分成多个小任务,通过监听Runloop的Observer空闲时机,在空闲时强制其执行小任务,高效利用系统资源提升性能。
RunLoopWorkDistribution了解一下(😏)

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

推荐阅读更多精彩内容