Run Loop使用(一)

在上一节,简单的说了一下Run Loop相关的知识,这一节将做进一步练习巩固。

每个runloop对象都提供了一些主要的接口,用来添加输入源、定时器和runloop的观察者,然后启动它。每个线程都有一个与之相关联的runloop。在Cocoa框架下,使用NSRunLoop。在更低层,它使用的是CFRunLoopRef。

一、获取runloop对象

要获取当前线程的runloop,有如下两种方法:

  • 如果是Cocoa框架,使用NSRunLoop的类方法currentRunLoop获取
  • 使用CFRunLoopGetCurrent获取
    尽管它们之间不能桥接,但是你可以通过NSRunLoop的方法getCFRunLoop方法获取CFRunLoopRef类型用来沟通。因为两个对象都指向同一个runloop,所以你可以在后面的使用中按照需要决定使用哪个。
- (void)getCurrentRunLoop {
    //使用Cocoa框架
    NSRunLoop * currCocoaLoop = [NSRunLoop currentRunLoop];
    NSLog(@"Cocoa:%@",currCocoaLoop);
    //使用Core Foundation框架
    CFRunLoopRef loopRef = CFRunLoopGetCurrent();
    NSLog(@"Core Foundation:%@",loopRef);
}

打印结果就不贴了,内容太长了。

二、配置Run Loop

在你启动自己创建的线程上的runloop时,你必须添加一个输入源或者定时器。如果该runloop没有任何可监控的资源,就会立马退出。
除了添加时间源,你也可以添加runloop的观察者,使用这些观察者去检查不同时期的runloop的运行情况。要添加runloop的观察者,需要创建CFRunLoopObserverRef类型的引用对象,使用CFRunLoopAddObserver方法去添加观察者。要添加观察者必须使用Core Foundation框架。
下面是相关方法的简单说明,给runloop添加观察者,都是围绕着这个方法展开的:
void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);

1、CFRunLoopRef rl

该值的获取比较简单,大致有两种方法:
方法一:

    NSRunLoop * nsRunLoop = [NSRunLoop currentRunLoop];
    CFRunLoopRef loopRef = [nsRunLoop getCFRunLoop];

方法二:

    CFRunLoopRef loopRef = CFRunLoopGetCurrent();
2、CFRunLoopObserverRef observer

这是这三个参数里最复杂的一个。

  • 创建,有两种方法
    普通方法:CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
    block方法:CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity))

  a、allocator:内存中需要开辟的空间,一般传NULL或者kCFAllocatorDefault ,该值有6种类型,读者可以自己去详细探索;
  b、activities:该值对应于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
};

  c、repeats:标记观察者可执行的次数,因为runloop的观察者和普通的不一样,如上一节所说,它和定时器一样,可以规定其可使用次数,如果是一次,则执行完一次就会被移除,如果是可重复的,则不会被移除。
  d、order:确定当前观察者的优先级。因为每个runloop可以添加多个观察者,那么如果要想有一定的秩序,可以设置优先级。不过一般不推荐设置该值,一般都传0;
  e、callout和block,不同时期的runloop的回调和可执行块,这个,没什么好解释的。
  f、context,一般没什么用,先传NULL吧

最后的代码如下:

void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    
    NSLog(@"%@", @(activity));
}

- (void)addRunLoopObserver {
    //获取CFRunLoopRef方法一:
//    NSRunLoop * nsRunLoop = [NSRunLoop currentRunLoop];
//    CFRunLoopRef loopRef = [nsRunLoop getCFRunLoop];
    //获取CFRunLoopRef方法二:
    CFRunLoopRef loopRef = CFRunLoopGetCurrent();
    //添加观察者方法一:
//    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, true, 0, &runLoopObserverCallBack, NULL);
    //添加观察者方法二:
    CFRunLoopObserverRef observerRef = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting, true, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"%@", @(activity));
    });
    CFRunLoopAddObserver(loopRef, observerRef, kCFRunLoopCommonModes);
    if (observerRef) {
        CFRelease(observerRef);
    }
}
通过上述办法,去给主线程的runloop添加通知,你可以发现主线程的runloop一直处在运行状态,在不断的等待--结束等待--立马等待--结束等待,如下图所示:
image.png
其中结果为64的是结束等待,32表示即将进入等待,可以发现基本上处在等待期的时间较长,即使是进入结束等待状态,也会立马进入等待,也正是因为这个原因,我们在屏幕上做操作的时候能立马响应。

三、启动Run Loop

只有在自己创建的线程上才需要手动去开启runloop,因为程序的主线程的runloop默认是启动的。一般有三种方式启动runloop:

  • 无条件
    进入你的app的runloop时最简单的选择,也是最理想的。无条件的运行runloop,会把线程放进一个永久的循环中,使得你几乎没有办法控制它,你可以添加和移除输入源和定时器,但是要想停止它,就只能把线程杀死。同时你也没有办法让runloop运行在自定义的模式下。
  • 有时间限制
    比起上面的无条件运行runloop,设置一个过期时间是更好一点儿的选择。当有事件触发,就处理事件,处理完了就退出,一直到下一个事件到达,可以再启动;如果分配的时间到了或者时间过期了,你也可以重新设置过期时间然后重新启动。
  • 在一个特定模式下
    除了设置过期时间,你也可以在特定的mode下启动runloop。Mode和有效期不是相互独立或者对立的,可以一起作用于同一个runloop。

看下面的代码:

- (void)onlyRun {
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(testOnlyRun) object:nil];
    [thread start];
}

- (void)testOnlyRun {
    NSTimer * onlyRunTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runPerformSelectorForOnlyRun) userInfo:nil repeats:YES];
//    [self performSelector:@selector(runPerformSelectorForOnlyRun) withObject:nil];
}

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

这里会执行log的打印吗?
答案是不会,通过检测NSThreadWillExitNotification通知,会发现线程很快就结束了。这也从另一个侧面说明runloop默认是不会启动的。
下面一起看看怎么启动和启动的方式以及注意事项。

1、使用[[NSRunLoop currentRunLoop] run]方法

- (void)onlyRun {
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(testOnlyRun) object:nil];
    [thread start];
}

- (void)testOnlyRun {
    NSTimer * onlyRunTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runPerformSelectorForOnlyRun) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:onlyRunTimer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

- (void)runPerformSelectorForOnlyRun {
    NSLog(@"%@",[NSThread currentThread]);
}
  • 说明
    该启动方式会启动一个永久运行runloop,直到runloop里没有输入源和定时器。如果在启动过的时候就没有输入源和定时器,就会立即退出。它运行的模式是NSDefaultRunLoopMode,它会无限循环重复调用方法runMode:beforeDate:,直到满足结束的条件。
  • 问题:上面将导致无法释放问题
    如果打印日志或者断点,会发现该对象的dealloc并不会执行,如果上面的repeat为yes则会在该页面退出后依然一直会打印,如果为no,则会在结束一次后执行释放。所以可以想象一下,如果夸张的说,如果有成千上百个个线程做类似的操作,所占用的内存会有多少。怎么办呢?
尝试一:把timer改为属性或者变量,在view消失的时候,执行以下操作:
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    [onlyRunTimer invalidate];
    onlyRunTimer = nil;
    NSLog(@"%s",__func__);
}

此时,打印结果如下:
image.png

然而,你会发现,这么做只是把定时器停了,runloop进入了永久等待状态,资源并不会被释放。额,懵逼了。。。

尝试二:让线程暂停
- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    if ([thread isExecuting]) {
        [thread cancel];
    }
//    thread = nil;
    NSLog(@"%s",__func__);
}

然鹅,你发现,并没作用。。。。。。

怎么办?对不起,没办法。。。只能换方法启动runloop。对此,苹果官方如下说:
If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
where shouldKeepRunning is set to NO somewhere else in the program.

2、使用- (void)runUntilDate:(NSDate *)limitDate方法

先看代码:

- (void)runWithDate {
    NSThread * dateThread = [[NSThread alloc] initWithTarget:self selector:@selector(testRunWithDate) object:nil];
    [dateThread start];
}

- (void)testRunWithDate {
    NSTimer * runDateTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runPerformSelectorForRunWithDate) userInfo:nil repeats:YES];
    [self addObserverToRunLoop];
    [[NSRunLoop currentRunLoop] addTimer:runDateTimer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

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

打印结果为:
image.png
  • 说明:
    她会运行到指定时间,然后结束runloop。同样的,如果runloop里没有需要处理的事情,则会在启动的时候,然后立马退出runloop。其在内部也是无限调用方法runMode:beforeDate:
  • 问题:虽然比起第一种启动,它能够自动结束runloop,但是实效性还是不够,因为通过上面的log,会发现及时页面退出了,但是时间还没到,她依旧会导致当前vc不能及时释放。
其他问题:

代码如下:

- (void)runWithDate {
    NSThread * dateThread = [[NSThread alloc] initWithTarget:self selector:@selector(testRunWithDate) object:nil];
    [dateThread start];
}

- (void)testRunWithDate {
    NSTimer * runDateTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(runPerformSelectorForRunWithDate) userInfo:nil repeats:YES];
    [self addObserverToRunLoop];
    [[NSRunLoop currentRunLoop] addTimer:runDateTimer forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
}

- (void)runPerformSelectorForRunWithDate {
    for (NSInteger i = 0; i < 10000000000; i++) {
        
    }
    NSLog(@"%@",[NSThread currentThread]);
}

这里比起上面的就是多了一个for循环,只不过这次循环比较占时间,然后看打印结果:
image.png

嗯?只执行了一次,不是说好的两次吗?因为第一次runloop循环后,时间已经到了,所以不再执行了。

3、使用方法- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate

使用和方法二相同,只不过这里新增了mode的限制。
该方法的返回值是个bool值类型,比较有用,针对该返回值,官方如此说:
YES if the run loop ran and processed an input source or if the specified timeout value was reached; otherwise, NO if the run loop could not be started.
还说A timer is not considered an input source and may fire multiple times while waiting for this method to return

通过实验发现,上面说到的三个方法都没办法手动结束runloop!

解决不能释放问题,能做到及时停止
方法一:我们知道runloop的运行条件是事件源源和时间源(定时器)二者有其一时才会运行,所以是不是可以考虑移除其中的输入源呢?很遗憾,苹果明确指出不能保证。
方法二:使用方法方法- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate

首先明确的一点是方法- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate后面的代码只有在该方法有返回值后才会执行。
结合苹果提供的意见,可以做如下处理:

- (void)dealWithTimerRepeat {
    isContinue = YES;
    NSThread * dateThread = [[NSThread alloc] initWithTarget:self selector:@selector(runToDealWithTimer) object:nil];
    [dateThread start];
}

- (void)runToDealWithTimer {
    [self addObserverToRunLoop];
    [self runToStartRunLoop];
}

- (void)runToStartRunLoop {
    NSTimer * runDateTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(cycleRepeat) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:runDateTimer forMode:NSDefaultRunLoopMode];
    while (isContinue && [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) {
        NSTimer * runDateTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(cycleRepeat) userInfo:nil repeats:NO];
        [[NSRunLoop currentRunLoop] addTimer:runDateTimer forMode:NSDefaultRunLoopMode];
    }
    
}

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

log如下:
image.png

4、使用方法CFRunLoopRun()

该方法会启动一个无限循环的runloop,在有输入源和时间源的时候才会运行。但是它和方法[[NSRunLoop currentRunLoop] run];(上面的第一种启动方法)不同,该方法启动的runloop可以通过方法CFRunLoopStop(CFRunLoopGetCurrent())暂停,然后等待下次启动。runloop能够递归运行,你可以通过这个方法在一个runloop调用里创建一个嵌套的runloop并且加入当前线程的调用栈.
代码如下:

- (void)runwithCFRunLoopRun {
    isContinue = YES;
    NSThread * thread = [[NSThread alloc] initWithTarget:self selector:@selector(addTimerToCFRunLoopForRun) object:nil];
    [thread start];
}

- (void)addTimerToCFRunLoopForRun {
    NSTimer * runTimer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(logForCFRunLoopForFun) userInfo:nil repeats:YES];
    [self addObserverToRunLoop];
    [[NSRunLoop currentRunLoop] addTimer:runTimer forMode:NSDefaultRunLoopMode];
    CFRunLoopRun();
    NSLog(@"addTimerToCFRunLoopForRun");
    CFRunLoopRun();  //第二次启动
}

- (void)logForCFRunLoopForFun {
    NSLog(@"%@",[NSThread currentThread]);
    if (!isContinue) {
        CFRunLoopStop(CFRunLoopGetCurrent());
    }
}

最后的结果为:
image.png
可以发现,的确是被暂停了!!!

可能又会想,如果在第一种启动方法里,也加上这句话会怎么样呢?修改的代码如下:

- (void)runPerformSelectorForOnlyRun {
    NSLog(@"%@",[NSThread currentThread]);
    CFRunLoopStop(CFRunLoopGetCurrent());   //增加了这一行
}

但是打印结果如下:
image.png

可以发现,暂停确实有作用,但是它会立马再启动一个runloop,没完没了。

5、使用方法CFRunLoopRunResult CFRunLoopRunInMode(CFRunLoopMode mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled)

  • mode:runloop所运行的模式
  • seconds:runloop的运行时间
    指定runloop运行时间. 如果为0,在runloop返回前会被执行一次;忽略returnAfterSourceHandled的值, 如果有多个sources或者timers已准备好立刻运行,仅有一个能被执行(除非sources中有source0)
  • returnAfterSourceHandled
    判断运行了一个source之后runloop是否退出。如果为false,runloop继续执行事件直到第二次调遣结束
  • 返回值:表明了返回的原因
/* Reasons for CFRunLoopRunInMode() to Return */
typedef CF_ENUM(SInt32, CFRunLoopRunResult) {
    kCFRunLoopRunFinished = 1,   //runloop中已经没有sources和timers
    kCFRunLoopRunStopped = 2,  //runloop通过 CFRunLoopStop(_:)方法停止
    kCFRunLoopRunTimedOut = 3, //runloop设置的时间已到
    kCFRunLoopRunHandledSource = 4  //当returnAfterSourceHandled值为ture时,一个source被执行完
};

四、runloop退出

这里有两种方式退出runloop:

  • 设置超时时间
  • 告诉runloop去退出
移除事件源和定时器的方法不能保证一定会让runloop退出,因为有时候在处理一些事件的时候,系统会自动添加一些事件源和定时器,而这些事件源和定时器是不能被清除的,只能由系统清除。

五、RunLoop对象线程安全

线程安全取决于您使用哪个API来操作runloop。Core Foundation框架下的方法一般是线程安全的,可以在做跨线程调用。不过,如果你要做一些操作去修改runloop,如果有可能的话,最好还是在runloop所属的线程中操作。
Cocoa框架下的NSRunLoop和CoreFoundation框架里的相对应的方法相比,不是那么安全,如果你要使用NS RunLoop去更新你的runloop,你应该只在runloop对应的线程中做操作,如果跨线程添加事件源和定时器,会引发崩溃或者不可预知的错误。

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

推荐阅读更多精彩内容