Selector 有什么用途?

我们会在几个地方应用selector:

Target/Action pattern

Selector 的主要用途,就是实作target/action。相信你应该试过在Xcode中建立一个新专案之后,可能在Interface Builder 中建立了一个UIButton或是NSButton 控件,然后将按钮连接到controller 中宣告成IBAction 的method 上,这时候,我们的Controller 就是Button的target,而要求controller 执行的method,就叫做action。

我们在Interface Builder里头做的事情,也可以透过程式码做到。而如果我们想要设计一套系统Framework里头所没有的定制化UI 元件,第一步就是要了解怎么实例化target/action。

在UIKit 中的Target/Action 稍微复杂一些,因为同一个按钮可以一次连接好几个target 与action,我们在这边使用AppKit 示范—在Mac 上,一次只会指定单一的target 与action。如果想要产生一个按钮或是其他的custom control,我们会继承自NSView,然后建立两个成员变数:target 与action, action 是一个selector。

@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

我们在这边将target 的类型设定为id,代表的是任意Objective-C物件的指标,如同前面提到,Controller 到底是什么class,在这边并不重要,而且我们也不该将target 的class写死,因为如此一来,就变成只有某些Controller 才能使用这个按钮。

我们接着在mouseDown:中,要求target执行之前传入的action,由于selector是字串,是可以传递的参数,所以也就可以成为按钮的成员变量。

我们接下来也可以使用程式码连结target 与action,在Controller 的程式中,只要这么写即可:

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

把要做什么事情当做参数传递,每个语言都有不同的作法。Objective-C用的是拿字符串来寻找对应的实作function 指标,在C语言里头就会直接传递指标,一些更高阶的语言或着是把一段程式码当做是字串传递,要使用的时候再去evaluate这段程式码字串,或是一段代码本身就是一个实例,所以可以把程式码当做实例传递,我们称之为「匿名函式」(Anonymous Function ),现在Objective-C 也有匿名函式,叫做block,不过,对这个1983年诞生的语言来说,这是很晚近才有的功能,我们会稍晚讨论。

检查method 是否存在

前面提到,我们有可能会呼叫到并不存在的method,如果这么做就会产生错误。但我们有时候会遇到的状况是:我们并不确定某些method 到底有没有实现,如果有,就调用,如果没有,就略过或是使用其他的method。

这种状况最常遇到的就是顾及向下相容。比方说,在iOS 4之后,才开始支持Retina Display,我们在绘图的code中要决定现在应该绘制怎样精细程度的图片,需要知道目前用的是传统的一倍品质,还是Retina Display的两倍品质,就要去问UIScreenscale属性。但是,当我们开始支援iOS 4的时候,可能还要顾及iOS 3的使用者,导致我们不能够贸然直接调用scale(当然,如果你的应用程序都只支持最新版本的操作系统,那是再幸福不过的事),而是要去检查这个属性是否存在,如果没有,就代表使用者的操作系统是iOS 4之前的版本,我们只需要提供一倍品质的图片就可以了。在未来,只要遇到向下兼容,我们就还是得处理这样的状况。

另外,虽然苹果只允许iOS上面的应用程序只能够是单一的执行档,不能够在执行时载入其他的binary,但是在Mac OS X 上面却可以载入loadable bundle,或是在应用程序中放置private framework,一个物件的某些method可以不在主程式中,而是在plug-in 中实作。我们也要做这样的检查。

检查某个物件是否实例化了某个method,只要调用respondsToSelector:就可以了:

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

在其他编程语言中,也需要这样检查method是否存在吗?在Ruby语言中,有类似的respond_to?语法,至于Python,我们或着可以用dir这个funciton检查某个对象的全部attribute中是否存在对应到某个method的key,但是更常见的作法就是使用try…catch语法,如果遇到某个method可能不存在,就包在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,会导致内存泄露(Memory Leak)。

Objective-C 本身并不算有内存回收机制(Garbage Collection,以下简称GC)的语言,虽然在Mac OS X 10.5 的时代,苹果尝试在Objective-C 上实现GC,但是成果实在不甚理想,如果贸然在Mac OS X 上大量使用GC,实际运作会有严重的内存泄漏问题;苹果在推出iOS之后,也不敢将这套机制用在行动装置上,而是在iOS 5 时放弃在runtime管理记忆体,而是推出ARC(Automatic Reference Counter),在compile time时决定什么时候应该释放内存。

由于传统的Objective-C 内存管理大量使用一套叫做auto-release的机制—虽然说是auto,其实也没多自动,顶多算是半自动—将一些应该要释放的物件延迟释放,在这一轮runloop中先不释放,而是到了下一轮runloop开始时才释放这些记忆体。如果使用try…catch 捕捉例外错误,就会跳出原本的runloop,而导致应该释放的记忆体没被释放。

我们接下来还会在内存管理Part 1与Responder讨论这个部分。

Timer

NSObject除了performSelector:这个method之外,同样以performSelector开头的,还有好几组API可以调用,例如-performSelector:withObject:afterDelay:,就可以让我们在一定的秒数之后,才要求某个method执行。

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

如果时间还不到已经预定要执行的时间,method还没有执行,我们也可以反悔,取消刚才预定要执行的method,只要呼叫cancelPreviousPerformRequestsWithTarget:即可。如以下范例:

[NSObject cancelPreviousPerformRequestsWithTarget:self];

performSelector:withObject:afterDelay:的效果相当于产生NSTimer对象,当我们想要延迟呼叫某个method,或是要某件事情重复执行,都可以透过建立NSTimer达成,要使用timer,我们也必须使用selector语法。

我们先定义一个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];

除了透过指定target与selector之外,还可以透过指定NSInvocation建立NSTimer对象;NSInvocation其实就是将target/action以及这个action中要传递给target的参数这三者,再包装成一个对象。调用的method是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:1.0
                          invocation:invocation
                          repeats:YES];

请注意,在呼叫NSInvocationsetArgument:atIndex的时候,我们要传递的参数,要从2开始,因为在这边我们要想成,这是给objc_msgSend呼叫用的参数,在0的参数是对象的self,index在1的则是selector。

接收NSNotification

我们稍晚才会讨论NSNotification以及NSNotificationCenter,不过在这边先简单提到:如果我们要接收NSNotification,我们也要在开始订阅通知的时候,指定要由哪个selector处理这个通知。详见Notification Center这一章。

在某个Thread 执行method

除了已经提到的-performSelector:withObject:afterDelay:之外,NSObject还有好几个method,是让指定的selector丢到某个Thread执行,包括:

  • -performSelectorOnMainThread:withObject:waitUntilDone:modes:
  • -performSelectorOnMainThread:withObject:waitUntilDone:
  • -performSelector:onThread:withObject:waitUntilDone:modes:
  • -performSelector:onThread:withObject:waitUntilDone:
  • -performSelectorInBackground:withObject:

假如有一件事情—在这边叫做doSomething—会执行太久,我们可以将这件事情丢到背景,也就是另外建立一条Thread 执行:

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

注意,在背景执行时,这个method 的内部需要建立自己的Auto-Release Pool。

执行完毕后,我们可以透过-performSelectorOnMainThread:withObjectwaitUntilDone:,通知主Thread我们已经把事情做完了。像是,如果我们要转换一个比较大的档案,就可以在背景实际转档,转完之后,再告诉主Thread,在UI上跳出提示视窗,提示使用者已经转档完毕。

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

Array 排序

我们今天想要对NSArray做排序,就得要告诉这个Array怎样比较里头每个东西的大小,所以我们需要把怎么比较大小这件事情传递到array上。Cocoa Framework提供三种方式排序Array,我们可以把怎么比大小写成C Function,然后传递C Function的指标,现在也可以传递Block,而如果Array里头的实例有负责比较大小的method的话,我们也可以透过selector指定要用哪个method排序。

NSStringNSDateNSNumber以及NSIndexPath,都提供compare:这个method,假如有一个array里头都是字串的话,我们就可以使用compare:排序,NSString用来比较大小顺序的method与选项(像是是否忽略大小写,字串中如果出现数字,是否要以数字的大小排列而不是只照字元顺序…等等),其中最常用的,该是localizedCompare:,这个method会参考目前使用者所在的系统语系决定排序方式,像是简体中文语系下用拼音排序,繁体中文语系下用笔划排序…等等。

我们使用sortedArrayUsingSelector:产生重新排序的新Array,如果是NSMutableArray,则可以呼叫sortUsingSelector:

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

我们也可以透过传递selector,要求Array 里头每一个物件都执行一次指定的method。

[anArray makeObjectsPerformSelector:@selector(doSomething)];

代替if...else 与switch…case

因为selector 其实就是C 字串,除了可以当做参数传递之外,也可以放在array 或是dictionary 里头。有的时候,如果你觉得写一堆if…else 或是switch…case 太过冗赘,例如,原本我们可能这么写:

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

如果没有什么会超过边界的问题的话,其实可以考虑搭配Xcode 4.4之后所提供的literal新写法1,看起来就精简一些。

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

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

调用Private API

Objective-C里头其实没有真正所谓的private method,一个对象实例实现了那些method,即使没有import对应的header,我们都调用得到。系统里头许多原本就内建的class,有一些header并没有宣告的method,但是从一些相关网站或是其他渠道,我们就是知道有这些method,先不管究竟是什么原因,我们有的时候就是想要呼叫看看,这时候我们往往会用performSelector:呼叫。原因也很简单:因为我们没有header。

但我们并不建议做这样的事情:今天一个method 没有被放在header里头,就代表在作业系统改版的时候,系统可能把整个底层的实作换掉,这个method 可能就此消失,而造成系统升级之后,因为呼叫不存在的method而造成应用程式crash。而如果你打算写一套iOS 应用程序,在AppSrtore上架发售,苹果的审查过程中就会拒绝使用private API 的APP。

1
.参见
http://developer.apple.com/library/mac/#documentation/DeveloperTools/Conceptual/WhatsNewXcode/Articles/xcode_4_4.html

results matching ""

    No results matching ""