在上一节,简单的说了一下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一直处在运行状态,在不断的等待--结束等待--立马等待--结束等待,如下图所示:
其中结果为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__);
}
此时,打印结果如下:然而,你会发现,这么做只是把定时器停了,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]);
}
打印结果为:- 说明:
她会运行到指定时间,然后结束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循环,只不过这次循环比较占时间,然后看打印结果:嗯?只执行了一次,不是说好的两次吗?因为第一次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如下: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());
}
}
最后的结果为:可以发现,的确是被暂停了!!!
可能又会想,如果在第一种启动方法里,也加上这句话会怎么样呢?修改的代码如下:
- (void)runPerformSelectorForOnlyRun {
NSLog(@"%@",[NSThread currentThread]);
CFRunLoopStop(CFRunLoopGetCurrent()); //增加了这一行
}
但是打印结果如下:可以发现,暂停确实有作用,但是它会立马再启动一个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对应的线程中做操作,如果跨线程添加事件源和定时器,会引发崩溃或者不可预知的错误。