Objective-C Class/Object 到底是什么?
你应该在其他的文件里头听说过,Objective-C 是C 语言的Superset,在C语言的基础上,加上了一层稀薄的物件导向,而Cocoa Framework 的Cocoa这个名字就是这么来的—Cocoa 就是C 加上O O。也因此,在Objective-C程序中,可以直接调用C 的API,而如果你将.m 文件后缀改改为.mm,程序里还可以和C++ 语法混编,变成Objective-C++。
Objective-C 程序在compile time 时,Compiler 其实会编译成C然后继续编译。所有的Objective-C Class 会变成C 的Structure,所有的method (以及block)会被编译成C function,接下来,在程序执行的时候,Objective-C runtime 才会建立某个C Structure 与C function 的关联,也就是说,一个物件到底有哪些method可以调用,是在runtime 才决定的。
Objective-C 物件会被编译成Structure
比方说,我们现在写了一个简单的Class,里头只有int a 这个成员变数:
@interface MyClass: NSObject {
int a;
}
@end
会被编译成
typedef struct {
int a;
} MyClass;
因为Objective-C的对象其实就是C的structure,所以当我们建立了一个Objective-C对象之后,我们也可以把这个对象当用C structure调用:
MyClass *obj = [[MyClass alloc] init];
obj->a = 10;
在Class 添加 method
在执行的时候,runtime会为每个class准备好一张表格(专用术语叫做virtual table),表格里头会以一个字符串当key,每个key会对应到C function的指标位置。Run time里头,把实作的C function定义成IMP
这个type;至于拿来当作key的字符串,就叫做selector,type定义成SEL
,然后我们可以使用@selector关键字建立selector。
而其实SEL
就是C字符串,我们可以用下面的方法来验证一下:
NSLog(@"%s", (char*)(@selector(doSomething)));
我们会顺利打印出「doSomething」这个C 字串。
对象每次调用某个method,runtime时,在做的事情,就是把method的名称当做字串,寻找与字串符合的C function实作,然后执行。也就是说,下面这三件事情是一样的:
我们可以直接要求某个对象执行某个method:
[myObject doSomthing];
或是通过过performSelector:调用
。performSelector:
是NSObject
的method,而在Cocoa Framework中所有的对象都继承自NSObject
,所以每个对象都可以调用这个method。
[myObject performSelector:@selector(doSomething)];
我们可以把performSelector:
想成台湾的电视新闻用语:如果原本的句子是「我正在吃饭」,使用performSelector:
就很像是「我正在进行一个吃饭的动作」。而其实,最后底层执行的是objc_msgSend
。
objc_msgSend(myObject, @selector(doSomething), NULL);
我们常常会说「要求某个object 执行某个methood」、「要求某个object执行某个selector」,其实是一样的事情,我们另外也常听到一种说法,叫做「对receiver 传递message」,这是沿用来自Small Talk 的术语—Objective-C 受到了Small Talk 语言的深刻影响—但其实也是同一件事。
因为一个Class 有哪些method,是在run time 时一个一个加入的;所以我们就有机会在程序已经在执行的时候,继续对某个Class 加入新method,一个Class已经存在了某个method,也可以在run time 用别的method换掉,一般来说,我们会用Category 做这件事情,不过Category是下一章的主题,会在下一章继续讨论。
我们在这里首先要记住一件非常重要的事:在Objective-C中,一个class会有哪些method,并不是固定的,如果我们在程式中对某个对象调用了目前还不存在的method,编译的时候,compiler并不会当做编译错误,只会发出警告而已,而跳出警告的条件,就是引入的header文件中到底有没有这个method而已,所以我们一不小心,就很有可能调用到了没有实现的method(或这么说,我们要求执行的selector并没有对应的实现)。如果我们是使用performSelector:
呼叫,更是完全不会有警告。直到实际执行的时候,才发生unrecognized selector sent to instance错误而导致应用crash。
之所以只有警告,而不当做编译错误,就是因为某些method有可能之后才会被加入。苹果认为你会写出调用到没有实现的selector,必定是因为你接下来在某个时候、某个地方,就会加入这个method的实现。
由于Objective-C语言中,对象有哪些method可以在run time改变,所以我们也会将Objective-C列入像Perl、Python、Ruby等所谓的动态语言(Dynamic Language)的列表中。而在写这样的动态对象导向语言时,一个对象到底有哪些method可以呼叫,往往会比这个对象到底是属于哪个class更为重要。2
如果我们不想用category,而想要自己动手写点代码,手动将某些method 加入到某个class 中,我们可以这么写。首先定义一个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];
1
.不过,如果你直接在程序中这么调用,Xcode会在编译的时候发出警告,告诉你在不久的将来会禁止这样直接调用函数的成员变量,如果想要调用成员变量,必须另外写getter /setter。
而如果这个成员变量被定义成是private的,Xcode会直接出现编译错误,禁止你这样调用。
↩2
.这种强调实例有哪些method,会比实例继承自哪个Class来得重要的观念,有一个专有名词,叫做Duck Typing,中文翻译做「鸭子型别」。
观念是:我眼前这个东西到底是不是鸭子?
它是不是鸟类或是哪个种类根本就不重要,反正它走路游泳像鸭子,叫起来像鸭子,那我就当它是鸭子。
可以参见Wikipedia上的说明:
http://en.wikipedia.org/wiki/Duck_typing
。
↩