【转载自:https://juejin.im/post/5c70b391e51d451646267db1】
自己平常开发中比较少用到performSelector
相关的API,但是平常看些第三方的时候,发现第三方作者用到performSelector
相关的API比较多。自己理解的是,可以在一定程度上解耦,不必引入相关类。但是最近在用到时,遇到了一些问题。由此,查看了一些博客,自己也做了验证,在此记录一下。
先看一段代码:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"1");
[self performSelector:@selector(testPerform) withObject:nil afterDelay:0];//
NSLog(@"3");
});
}
- (void)testPerform{
NSLog(@"2");
}
复制代码
执行结果如下:没有打印出2,只打印出了1和3。
看文档中对这个API的注释是说,这个方法调用后,在当前runloop里设置了一个timer,来触发这个方法执行。而当前这个方法是在子线程中调用的,在子线程中runloop不是自动创建并跑起来的,需要手动调用,才会创建。因为这个在子线程中的调用没有创建runloop,所以就没有执行testPerform
。
官方注释:
那按照官方文档说明在子线程中加入runloop,看下执行效果。
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[super touchesBegan:touches withEvent:event];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"1");
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[self performSelector:@selector(testPerform) withObject:nil afterDelay:0];//
NSLog(@"3");
});
}
复制代码
通过获取当前的runloop,系统就会返回当前的runloop,如果没有的话,会创建后返回,但是加入了runloop的时候,执行结果,依然是只有打印出来1和3,没有打印2。在[self performSelector:@selector(testPerform) withObject:nil afterDelay:0]
方法调用前后,通过控制台打印runloop对象,确实看到了调用方法后,runloop里多了一个timer源。
前后runloop对比:
有了runloop也有了触发方法testPerform
执行的timer,为什么还依然没有执行。因为runloop没有跑起来。 所以创建完runloop后,还需要runloop跑起来。【通过给当前runloop添加观察者,查看runloop的状态,runloop没有跑起来】当我们调用[runloop run];
方法后,将runloop跑起来后,testPerform
才会执行。打印结果为1,2,3。
但,问题又来了,既然加入了runloop,并且跑起来了,为什么3还会打印出来,runloop不是相当于死循环吗?循环外的3为什么会打印出来?这个问题,通过加入的runloop的观察者的打印情况可以看出来,是因为,runloop在执行完testPerform
后,就退出了。所以下边的3页打印出来了。
观察者打印:
可以看出,3是在runloop退出后,打印出来的。【在testPerform
方法内打印runloop,看到此时runloop对象的timers数组里边已经是空的了。runloop的mode里没有source1、没有source0、也没有timer源,所以就退出了】由此,也可以猜测:在runloop里设置的timer触发[self performSelector:@selector(testPerform) withObject:nil afterDelay:0]
方法后,该timer就销毁了。
怎样让runloop不退出呢?给当前runloop加入事件源或定时器temers,当前runloop就不会退出了,只是在不需要执行任务的时候进入休眠。
我在子线程中加入了timer后,通过观察者的打印结果来看,该runloop一直没有退出,所以3也就没有打印出来。【注意,repeats
参数要设置为YES,否则执行完timer之后,runloop就不再持有timer,runloop就退出来了。还可以通过[runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
加入事件源的方法,使runloop一直不退出。】
还有一个方法是- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
这个方法多了一个设置mode的参数,可以通过这个参数设置在timer在哪个mode下执行,读者可自己检测。
添加runloop观察者的代码:
- (void)addObserver
{
/*
kCFRunLoopEntry = (1UL << 0),1
kCFRunLoopBeforeTimers = (1UL << 1),2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7),128
kCFRunLoopAllActivities = 0x0FFFFFFFU
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case 1:
{
NSLog(@"进入runloop");
}
break;
case 2:
{
NSLog(@"timers");
}
break;
case 4:
{
NSLog(@"sources");
}
break;
case 32:
{
NSLog(@"即将进入休眠");
}
break;
case 64:
{
NSLog(@"唤醒");
}
break;
case 128:
{
NSLog(@"退出");
}
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);//将观察者添加到common模式下,这样当default模式和UITrackingRunLoopMode两种模式下都有回调。
self.obsever = observer;
CFRelease(observer);
}
作者:一修Grace
链接:https://juejin.im/post/5c70b391e51d451646267db1