深入理解RunLoop

当你试图解决一个你不理解的问题时,复杂化就产生了。—— AndyBoothe

**RunLoop: **顾名思义也就是循环运行的意思。做iOS 的同学都会接触到这个概念,但是真正用上的却不是很多。在这里,我将结合以往的一些经验及实践来谈谈我对RunLoop的理解。

一、 为什么会存在RunLoop

官方RunLoop模型图

我们都知道,oc是一种面向对象的语言,但是代码的执行终究还是面向过程的,也就是说会有始有终。而线程也是一样的,我们的线程从创建到运行再到销毁也是会存在一个生命周期的。在项目开发中,有时候会存在对持续异步任务的需求,那么我们就需要来维护特定线程的生命周期,这时就该轮到RunLoop上场了。说白了,RunLoop就是来保证你的线程以一种环形的结构运行下去,在需要的时候唤醒,不需要的时候让线程进入休眠状态,从而来减少对CPU的开销。

二、RunLoop与线程的关系

在我们的main.m文件里会有这样的一段代码:

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

当我们的程序启动后,上面的代码就会被调用,主线程也就开始执行。大家一定注意到了,我们的主线程是一直存在的,所有的视图、控件的操作以及事件链的监听都是在主线程下进行的,直到APP退出。所以可以推测出,当主线程被创建时,必然存在一个RunLoop来维护它的生命周期,保证后面程序的运行。线程与RunLoop可以说是一种线性的关系(一对一),除主线程的RunLoop会被自动创建,并运行在默认模式外,子线程的RunLoop是需要我们手动来创建的。

三、认识RunLoop

NSRunLoop是Cocoa框架中的类,与之对应,在Core Fundation中是CFRunLoopRef类。这两者的区别是前者不是线程安全的,而后者是线程安全的。
这里我们先从CFRunLoopRef中来剖析一下RunLoop的结构。在CoreFoundation里面有关于RunLoop的5个类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

刚才也提高过线程与RunLoop是一一对应的关系,而在RunLoop里会存在若干个Mode,每个Mode下又会存在若干个Source、Timer、Observer(观察者)。

RunLoop的相关类关系图

Run Loop Mode主要定义有以下几种:

NSDefaultRunLoopMode: 大多数工作中默认的运行方式。

NSConnectionReplyMode: 使用这个Mode去监听NSConnection对象的状态,我们很少需要自己使用这个Mode。

NSModalPanelRunLoopMode: 使用这个Mode在Model Panel情况下去区分事件(OS X开发中会遇到)。

UITrackingRunLoopMode: 使用这个Mode去跟踪来自用户交互的事件(比如UITableView上下滑动)。

GSEventReceiveRunLoopMode: 用来接受系统事件,内部的Run Loop Mode。

NSRunLoopCommonModes: 这是一个伪模式,其为一组run loop mode的集合。

每一次运行自己的Run Loop时,都需要显示或者隐示的指定其运行于哪一种Mode。Run Loop运行时只能以一种固定的Mode运行,并监控这个Mode下添加的Timer source和Input source。如果这个Mode下没有添加事件源,Run Loop会立刻返回。

Run Loop从两个不同的事件源中接收消息:

Input source用来投递异步消息,通常消息来自另外的线程或者程序。在接收到消息并调用程序指定方法时,线程中对应的NSRunLoop对象会通过执行runUntilDate:方法来退出。

Timer source用来投递timer事件(Schedule或者Repeat)中的同步消息。在处理消息时,并不会退出Run Loop。Run Loop还有一个观察者Observer的概念,可以往Run Loop中加入自己的观察者以便监控Run Loop的运行过程。

Input source有两个不同的种类: Port-Based Sources 和 Custom Input Sources:Port-Based Sources由内核自动发送,Custom Input Sources需要从其他线程手动发送。

Cocoa框架为我们定义了一些Custom Input Sources,允许我们在线程中执行一系列selector方法:

1.在主线程的Run Loop下执行指定的 @selector 方法

performSelectorOnMainThread:withObject:waitUntilDone:

performSelectorOnMainThread:withObject:waitUntilDone:modes:

2.在当前线程的Run Loop下执行指定的 @selector 方法

performSelector:onThread:withObject:waitUntilDone:

performSelector:onThread:withObject:waitUntilDone:modes:

3.在当前线程的Run Loop下延迟加载指定的 @selector 方法

performSelector:withObject:afterDelay:

performSelector:withObject:afterDelay:inModes:

4.取消当前线程的调用

cancelPreviousPerformRequestsWithTarget:

cancelPreviousPerformRequestsWithTarget:selector:object:

以下是在CFRunLoopRef下添加Sources和Observer的方法:

- (void)runDefaultLoop {
    
    CFRunLoopSourceContext context = {0, (__bridge void *)(URLConnection), NULL, NULL, NULL, NULL, NULL, ScheduleCallBack, CancelCallBack, PerformCallBack};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    while (KRunAlways) {
        @autoreleasepool {
            CFRunLoopRun();
        }
    }
}

void ScheduleCallBack(void *info, CFRunLoopRef rl, CFRunLoopMode mode)
{  
}
void CancelCallBack(void *info, CFRunLoopRef rl, CFRunLoopMode mode)
{  
}
void PerformCallBack(void *info)
{
}

四、RunLoop的使用

1.获取当前线程的RunLoop:有则获取,无则创建

+ (NSRunLoop *)currentRunLoop;

2.获取主线程的RunLoop

+ (NSRunLoop *)mainRunLoop ;

3.获取RunLoop的CFRunLoopRef对象

- (CFRunLoopRef)getCFRunLoop;

4.将定时器添加到runloop中

- (void)addTimer:(NSTimer *)timer forMode:(NSString *)mode;

5.添加输入源端口到runloop中,NSPort对象可以理解为详细的载体,会传递消息与其代理。

- (void)addPort:(NSPort *)aPort forMode:(NSString *)mode;

6.将某个输入源端口移除

- (void)removePort:(NSPort *)aPort forMode:(NSString *)mode;

7.开始运行

- (void)run;

8.在某个期限前运行

- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

五、RunLoop的应用

CFRunLoopRef的作用主要还是用在对于消息的监听上面,所以这里主要讲的是关于NSRunLoop的应用场景。

1.创建一个与APP生命周期相同的子线程(不太推荐)

- (id)init{
    if (self = [super init]) {
        mdapThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        mdapThread_.name = @"MdapThread";
        isThreadNeedRun = YES;
        conditionLock_ = [[NSConditionLock alloc] init];
        
        [mdapThread_ start];
    }
    
    return self;
}
- (void)run{
    // 为runloop 加入输入源
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
}

2.维护线程的生命周期,让线程不主动退出

- (id)init{
    if (self = [super init]) {
        mdapThread_ = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        mdapThread_.name = @"MdapThread";
        isThreadNeedRun = YES;
        conditionLock_ = [[NSConditionLock alloc] init];
        
        [mdapThread_ start];
    }
    
    return self;
}
- (void)run{
    // 为runloop 加入输入源
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    while (isThreadNeedRun) {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
}
**注意:在这里如果输入源不存在可能会造成线程的循环空转,造成CPU的浪费**

3.阻塞线程

(void)handleRunLoopThreadButtonTouchUpInside
{

NSLog(@"Enter handleRunLoopThreadButtonTouchUpInside");

self.runLoopThreadDidFinishFlag = NO;

NSThread *runLoopThread = [[NSThread alloc] initWithTarget:self selector:@selector(handleRunLoopThreadTask) object:nil];

[runLoopThread start];

//在这里如果self.runLoopThreadDidFinishFlag不为YES,则  NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside”);代码是不会执行的,我们就可以在handleRunLoopThreadTask方法里执行我们想要的操作了

while (!self.runLoopThreadDidFinishFlag) {

NSLog(@"Begin RunLoop");

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

NSLog(@"End RunLoop");

}

NSLog(@"Exit handleRunLoopThreadButtonTouchUpInside");

}

4.在一定时间内监听某种事件,或执行某种任务的线程

NSTimer*udpateTimer=[NSTimer timerWithTimeInterval:30

target:self

selector:@selector(onTimerFired:)userInfo:nil

repeats:YES];

[NSRunLoopcurrentRunLoop] addTimer:udpateTimerforMode:NSRunLoopCommonModes];

注意:NSTimer的初始化有两种scheduledTimerWithTimeInterval和timerWithTimeInterval。在使用scheduledTimerWithTimeInterval进行初始化时,它是会被自动的添加到NSDefaultRunLoopMode这种模式下的。而使用timerWithTimeInterval初始化时则需要我们来手动的添加Mode。那么为什么会有这两种情况呢?不知道大家有没有遇到过这样的情况,就是当NSTimer运行在NSDefaultRunLoopMode模式下,如果我们在滑动页面如UIScrollView或UITableView时,定时器的方法是不执行的。这是因为苹果公司为了增加用户的体验感,在用户进行滑动操作时,会将主线程的RunLoop模式切换到UITrackingRunLoopMode下,UITrackingRunLoopMode的优先级高于NSDefaultRunLoopMode,所以定时器方法会延缓执行。为了避免这种错误的发生,在我们初始化NSTimer时,可以选择将其放入UITrackingRunLoopMode或NSRunLoopCommonModes模式下。

5.避免APP的崩溃
我们可以在自定义的错误捕捉方法里,添加这样一段代码来处理app崩溃事件,可以有效的阻止app奔溃。(关于具体的实现方法,有兴趣的同学可以看看我在简书里的另一篇关于崩溃捕获的博客)

CFRunLoopRef runLoop = CFRunLoopGetCurrent();

CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);

while (!_isDismisssed) {

for (NSString *mode in (NSArray *)allModes) {

CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);

}

}

CFRelease(allModes);

另外附送上CFRunLoop的源码地址,有兴趣的同学可以自行下载。

CFRunLoop的源码地址

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

推荐阅读更多精彩内容

  • RunLoop的定义与概念RunLoop的主要作用main函数中的RunLoopRunLoop与线程的关系RunL...
    __silhouette阅读 964评论 0 6
  • 转载:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling阅读 1,418评论 0 13
  • http://www.cocoachina.com/ios/20150601/11970.html RunLoop...
    紫色冰雨阅读 814评论 0 3
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技术 RunLoop 是 iOS 和 ...
    橙娃阅读 821评论 1 2
  • 我们会有小小的家。城市的一方烟火,容纳着温暖的阳台,昼夜的情话和蠢萌的猫狗。你从心上人,变成枕边人。我稚嫩地牵着你...
    空兰山阅读 176评论 0 0