Objective-C 格致余论 1 - Selector
本页目录
Selector 作为 Objective-C 中一个非常重要的概念,虽然在 Swift 中几乎已经不见踪影,但是在某些混编的项目中仍旧起到举足轻重的作用,所以仍然有必要深入理解一下。
Selector 是什么
所谓对概念的掌握,一定是能够用简短的语言描述出核心含义。并且还要理解这个概念的应用场景和常见上下文,这才算是『掌握』。
在我的理解来看,所谓 selector,实际上是函数指针的一种实现形式,我们用一个 C string 来表示对象中的某个函数,所以就可以把这个函数作为参数,传到其他的方法中去进行调用。
为了理解 selector,就先要搞明白,Objective-C 中的 Object,或者说 Class,到底是什么。我们知道 C 语言中是没有『类』这个概念的,只有 struct,所以 Objective-C 的 Class 在编译时会变成 C struct,Class 中包含的方法也会转换成 C function。之后在运行的时候,runtime 会建立起从 Objective-C Method 到 C function 的映射(可以认为是一个 virtual table)。
举个例子,我们写了一个简单的类
@interface Wdx : NSObject {
int mode;
}
@end
编译之后会变成
typedef struct {
int mode;
} Wdx;
所以你会发现,其实用无论用 Objective-C 还是 C 的方式进行调用,都没有问题:
Wdx *wdx = [[Wdx alloc] init];
wdx->mode = 42;
具体发生了什么?其实也很简单。Runtime 会为每个类准备一个 virtual table,里面是一个个键值对,key 称为 selector,类型是 SEL,value 实际上是 C function 的函数指针,类型是 IMP。而这里的 SEL 类型实际上就是 C string,可以用下面语句来进行输出:
NSLog(@"%s", (char *)(@selector(doSomething)));
了解了这个之后,我们就会发现,实际上调用某个方法,至少有以下三种方式:
-
直接通过对象进行调用:
[myObject doSomething]; -
通过
performSelector:调用:[myObject performSelector:@selector(doSomething)];- 这里
performSelector是基类NSObject的方法
- 这里
-
使用
objc_msgSend调用:objc_msgSend(myObject, @selector(doSomething), NULL);
这里一定要清楚的意识到,其实一个类中有什么方法,是在 runtime 里决定的,既然如此,肯定有某种方法动态添加方法(比如说 category 和 swift 中的 extension,不过这里不展开)
也是因为这个机制,所以编译的时候,即使编译器没有发现类中对应的方法,也只是会发出警告而已;甚至如果使用 performSelector: 的话,连警告的不会有,只有在运行的时候才会发生 unrecognized selector sent to instance 错误导致程序崩溃。
比方说,我们可以通过 class_addMethod 方法来动态给一个类添加方法:
void myMethodIMP(id self, SEL _cmd){
doSomething();
}
// 另一个文件
# import <objc/runtime.h>
class_addMethod([MyClass class], @selector(myMethod), (IMP)myMethodIMP, "v@:");
接下来就可以这么用
MyClass *myObejct = [[MyClass alloc] init];
[myObject myMethod];
当然,直接这么做 Xcode 会给出警告的,这里只是用来介绍基本原理,实际开发中除非确实需要,否则不要这么做
Selector 用在哪
因为 selector 可以看做是函数的另一个名字,所以很多需要调用函数或者建立连接的地方,都可以用到,以下是一些具体的使用场景
Target/Action 模式
这个模式非常常用,比方说我们新建的一个按钮,通过把 controller 中对应的 IBAction 方法和它连接,整个过程就是一个 Target/Action 模式,这个 controller 是 Button 的 target,而对应执行的方法,就是 action。
所以现在我们知道,其实在 storyboard 中做得代码和界面元素的连接,实际上就是建立一个 Target/Action 模式。不过 UIKit,也就是 iOS 上的 Target/Action 会稍微复杂一些(因为可以一次建立多个),这里用 AppKit 做例子(一次建立一个),比方说我们想要让一个按钮有自定义的行为,可以这么做:
@interface MyButton : NSView
{
id target;
SEL action;
}
@property (assign) IBOutlet id target;
@property (assign) SEL action;
@end
@implementation MyButton
- (void)mouseDown:(NSEvent *)e
{
[super mouseDown:e];
[target performSelector:action withObject:self];
}
@synthesize target, action;
@end
上面代码的意思是,在鼠标按下的时候,执行之前指定的 action,所以整个绑定的过程用下面的代码:
[(MyButton *)button setTarget:self];
[(MyButton *)button setAction:@selector(clickAction:)];
我们可以看到在 Objective-C 中是用字符串来作为不同方法的标识符,在 C 语言会直接传递指针,其他抽象层级更高的语言有不同的处理方式,比如说把一段代码当做字符串传递,然后使用的时候去 evaluate,也可以把函数本身看做对象,直接像传递对象一样使用(也就是匿名函数),Objective-C 中的匿名函数实际上就是 block,不过这里不展开。
检查 method 是否存在
这里一般用来配合向下兼容,比方说我们的代码需要调用一个新版本 iOS 才有的 API,那么最好先检查下对应方法是否存在,如果不存在,则做一些额外的处理,不然程序在较低版本的 iOS 就会崩溃,具体的检测方法也很简单,如:
BOOL scale = 1.0;
if ([[UIScreen mainScreen] respondsToSelector:@selector(scale)]){
scale = [UIScreen].scale;
}
不过一般来说,在其他语言里,try catch 的方法是最常用的处理 method 是否存在的方式,不过因为 Objective-C 不算原生支持垃圾回收,try catch 可能会导致比较严重的内存泄露。好在 iOS 通过 runtime 提供了 ARC(Automatic Reference Conter) 来管理内存。
传统 Objective-C 使用 auto-release 的机制来释放内存(然而并不算太『自动』),会把要释放的内存放到下一轮 runloop 进行释放,这也是为什么不建议使用 try catch,因为实际上 try catch 是程序流中的非可控跳转,跳出了原来的 runloop,就会导致原来应该释放的内存没有释放。
Timer
另一个常见的用法是延迟调用某个方法,我们可以使用 NSObject 中的 performSelector:withObject:afterDelay: 在一定的延迟后调用某个方法,如:
[self performSelector:@selector(doSomething) withObject:nil afterDelay:1.0];
甚至还可以在方法执行之前取消方法的执行,如:
[NSObject cancelPreviousPerformRequestsWithTarget:self];
具体的实现方法可以是通过 NSTimer,如:
# timer 要做的事情
- (void)doSomething:(NSTimer *)timer
{
// Do Something
}
然后通过 doSomething: 的 selector 来建立 timer:
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:someObject
selector:@selector(doSomething:)
userInfo:nil
repeats:YES];
也可以通过 NSInvocation 来进行调用,实际上是把 target, action 和参数这三个东西包装成一个对象,然后进行调用,如:
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:1.0
invocation:invocation
repeats:YES];
注意设置参数位置的时候要从 2 开始,参数 0 是 self, 参数 1 是 selector。
NSNotification
通知的机制其实也依赖于 selector,相当于指定一个回调函数在接收到通知的时候执行对应的操作,这部分后面会专门介绍,这里就不过分展开了。
在线程中执行方法
NSObject 实际上还有很多多线程执行的方法,如:
(原图缺失)
一般来说,如果一个操作需要的时间比较多,应该放到不同线程去执行
[self performSelectorInBackground:@selector(doSomething) withObject:nil];
注意在线程中需要建立 auto-release pool,执行完毕之后通过 performSelectorOnMainThread:withObjectwaitUntilDone: 通知主线程,如
- (void)doSomething
{
@autoreleasepool{
// 在这里执行需要时间比较久的工作
[self performSelectorOnMainThread:@selector(doAnotherThing)
withObject:nil
waitUntilDone:NO];
}
}
数组排序
我们实际上也可以把一个 comparator 传给排序函数,NSString, NSDate, NSNumber, NSIndexPath 都有 compare: 方法,可以用来进行排序,比较有用的方法是 localizedCompare:,会自动根据当前语言来排序。
这里简单列出两个例子:
NSArray *sortedArray = [anArray sortedArrayUsingSelector:@selector(localizedCompare:)];
也可以利用 selector 让数组中的每个元素都做一次指定的操作:
[anArray makeObjectsPerformSelector:@selector(doSomething)];
代替 if else / switch
因为 selector 其实就是 C string,所以可以放在数组或者字典中备用,因为如此可以用来做条件选择来代替 if else 或者 switch,比如下面这个例子:
switch(condition) {
case 0:
[object doSomething];
break;
case 1:
[object doAnotherThing];
break;
default:
break;
}
// 可以改写为
[object performSelector:NSSelectorFromString(@[@"doSomething", @"doAnotherThing"][condition])];
调用私有 API
这里的私有 API,值得就是官方文档里没有说明,但是实际上可以通过 performSelector: 来调用的内部方法,不过如果打算上架的话,是不能使用的,可以平时用来测试或了解系统本身的运作机制。
注意事项
使用 selector 时也有一些需要注意的地方,比方说我们使用 super 来调用父类的方法:
[super doSomething];
会执行父类的 doSomething 方法,而如果是用
[super performSelector:@selector(doSomething)];
实际的效果等同于 [self doSomething]
优势与限制
Selector 的机制本身有其便捷性,但是反过来,也造成了一定的限制。
在 Objective-C 中,一个对象的方法都会保存在 virtual table 中,而因为这个表是在 runtime 决定的,所以其实是非常动态的,比如说不用继承就可以增加方法,或者是直接交换不同的 selector 的指向(也就是 method swizzling)。
但是因为一个 selector 能且仅能对应一个方法,所以不会有 C++, Java, C# 中的重载功能,这也是为什么 Objective-C 的函数名称普遍比较长,毕竟不能根据参数列表来具体判断要执行的函数,只能在起名字上下功夫了。如果有同一个名称的方法,那么新的会覆盖旧的。
在 Objective-C 中,我们调用某个方法,实际上是在 virtual table 中找寻对应这个 selector 的方法,而 C++ 或 Java 则是直接指定执行 vitual table 中的某个方法。问题就来了,每次函数调用都要查表,那效率肯定不会很高,这也是为什么之前 Objective-C 一直不流行的原因。
不过这样做也有好处,实际上 Objective-C 没有所谓固定版本的 runtime,只要 selector 不变,通过查表一样可以找到,新系统不用保留旧系统的库,避免了 C++ 等语言中 dll 地狱的问题
而最新的 Swift 中,实际上苹果放弃了这个做法,选择了和 C++, Java 类似的设计,这也是为什么 Swift 的性能反而会更好一些的原因,不过这样一样,就需要包含对应版本的 swift runtime 了,这也是为什么 swift 会出现更多因为版本不一致而导致的兼容问题的原因。
参考资料
相关文章
- Python 编程思想:名字、对象与资源边界2026-11-06
- RESTful API 框架 HUG 指南2017-03-09
- Cpp 编码风格指南2017-01-29
- Javascript 编码风格指南2016-11-14