performSelector扩展

上一节《说一说基类 NSObject(三)
》中我们学习了NSObject类中的三个方法:

image.png

简单回忆一下。
新建一个target,类ClassA,如下所示:


image.png

实现:


image.png

测试:


image.png

看看结果:


image.png

注意截图中,备注中的提示。

从结果可以看出,方法都能够顺利调用。这是最基本的调用使用方法。

本节中,我们还将扩展一下performSelector的用法。我们发现,系统提供的三个方法,最多就带两个参数,我们有时候不仅仅是两个参数,可能是多个,该如何解决呢?

一、多个参数的使用方法

(1)利用数组传递
ClassA.h

-(void)sayHelloWithArray:(NSArray *)params;

ClassA.m

-(void)sayHelloWithArray:(NSArray *)params {
    
    NSLog(@"数组: %@", params);
}

测试

        //利用数组传递多个参数
        [a performSelector:@selector(sayHelloWithArray:) withObject:@[@"小明",@"11",@"四年级”]];

结果:


image.png

(备注:为了打印数组的汉字,我写了一个NSArray的Category,重写了-(NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level 这个方法)

实际上,这并不算真正意义上的传递多个参数,依然是传递的一个参数,不过是一个数组参数而已。

(2)给NSObject写一个分类,新建一个performSelector方法,传递多个参数。
首先,新建一个NSObject的分类。


image.png

然后,实现代码

#import <AppKit/AppKit.h>
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface NSObject (Perform)

//传递多个参数
-(id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects;

@end

NS_ASSUME_NONNULL_END

.m

//
//  NSObject+Perform.m
//  Lesson8_4
//
//  Created by wenhuanhuan on 2020/3/4.
//  Copyright © 2020 weiman. All rights reserved.
//

#import "NSObject+Perform.h”

#import <AppKit/AppKit.h>


@implementation NSObject (Perform)

-(id)performSelector:(SEL)aSelector withObjects:(NSArray *)objects {
    
    /**
     根据SEL去实例化方法签名NSMethodSignature(方法签名中有方法的名称,参数和返回值)
     */
    NSMethodSignature * sign = [[self class] instanceMethodSignatureForSelector:aSelector];
    if (sign == nil) {
        @throw [NSException exceptionWithName:@"签名异常" reason:@"没有这个方法" userInfo:nil];
        return nil;
    }
    
    //根据方法签名拿到方法的信息
    NSInvocation * invocation = [NSInvocation invocationWithMethodSignature:sign];
    [invocation setTarget:self];
    [invocation setSelector:aSelector];
    
    //签名中方法参数的个数,内部包含了self和_cmd,所以参数从第3个开始
    NSInteger paramCount = sign.numberOfArguments - 2;
    NSInteger resultParamCount = MIN(paramCount, objects.count);
    
    for (NSInteger i = 0; i < resultParamCount; i++) {
        id object = objects[I];
        [invocation setArgument:&object atIndex:i+2];
    }
    [invocation invoke];
    
    //处理返回值
    id callBack = nil;
    if (sign.methodReturnLength > 0) {
        [invocation getReturnValue:&callBack];
    }
    return callBack;
}

@end

测试:
在ClassA中再添加一个多参数的方法:


image.png
image.png

在main中进行调用测试

//重写的performSelector,多参数传递
        NSNumber * age = [NSNumber numberWithInt:20];
        NSString * name = @"小土豆”;
        NSString * gender = @“男”;
        SEL selector = NSSelectorFromString(@"sayHelloWithName:age:gender:”);
        NSArray * array = @[name, age, gender];
        [a performSelector:selector withObjects:array];

先看看打印结果:


image.png

正确打印了结果。

我们来大致看看多参数传递的方法吧。


image.png

程序执行到这个方法中,先来看看参数,aSelector和objects都是正确传递过来了。

接着往下走,


image.png

打印一下sign,看看结果:

(lldb) po sign
<NSMethodSignature: 0x100685970>
    number of arguments = 5
    frame size = 224
    is special struct return? NO
    return value: -------- -------- -------- ————
        type encoding (v) ‘v’
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 0}
    argument 0: -------- -------- -------- ————
        type encoding (@) ‘@‘
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- ————
        type encoding (:) ‘:’
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- ————
        type encoding (@) ‘@‘
        flags {isObject}
        modifiers {}
        frame {offset = 16, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 3: -------- -------- -------- ————
        type encoding (@) ‘@‘
        flags {isObject}
        modifiers {}
        frame {offset = 24, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 4: -------- -------- -------- ————
        type encoding (@) ‘@‘
        flags {isObject}
        modifiers {}
        frame {offset = 32, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}

(lldb) 

参数是5个。
这个对象中还包含返回值信息,参数详细信息(类型,内存大小等)。

继续往下走:


image.png

打印下invocation:

(lldb) po invocation
<NSInvocation: 0x100713280>
return value: {v} void
target: {@} 0x0
selector: {:} null
argument 2: {@} 0x0
argument 3: {@} 0x0
argument 4: {@} 0x0

(lldb) 

我们看到,返回值是void,目标对象target,选择器selector以及三个参数都是空的。

继续往下走,赋完值以后看看。


image.png
(lldb) po invocation
<NSInvocation: 0x100713280>
return value: {v} void
target: {@} 0x102000b70
selector: {:} sayHelloWithName:age:gender:
argument 2: {@} 0x0
argument 3: {@} 0x0
argument 4: {@} 0x0

(lldb) 

我们看到,返回值为void,目标target有值为self,selector有值为sayHelloWithName:age:gender: 。都是我们刚刚赋的值。但是参数依然为空,因为我们还没有给他赋值。

继续往下走。


image.png

再次打印invocation。

(lldb) po invocation
<NSInvocation: 0x100713280>
return value: {v} void
target: {@} 0x102000b70
selector: {:} sayHelloWithName:age:gender:
argument 2: {@} 0x100002288
argument 3: {@} 0xf85f415ba9fdb217
argument 4: {@} 0x1000022a8

参数都有值了。
执行完[invocation invoke];控制台打印出了我们想要打印的结果:


image.png

说明我们的方法是没有问题的。

注意:
因为NSArray中的元素都要求是对象,所以,依然不能直接传递值类型的参数。

这种方法相当于自己实现了performSelector,也算是解决了多参数传递的问题吧。

二、RunLoop中,performSelector的其他方法

本节中,我们继续看一看performSelector系列的其他用法,这些方法没有定义在NSObject.h中,而是在另一个文件NSRunLoop.h中。

image.png

我们来试一试。

  1. -(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
    延迟执行某个方法。

注意:
这个方法是一个异步方法,在我们的控制台main方法中调用是不会执行的。比如:


image.png

个人猜想,是因为主线程已经走完了,这个任务还没来得及执行呢,程序就结束了。
我们要在VC中进行测试。


image.png

test方法延迟了1秒执行了。

使用的时候一定要注意,如果不需要延迟的时候,就是用NSObject中定义的三个performSelector方法,比较安全。

如果把这个方法放在另一个线程中也是会出问题的。


image.png

猜猜test会执行吗?
看看结果吧。


image.png

没错儿,它不会执行的。有人说,这是因为子线程的runloop默认是不开启的,需要开启runloop才会执行,来试试看。


image.png

再看看结果:


image.png

依然没有打印test中的内容,说明没有执行。
所以在使用的时候一定要注意,不要在子线程中使用这个方法。至于为什么不执行,我没有找到答案,也欢迎大神指教(一定要有源码验证哟)。

2.- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;

此方法多了一个modes,也跟上面的方法一样,是个异步方法,放在main中,也是不会执行的。


image.png

同样,放在VC中,可以执行。


image.png

3.+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
取消执行某个延迟执行的方法。我们试一试,同样的,我们还得在VC中试。


image.png

我们有两个方法,一个是test,一个是hello,test在3秒后执行,hello在2秒后执行,然后我们又取消了test的执行,看看结果吧。


image.png

我们发现test被取消了,没有执行,hello在2秒后执行了。

4.+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
该方法与上一个方法类似,它取消的是所有的target中没有执行的方法。
为了更好的验证,我们多写几个方法,分别是3秒,2秒,0秒后执行,还有个直接执行的方法sing。然后,我们取消所有的未执行方法,看看结果。


image.png

结果如下:


image.png

测试发现,只有使用
-(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
这个方法执行的任务才会被取消,即使是0秒后执行,也是会被取消的。个人以为,是因为上面这个方法是异步的,执行速度比较慢,所以会被取消。

以上四个方法都是RunLoop中定义的,延迟执行的两个方法是异步的,使用的时候一定要注意。还有几个perform的方法,定义在NSThread中,如下图:


image.png

三、NSThread中的perform

在NSThread中,也有几个performSelector开头的方法,这几个方法又是干什么的呢?如何使用呢?我们来一起看看吧。
首先,我们来观察一下这些方法。

1.都没有返回值。
有返回值的方法就不宜使用这种方法进行调用啦。
2.都与线程有关
不管是mainthread还是background还是子线程,这些方法都是与线程有着密切的关系。
3.是NSObject分类中的方法
也就是说,继承自NSObject的任何类都是可以调用这些方法的。

我们来一一尝试吧。

1.- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
基于默认模式,在主线程进行方法的调用。
参数说明:
aSelector:选择器,就是要执行的方法名字;
arg:要传递的方法参数,对象类型。
wait:是否阻塞当前线程直到指定选择器在主线程中执行完毕。选择YES会阻塞这个线程;选择NO,本方法会立刻返回。

我们在main方法中,直接调用

    ClassA * a = [[ClassA alloc] init];
    [a performSelectorOnMainThread:@selector(sayHelloWithName:) withObject:@"小绵羊" waitUntilDone:YES];

打印结果:


image.png

此方法常用语更新UI,我们还是在APP中试一试。
我们新建一个APP测试程序,如下图所示:


image.png

类说明:
TableController: 列表VC。
TableViewModel: 列表的viewmodel,为了简化VC。
TableCell:自定义的cell。
DataModel:自定义Model。

界面用storyboard进行实现,如下图:


image.png

部分代码:

-(void)loadData {
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
       
        for (int i = 0; i<1000; i++) {
            DataModel * model = [[DataModel alloc] init];
            model.name = [self getName];
            model.className = [self getClassName];
            model.num = [NSString stringWithFormat:@"%d", (i+1)];
            [weakSelf.viewModel.datas addObject:model];
        }
        
        [weakSelf.viewModel performSelectorOnMainThread:@selector(refreshTable) withObject:nil waitUntilDone:YES];
    });
}

运行结果:


image.png
image.png

在这个例子中,
[weakSelf.viewModel performSelectorOnMainThread:@selector(refreshTable) withObject:nil waitUntilDone:YES];
这个方法就是在子线程中,回到主线程更新UI的操作。

那么performSelectorOnMainThread和dispatch_async(dispatch_get_main_queue(), ^{ })有什么区别呢?

在网上搜寻了半天,答案几乎一致,都是说他们运行的mode不一样,dispatch_async不管什么模式都能运行,更安全。而performSelectorOnMainThread只有在默认NSDefaultRunLoopMode模式下才会运行,其他模式不能运行。但是,就是没有几个完整的代码例子可以看一看到底怎么验证的。(吐槽一下,他们得出结论的时候不能顺手把代码贴出来吗?也让我们看看心服口服呀。)

扩展

RunLoop的mode: 用来控制一些特殊操作只能在指定模式下运行,一般可以通过指定操作的运行mode来控制执行时机,以提高用户体验。

系统默认注册了5个Mode:

kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行,对应OC中的:NSDefaultRunLoopMode。

UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响。

kCFRunLoopCommonModes:这是一个标记Mode,不是一种真正的Mode,事件可以运行在所有标有common modes标记的模式中,对应OC中的NSRunLoopCommonModes,带有common modes标记的模式有:UITrackingRunLoopMode和kCFRunLoopDefaultMode。

UIInitializationRunLoopMode:在启动 App时进入的第一个 Mode,启动完成后就不再使用。

GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到。

我尝试了一些方法,都没能更改当前运行的mode。写了一个带tableview的VC,一个scrollView的VC,发现当前运行的mode依然是kCFRunLoopDefaultMode。知识有限,不知道该如何验证了。如有大神知道,还请不吝赐教。

苹果官方文档截取部分解释:
This method queues the message on the run loop of the main thread using the common run loop modes—that is, the modes associated with the NSRunLoopCommonModes constant.

1.默认子线程调用,两个都能执行
注意:在APP下测试的

- (IBAction)test2:(id)sender {
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        
        NSLog(@"哎哟");
        NSLog(@"000%@", [NSRunLoop currentRunLoop].currentMode);
        
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"哈哈哈哈哈哈哈");
            NSLog(@"111%@", [NSRunLoop currentRunLoop].currentMode);
        });
        
        [self performSelectorOnMainThread:@selector(hi) withObject:nil waitUntilDone:YES];
    });
    
    
}


-(void)hi {
    NSLog(@"嘻嘻嘻嘻嘻嘻---");
}


打印结果:


image.png

2.- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;

此方法和上一个方法不一样的地方就在于后面运行的RunLoop的mode可以指定,按照网上的理论来讲,上一个方法有mode的限制,那么我们就使用这个方法来指定mode是不是比较安全呢。

    NSLog(@"------performSelectorOnMainThread,modes--------");
    [a performSelectorOnMainThread:@selector(sayHelloWithName:) withObject:@"小露珠" waitUntilDone:YES modes:@[NSRunLoopCommonModes]];

结果:


image.png

特意将mode设置成NSRunLoopCommonModes,发现也是可以正常执行的。真的不知道该如何验证不同mode下performSelectorOnMainThread这个方法不执行的问题了。

3.- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

此方法是开启了一个新的线程来执行方法。在控制台程序中,是不会执行的,这与上面的performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay类似,这里不再赘述。
代码如下:

void test5() {
    
    ClassA * a = [[ClassA alloc] init];
    if ([a respondsToSelector:@selector(sayHelloWithName:)]) {
        NSLog(@"a 响应了sayHelloWithName方法");
        [a performSelectorInBackground:@selector(sayHelloWithName:) withObject:@"小花狗"];
    } else {
        NSLog(@"不执行哟");
    }
    
}

打印结果:


image.png

我们在App中再试一下。


image.png

在App中是可以执行的。

我们使用NSThread试一试。
控制台程序:

void test5() {
    
    ClassA * a = [[ClassA alloc] init];
    if ([a respondsToSelector:@selector(sayHelloWithName:)]) {
        NSLog(@"a 响应了sayHelloWithName方法");
        [a performSelectorInBackground:@selector(sayHelloWithName:) withObject:@"小花狗"];
    } else {
        NSLog(@"不执行哟");
    }
    
    //使用NSThread试试
    //方法一
    [NSThread detachNewThreadSelector:@selector(sayHelloWithName:) toTarget:a withObject:@"小红花"];
    
    //方法二
    NSThread * myThread = [[NSThread alloc] initWithTarget:a selector:@selector(sayHelloWithName:) object:@"小叶子"];
    [myThread start];
    
}

打印结果:


image.png

依然不执行。

使用GCD再试试。

void test5() {
    
    ClassA * a = [[ClassA alloc] init];
    if ([a respondsToSelector:@selector(sayHelloWithName:)]) {
        NSLog(@"a 响应了sayHelloWithName方法");
        [a performSelectorInBackground:@selector(sayHelloWithName:) withObject:@"小花狗"];
    } else {
        NSLog(@"不执行哟");
    }
    
    //使用NSThread试试
    //方法一
    [NSThread detachNewThreadSelector:@selector(sayHelloWithName:) toTarget:a withObject:@"小红花"];
    
    //方法二
    NSThread * myThread = [[NSThread alloc] initWithTarget:a selector:@selector(sayHelloWithName:) object:@"小叶子"];
    [myThread start];
    
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"GCD这是一个子线程");
        NSLog(@"thread: %@", [NSThread currentThread]);
        [a sayHelloWithName:@"小青蛙"];
        
        testAA();
    });
}

void testAA() {
    NSLog(@"哇哈哈哈哈");
}

打印:


image.png

还是不执行呢。看来控制台执行多线程没有那么简单呢,也许是我的方法有问题,欢迎批评指正。

APP中试试。

- (IBAction)test:(id)sender {
    
    [self performSelectorInBackground:@selector(sayHiWithName:) withObject:@"小柳叶"];
    
    [NSThread detachNewThreadSelector:@selector(sayHiWithName:) toTarget:self withObject:@"小青青"];
    
    NSThread * myThread = [[NSThread alloc] initWithTarget:self selector:@selector(sayHiWithName:) object:@"小红红"];
    [myThread start];
    
}

-(void)sayHiWithName: (NSString *)name {
    NSLog(@"你好呀,我的名字是: %@",name);
}

打印结果:


image.png

都是可以执行的。猜测控制台程序和应用程序的RunLoop不一样,导致的以上的现象吧。在应用程序中,主线程的runloop是一直运行的,而控制台程序也许不是这样的。

到现在为止,还是有很多的perform开头的方法没有试验,由于篇幅有限,先到此暂停吧。

祝大家生活愉快,工作开心。
😁

本工程代码地址:
https://github.com/weiman152/iOSTestCode/tree/master/iOS/TestPerform

所有源码地址:

https://github.com/weiman152/iOSTestCode/tree/master/iOS/Lesson8-NSObject和运行时

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

推荐阅读更多精彩内容