RunLoop初探,满足项目的基本应用

iOS开发肯定离不开多线程编程,而多线程又跟RunLoop有着密切的关系,这篇文章就来解剖下RunLoop。

每个application运行都会开启一个主线程(UI线程),主线程默认是开启RunLoop的,让application可以随时接收用户的触摸事件实现交互,也可以处理复杂的业务逻辑,还可以休眠。

当我们开启子线程执行任务时,子线程默认是不开启RunLoop的,等子线程的任务执行完,子线程就会被系统销毁回收。但有时候我们会频繁的开启线程去执行任务,开启线程又销毁线程,这也是有一定的性能代价的,所以我们可以让一个子线程成为常驻线程,有任务就执行,没任务就休眠,这样就降低频繁开启和销毁线程的性能浪费。

让一个子线程成为常驻线程就必须开启子线程的RunLoop。开启RunLoop必须要有一个输入源或定时源,不然RunLoop开启就会马上关闭。输入源(input source)传递异步事件,通常事件来自其他的线程或程序。定时源(timer source)则传递同步事件,发生在特定时间或重复的时间间隔的事件。RunLoop的运行要指定其运行模式,无论是隐式或显式。

RunLoop模式

  • kCFRunLoopDefaultMode: 默认模式,
  • UITrackingRunLoopMode: 界面追踪模式,一般用于scrollView滑动触摸追踪
  • UIInitializationRunLoopMode: 启动APP模式,启动完成后就不再使用
  • NSRunLoopCommonModes: 占位模式,包含多种模式:default,modal,tracking

除了系统的模式,我们也可以使用自定义模式,NSRunLoopMode的字符串类型可以用于自定义。

RunLoop模式的切换

  • 对于非主线程,我们可以退出当前模式,然后再进入另一个模式,也可以直接进入另一个模式,即嵌套
  • 对于主线程,我们当然也可以像上面一样操作,但是主线程有其特殊性,有很多系统的事件。系统会做一些切换,我们更关心的是系统是如何切换的?系统切换模式时,并没有使用嵌套

简单开启子线程示例代码如下

- (void)startThread { @autoreleasepool {
    NSThread *currentThread = [NSThread currentThread];
    BOOL isCancelled = [currentThread isCancelled];
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];

    /**** 以时钟开启RunLoop ****/
    [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] repeats:YES block:^(NSTimer * _Nonnull timer) {
    // 空任务
    }];
/*
    [NSTimer scheduledTimerWithTimeInterval:5 repeats:YES block:^(NSTimer * _Nonnull timer) {
     // 空任务
    }];
*/
    // 开启RunLoop
    while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
        isCancelled = [currentThread isCancelled];
        NSLog(@"run ----------- run");
    }
}}

RunLoop的开启有几种方法,run, runUntilDate:, runMode:beforeDate:。run方法启动要关闭RunLoop就比较麻烦了,其它两个方法都可以轻易关闭RunLoop。
注意,以** 输入源 唤醒线程做任务,做完任务就会退出RunLoop,如果是以runMode:beforeDate:启动的RunLoop就会直接退出,子线程执行完毕被回收,另外两个方法启动的RunLoop,会退出RunLoop然后又进入RunLoop。以 时钟源 唤醒线程做任务,除了run方法外的启动RunLoop,会受到设置的期限影响,进而退出RunLoop。这里的示例代码为了方便控制RunLoop,使用runMode:beforeDate:启动,还加上线程的取消标志,让RunLoop退出又马上以runMode:beforeDate:**启动,直到当线程取消,使while循环被打破。

结束子线程的代码如下

- (void)stopRunLoop {
    [_thread cancelled];

    [self performSelector:@selector(stop) onThread:_thread withObject:nil waitUntilDone:NO];
}

/// 空任务唤醒线程
- (void)stop {}

空任务是为了唤醒线程,使子线程走到while循环,然后退出while循环。

RunLoop的观察者

RunLoop除了处理输入源和定时源的事件,也会生成RunLoop行为的通知。可以用Core Foundation框架注册观察者,实现对RunLoop行为的观察。使用观察者可以很清晰地知道RunLoop的行为,方便调试和实现功能。注册观察者使用C语言代码,如下

-(void)addRunloopObserver{
    //获取当前的RunLoop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    //定义一个centext
    CFRunLoopObserverContext context = {
        0,
        ( __bridge void *)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };
    //定义一个观察者
    static CFRunLoopObserverRef defaultModeObsever;
    //创建观察者
    defaultModeObsever = CFRunLoopObserverCreate(NULL,
                                             kCFRunLoopAllActivities,
                                             YES,
                                             NSIntegerMax - 999,
                                             &ObserverCallback,
                                             &context
                                             );
    //添加当前RunLoop的观察者
    CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
    //c语言有creat 就需要release
    CFRelease(defaultModeObsever);
}

/// 定义一个回调函数  RunLoop行为监听
static void ObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
}

其中的参数都是比较简单的,不在这里一一细说了_

自定义输入源

自定义输入源就比较复杂了,自己定义两个文件RunLoopSourceRunLoopContext,实现相关的功能。RunLoopSource需要实现的方法

/// 添加输入源到当前RunLoop
- (void)addToCurrentRunLoop;
/// 移除输入源
- (void)invalidate;
/// 当输入源唤醒RunLoop执行的任务
- (void)sourceFired;
- (void)fireAllCommandsOnRunLoop:(CFRunLoopRef)runloop;
/// 唤醒RunLoop
- (void)fireAllCommands;

在 RunLoopSource 实现文件创建输入源并初始化。

- (instancetype)init {
    if (self = [super init]) {
    
        CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL,
                                    NULL, &RunLoopSourceScheduleRoutine, RunLoopSourceCancleRoutine, RunLoopSourcePerformRooutine };
    
        // 初始化输入源
        runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
        commands = [[NSMutableArray alloc] init];
    }

 return self;
}

RunLoopSourceScheduleRoutine是将输入源添加到runloop的回调方法,定义如下

void RunLoopSourceScheduleRoutine(void *info, CFRunLoopRef rl, CFStringRef mode) {
}

RunLoopSourcePerformRooutine是输入源被告知时用来处理自定义数据的回调方法,定义如下

void RunLoopSourcePerformRooutine(void *info) {
}

RunLoopSourceCancleRoutine是将输入源从runloop移除的回调方法,定义如下

void RunLoopSourceCancleRoutine(void *info, CFRunLoopRef rl, CFStringRef mode) {
}

实现后相关的方法后,可以在子线程把RunLoopSource添加进去,这里使用run方法启动RunLoop

/// 添加输入源到runloop
- (void)addToCurrentRunLoop {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
    _runLoop = runLoop;
    CFRunLoopRun();
}

显式唤醒runloop,当客户端准备好处理加入缓冲区的命令后会调用此方法

- (void)fireAllCommands {
    CFRunLoopSourceSignal(runLoopSource);
    CFRunLoopWakeUp(_runLoop);
}

子线程被唤醒执行的任务

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

结束RunLoop,以退出子线程,注意,这个方法一定要在子线程里面调用

- (void)stopRunLoop {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopStop(runLoop);
}

迷之总结

使用这种自定义输入源,可以在任何时候唤醒子线程执行任务,而且RunLoop不会在执行完任务后就退出然后又进入(要用run方法启动)。当然,这个执行任务是固定的,跟时钟源以重复间隔开启RunLoop的效果很像,不过这种自定义输入源可以随便在任何时刻唤醒线程执行任务,而时钟要以一定的时间间隔。

用run方法启动RunLoop,就要用CFRunLoopStop结束RunLoop,不过苹果官方文档不推荐使用CFRunLoopStop来结束RunLoop。在我的示例代码,虽然可以结束到RunLoop,但不是马上结束的,有一定的延时,由系统来决定结束的时间,通过观察者就可以很好地观察到其行为。

经过测试,先移除RunLoop的输入源,在唤醒线程,然后线程不执行任务就直接退出RunLoop,退出线程。

示例代码已经上传到 GitHub

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

推荐阅读更多精彩内容