iOS套路面试题之Selector

之前去XXXX公司面试被问到“怎样使用performSelector传入3个以上参数,其中一个为结构体?”当时年少无知,学艺不精,现在开始总结吧。

Selector

对于要讨论的Selector来说,可以做这样简短的定义

Selector就是用字符串表示某个类的某个方法

看不懂吧,我也很烦这种概念类的说法,看似高上大,看得人一头雾水。
以下是更加专业的说法:

Selector就是OC的虚拟表(virtual table)中指向实际执行的函数指针(function pointer)的一个C字符串

那问题来了,z到底是干什么用的?

因为method可以用字符串表示,因此,某个method就可以变成用来传递的参数。

要想进一步了解概念:
Selector 官方解释

Objective-C 类和对象是什么?

其他的文章和书都说,Objective-C 是用C语言做父集,在C语言的基础上,加上了一层类做导向,而Cocoa Framework 的Cocoa这个名字就是来自于C加上OO。也是因为这个Objective-C 可以直接调用C的API,而如果将.m重命名.mm, 程序里面还可以混合C++的语法,从而就变成了Objective-C ++。
Objective-C 的程序在编译时,编译器会编译成C然后再继续编译。所有的Objective-C 类会变成C的结构体,所有的方法(以及block)会被编译成C function,接下来,在执行的时候,Objective-C的runtime才会建立某个C Structure与C function的关联,也就是说,一个类到底有哪些方法可以调用,是在运行时候(runtime)才决定的。
为什么

Objective-C的对象会被编译成结构体(Structure)?

例如,我们现在写一个最简单的类,里面只有 int i这个成员变量:

@interface SelectorClass : NSObject {
    int i;
}
@end”

会被编译为:

typedef struct {
    int a;
} SelectorClass;

因为Objective-C的对象本质就是C的结构体,所以当我们建立一个Objective-C的对象之后,我们就可以把这个对象当做调用C的结构体的调用:

SelectorClass *obj = [[SelectorClass alloc] init];
obj->a = 10;

如何向类中加入方法(method)?

在执行的时候,runtime会为每一个类准备好一个虚拟表(专业术语叫virtual table),虚拟表里面会以每一个字符串当做key,每一个key对应到C function的指定的位置。Runtime里面,把实现C function定义成IMP(具体的方法地址)这个类型(type);至于拿来当做key的字符串,就叫做selector,类型(type)定义成SEL(方法名称的描述),然后我们就可以使用@selector关键字建立selector。

《iOS套路面试题之Selector》 Selector示意图

其实SEL就是C语言字符串,我们可以来写一个简单的程序验证一下:

NSLog(@"%s", (char *)(@selector(doSomething)));

《iOS套路面试题之Selector》 顺利印出「doSomething」这个 C 字串

每次对一个对象调用某个方法,runtime在做的事情,就是把方法的名称作为字符串,寻找与字符串符合的C function实现,然后执行。也就是说,以下三件事是一样的:
我们可以直接要求某个对象执行某个方法:

[myObject doSomthing];

或者是通过performSelector:调用。performSelector:是NSObject的方法,而Cocoa Framework中所有的对象都继承自NSObject,所以每一个对象都可以调用这个方法。

[myObject performSelector:@selector(doSomething)];

我们可以把以上这个程序看做一句话【我们正在吃饭】,使用performSelector:就像是【我们正在进行一个吃饭的动作】。
而其实,底层执行的却是objc_msgSend

objc_msgSend(myObject, @selector(doSomething), NULL);

看其他的书或者文章,常常看到这句话【要求某个类执行某个方法】、【要求某个类执行某个selector】,其实都是一样的事情,我也常见过另外一种写法,叫做【对接受者(receiver)传递消息(message)】。因为一个类有些方法,是在runtime一个一个加入的;所以我们就有机会在程序已经执行的时候加入,继续对某个类加入新方法,一个类已经存在某个方法,也可以在runtime(运行时)用别的实现换掉,一般来说,我们会用Category做这件事。
重要的是:在Objective-C中,一个类会有哪些方法,并不是固定的如果我们在程序中对某个对象调用目前还不存在的方法,编译的时候,编译器并不会当做编译错误,只是会发出警告而已,而弹出警告的条件,也就只有是否会引入的头文件(header)中到底有没有这个方法而已,所以我们一不小心,就很可能调用没有实现的方法(或者换种说法,我们要求执行的selector并没有对应的实现)。如果我们是使用performSelector:调用,更是完全不会警告。直到实际执行的时候,才会发生unrecognized selector sent to instance错误而导致的应用程序的crash。之所以是警告,而不是当做编译错误,就是因为某个方法有可能之后才会被加入。苹果认为你会写出调用到没有实现的selector,必定是因为你接下来在某个时候、某个地方,就会加入这种方法的实现。
由于Objective-C语言中,类有哪些方法可以在runtime改变,所以我们也会将Objective-C 列入像是“ Perl、Python、Ruby等所谓的动态语言(Dynamic Language)之类。而在这种动态类导向的语言,一个类到底有哪些方法可以调用,往往会比这个对象到底属于哪些类更加重要。
如果我们不想用category,而想要自己动手写点程序,手动将这些方法加入到某个类中,我们就可以这么写。首先先声明一个C function,至少要有两个参数,第一个参数是执行method的对象,第二个参数是selector,像是这样:

void myMethodIMP(id self, SEL _cmd) {
    doSomething();
}

接下来可以调用class_addMethod加入selector与实现的封装。

“#import <objc/runtime.h>
// 此处省略。。。。。
class_addMethod([MyClass class], 
@selector(myMethod), (IMP)myMethodIMP, "v@:");

接下来就可以调用了:

MyClass *myObject = [[MyClass alloc] init];
[myObject myMethod];

那么:

Selector 有何用处呢?

1.Target/Action 模式

Selector的主要用途,就是实现target/action。在Xcode新建一个工程,在xib中建立一个UIButton对象,然后将按钮连线到controller中声明IBAction的方法上,这时候,Controller就是Button的target,而要求Controller执行的方法就叫做action。
如果我们要设计一个程序里没有的UI控件,第一步就是要了解怎么实现target/action。
在UIKit中的Target/Action 稍微复杂一点,因为同一个按钮可以一次连线好多个target和action。如果想要产生一个按钮或者其他的自定义控件,我们会继承自UIView,然后建立两个成员变量:target与action,action是一个selector。

@interface MyButton : UIView
{
    id target;
    SEL action;
}
@property (assign) IBOutlet id target;
@property (assign) SEL action;
@end

@implementation MyButton
- (void)mouseDown:(UIEvent *)e
{
    [super mouseDown:e];
    [target performSelector:action withObject:self];
}
@synthesize target, action;
@end

这里将target的类型特别设置为id,代表的是任意Objective-C对象的指标,如同:Controller到底是什么类,这里来说并不重要,而且这里也不该将target的Class写死,因为如此一来,就变成某个Controller才可以使用这些按钮。
接着在mouseDown:中,要求target执行之前传入action,由于selector是字符串,是可以传递参数,所以也就可以成为按钮的成员变量。
接下来就可以使用代码连接target与action,在Controller的程序中,按照以下写法即可:

[(MyButton *)button setTarget:self];
[(MyButton *)button setAction:@selector(clickAction:)];

把要做什么事情当做参数传递,每个语言都有不一样的做法。Objective-C用的是拿字符串来寻找对应的实现函数的指针,在C语言中就会直接传递指针,一些更高级的语言或者就会把一段代码当做字符串传递,要使用的时候再去评估(evaluate)这段代码字符c串,或者是一段代码本来就是一个对象,所以可以把代码当做一个对象传递,这就是所谓的【匿名函数】(Anonymous Function),Objective-C的匿名函数当然是block了。

检查方法是否存在?

有时可能调用到并不存在的方法,如果这样做就会产生错误。但是很多时候遇到的问题是:我们并不很确定某些方法到底有没有实现,如果有,就实现,如果没有,就略过或者使用其他的方法。
最常见的问题就是顾忌向下兼容的问题:比如不同的iOS版本间,高版本使用的方法,低版本可能没有。这样的话可能就要做检查。
检查某个对象是否运行了某个方法,只要调用respondsToSelector:即可:
比如:

BOOL scale = 1.0;
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]) {
    scale = [UIScreen mainScreen].scale;
}

在其他语言中,也需要这样检查method是否存在吗?在Ruby语言中,类似 的respond_to?语法,至于Python,我们可以用dir这个funciton检查某个对象的全部attribute 中是否存在对应到某个方法中的key,但是更常见的做法就是使用try…catch 语法,如果遇到某个方法可能不存在,就包在 try…catch的block中,像是:

try:
    myObject.doSomething()
except Exception, e:
    print "The method does not exist.

在Objective-C中,同样也有try…catch 语法,在很多语言中,善用try…catch也可以将程序写的条理清晰,但是Objective-C语言中不鼓励使用。原因是与Objective-C的内存管理机制有关系,如果大量使用 try…catch,会导致内存泄漏
Objective-C是没有垃圾回收机制(Garbage Collection,简称GC)的语言,在iOS 5的时候苹果放弃使用runtime管理内存,而是推出ARC(Automatic Reference Counter),在编译时决定什么时候释放内存。
由于传统的 Objective-C内存管理大量使用一套叫做auto-release的机制,虽然说是auto(自动),其实也没有多自动,顶多算是半自动–将一些应该释放的对象延迟释放,在这一轮的runloop中先不释放,而是到了下一轮的runloop开始时才释放这些内存。如果使用try…catch 捕捉错误,就会跳出原本的runloop,而导致该释放的内存没有被释放。

Timer是什么?

NSObject 除了performSelector:这个方法之外,同样可以performSelector开头的,还有好几组API可以调用,例如:

performSelector:withObject:afterDelay:

就可以让我们在一定的秒数之内之后,才要求某个方法执行。

[self performSelector:@selector(doSomething) 
withObject:nil afterDelay:2.0];

如果时间还不到已经预定要执行的时间,方法还没执行,我们也可以取消刚才预定要执行的方法,只要调用cancelPreviousPerformRequestsWithTarget:即可,如以下代码:

[NSObject cancelPreviousPerformRequestsWithTarget:self];

performSelector:withObject:afterDelay:的效果相当于新建NSTimer对象,当我们想要延长调用某个方法的时候,或者是要某件事情重复执行的,都可通过建立NSTimer对象实现,要使用Timer,也就必须使用selector语法。
首先要告诉一个timer要做的事情:

- (void)doSomething:(NSTimer *)timer
{
    // Do something
}

然后通过“`doSomething:“的selector 建立timer

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                          target:someObject
                          selector:@selector(doSomething:)
                          userInfo:nil
                          repeats:YES];


除了可以通过指定的 target与selector 除外,还可以通过指定NSInvocation来调用新建的NSTimer对象;NSInvocation其实就是将 target/action以及整个 action 中的要传递给target的参数这三者,再包装成一个类。调用的方法是scheduledTimerWithTimeInterval:invocation:repeats:.
通过建立NSInvocation对象建立timer的方式如下。

NSMethodSignature *sig = [MyClass
instanceMethodSignatureForSelector: @selector(doSomething:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setTarget:someObject];
[invocation setSelector:@selector(doSomething:)];
[invocation  setArgument:&anArgument atIndex:2];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0
                          invocation:invocation
                          repeats:YES];

注意,在调用NSInvocationsetArgument:atIndex的时候,我们要传递的参数,要从2开始,因为在这想象成:这是给““objc_msgSend“`调用参数,在0的参数是对象的self,位置在1的是selector。

接收 NSNotification?

接下来的面试套路再写
如果我们要接收NSNotification,我们也要在开始的时候注册通知,指定由哪一个 selector 处理通知。

如何在某个线程中执行方法?

除了-performSelector:withObject:afterDelay:之外,NSObject还有好几个方法,是让指定的selector丢到某个线程中执行,包括:

-(void)performSelectorOnMainThread:
(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait

-(void)performSelectorOnMainThread:
(SEL)aSelector withObject:(id)arg waitUntilDone:
(BOOL)wait modes:(NSArray<NSString *> *)array
-(void)performSelector:(SEL)aSelector 
onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait
-(void)performSelector:(SEL)aSelector
 onThread:(NSThread *)thr withObject:(id)arg 
waitUntilDone:(BOOL)wait modes:(NSArray<NSString *> *)array
-(void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg

假如有一件事情,暂且称为doSomething,它会执行的时间有点长,这时候我们就可以把这件事情丢到Background执行,也就是另外新建一条线层执行:

[self performSelectorInBackground:
@selector(doSomething) withObject:nil];

注意,在Background执行执行的时候,这个方法的内部要建立自己的自动释放池(AutoRelease Pool)。
执行完成后,可以通过-performSelectorOnMainThread:withObjectwaitUntilDone:,通知主线程,事情已经做完了,就好像,如果要转换一个比较大的文件,就可以在Background实现转换,转换完之后,再告诉主线程,在UI上跳出提示视窗,提示使用者已经转换完毕。

- (void)doSomthing
{
    @autoreleasepool {
        // Do something here.
        [self performSelectorOnMainThread:@selector(doAnotherThing)
              withObject:nil
              waitUntilDone:NO];
    }
}

Array排序

要做数组排序,就得先告诉这个数组里面每一个元素的大小,所以我们要把怎么比较大小这个事件传递到array上, Cocoa Framework 提供了三种方式排序数组,我们可以吧如何比较大小写成C Function然后传递给C Function的指针,现在可以传递给Block,而如果是数组里面的对象有负责比较大小的方法,也可以通过selector 指定用哪一个方法排序。
NSString、NSDate、NSNumber 以及 NSIndexPath,都提供 compare:这些方法,假如有一个数组里面都是字符串的话,就可以用compare:排序,NSString用来比较大小顺序的方法与选项(像是是否忽略大小写,字符串中如果出现数字,是否要以数字的大小排列而不是只照字母顺序。。。等等),其中最常用的:localizedCompare:,这个方法会参考目前使用者所在的系统语言而决定排列方式,像是简体中文下的用拼音排序,繁体中文会用笔画排序。。。等等。
在使用sortedArrayUsingSelector:产生重新排序的新的数组,如果是NSMutableArray(可变数组),则可以调用sortUsingSelector:

NSArray *sortedArray = [anArray sortedArrayUsingSelector:
                                 @selector(localizedCompare:)];

也可以通过传递selector,要求数组里面的每一个对象都执行一次指定的method。

[anArray makeObjectsPerformSelector:@selector(doSomething)];

代替if…else与switch…case

因为 selector 其实就是C的字符串,除了可以作为参数传递之外,也可以放在array或者是dictionary里面。有的时候,如果你觉得写一堆的if…else与switch…case太长太丑太low,例如,原来我们这样写:

switch(condition) {
    case 0:
        [object doSomething];
        break;
    case 1:
        [object doAnotherThing];
        break;
    default:
        break;
}

可以换成:

[object performSelector:NSSelectorFromString(@[@"doSomething",
    @"doAnotherThing"][condition])];

我们可以使用NSStringFromSelector,将selector转换成NSString,反之,也可以使用NSSelectorFromString将NSString换成selector。

….呼叫私有API

其实吧,Objective-C 里面其实没有真正所谓的私有方法,一个对象实现哪些方法,及时没有import对应的头文件,我们也可以调用的到。系统里面有许多原来就内建的class,有一些头文件并没有声明方法,但是从一些相关的网站或者其他途径,我们就是知道这些方法,我们想调用看看的时候,这时候我们往往会用performSelector调用:因为我们没有头文件。
但是苹果一般不建议这么做,今天一个方法没有放到头文件里面,就代表在做系统升级的时候,系统可能吧整个底层的实现替换掉,这个方法可能就此消失了,而造成了系统升级之后,系统调用不存在的方法而造成的程序crash。而且在上架的时候,苹果的审查会拒绝使用私有API的软件。
**以上都是说明selector是啥,下面开始写面试问的performSelector: **

呼叫 performSelector:需要注意的地方

1.对super呼叫performSelector:

对于一个对象调用某个方法,或者是通过performSelector:调用,意思是一样的,但是对于super的调用,却有不一样的结果。如果是:

[super doSomthing];

代表的是调用super的doSomthing实现。如果是:

[super performSelector:@selector(doSomething)];

调用的是super的performSelector:,最后结果仍然等同于[self doSomething].

Selector是 Objective-C 中所有的所有的开始

Objective-C 对象有哪些方法,就是这个对象的类中virtual table(虚拟表)中,有多少selection/C function pointer的pair,
这个特性造就成了在Objective-C 中可以做很多神奇的事情,同时也造成了Objective-C 这门语言的限制。
Objective-C 的神奇之处,就在于,既然对象有哪些方法可以在runtime决定,因此每个对象也都可以在运行时(runtime)改变。我们可以在不用继承对象的情况下,就新增加新的方法,通常最常见的方法就是category,我们也可以随时把既有的selector指到不同的 C function pointer 上,像是把两个selector所指定的function pointer交换,这种交换selector 实现的做法叫做method swizzling(方法交叉)。
由于一个selector只会指向一个实现,因此,Objective-C不会有C++、 Java、C#等语言当中的overloading(重载)。所谓的overloading,就是可以允许有很多名称相同,但是参数类型不同的function或者method存在,在调用的时候,如果传入指定类型的参数,就会调用该参数类型的那一组function 或 method。在Objective-C 当中,同一个名称的方法,就会以最后在runtime载入的哪一组,代替之前的实现。
Objective-C 的virtual table像一个dictionary,而C++、Java 等语言的virtual table则是一个array。在Objective-C中,我们调用[someObject doSomthing]时候,我们其实就是在表格中寻找符合“doSomthing”这个字符串的方法,在 C++ 或 Java 中,我们调用someObject.doSomething()时候,在做的事情大概的要求就是【执行virtual table 中第八个方法】。
由于没做一次方法的调用,都是在做一次 selector/function pointer 的查表。与其他的静态语言相比较,这种查表的动作比较耗时,因此执行率也比不上C++等程序语言。但是Objective-C 其实有一个其他的优点:我们不需要连接特定的版本的runtime与libraries,就算是libraries中export出来的 function换了位置,只要selector 不变,还是可以找到应该要执行的 C function,所以旧的版本的App在新版本的操作系统上执行时,新版本操作系统并不需要保留旧版的Libraries,而避免C++等语言等语言中的所谓的DLL Hell问题。
2014年,苹果推出Swift,说Swift是比 Objective-C更快、执行效率更高的语言,Swift之所以效率比Objective-C更高,是因为swift又改变了virtual table的实现,看起来更像是 C++、Java 等语言的virtual table 设计。因为Swift也有必须链接指定版本的runtime问题,所以在每一个Swift App中的App bundle中,其实都包含一份Swift runtime。

    原文作者:Miu七七
    原文地址: https://www.jianshu.com/p/40880fdb749c
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞