介绍
Objective-C与C语言不同的就是增加了面向对象的特性。然而具有面向对象的它由于Runtime系统的支持,使得和传统的面向对象语言例如C++,java又有很大的不同。在传统面向对象语言中,大部分的面向对象特性是由编译器完成的,而Objective-C把绝大部分工作延迟到了运行时去决定。例如,在传统的面向对象语言中,让某个对象执行某个方法,通常叫做方法调用(method invocate)。而在Objective-C通常不能直接调用方法,而是通过消息发送(messaging send)的形式,给对象发消息,在程序运行过程中由Runtime系统来分析该消息,确定消息对应方法所在的地址,然后调用方法。因此Objective-C相比于传统面向对象语言具有高度的灵活性和动态性。
消息传递
在Objective-C中我们最常见的消息发送语句就是如下形式:
[shape draw];
这里我们将它称之为消息表达式(message expression)。在编译阶段,编译器将这种消息表达式转换为对应的消息传递函数(message function), objc_msgSend
。此函数接受两个参数,第一个为消息发送的对象,在以上表达式中为shape
;第二个参数为消息选择器(message selector),在以上表达式中为@selector(draw)
;然后通过objc_msgSend
调用,由运行时系统确定实际方法所在地址,最后执行对应的方法。
objc_msgSend(shape, @selector(draw));
当然,如果发送的消息带有参数的时候:
[shape drawA: 80 B: 90 ...];
编译器会将其消息表达式转换成如下的消息传递函数
objc_msgSend(shape, @selector(draw), A, B, ...);
消息传递函数为动态绑定做了如下内容:
- 由于同样的消息可以有不同的实现形式,消息选择器首先根据消息的接受对象,找到消息选择器所指向的方法具体实现地址。
- 找到实现方法之后,调用其实现方法并将接受者作为self指针参数以及剩下的消息参数传递给实现方法。
- 最后将实现方法的返回值作为原消息返回值返回调用结果。
在上例中objc_msgSend
查找@selector(draw)
消息选择器的具体实现地址,找到后调用实现方法并将指向shape
对象的指针self
以及其余参数A, B, ...
传递给实现方法。最后实现方法返回值是void
。于是将其返回值(void代表无法返回值)作为消息发送的返回值返回给最初的表达式结果。
PS: 以上所有机制千万自己手动实现,这是在介绍原理,编译器会帮你生成对应的消息传递函数。
读到这里或许你有了疑问。如何将消息选择器对应具体实现方法的地址?如果子类中没有对应的方法,如果去查找父类中的方法?这些问题的关键在于编译器为每一个类做了结构处理。使得每一个类都有以下两个关键的组成部分:
- 一个指向父类的指针
- 一个类调度表。类调度表记录了每一个实现方法的地址以及与之对应的消息选择器。使得当一个属于该类对象的接收到消息时,通过查找该调度表,找到与消息选择器对应的实现方法,然后调用。
当然现在又有问题了,怎么知道对象是属于哪个类呢?
在对象初始化到时候,在所有对象实例变量中有个Class
类型的 isa
指针被初始化指向它的类结构。这使得通过isa
指针能够访问它所属的类以及整个继承结构树上的父类。
PS: Class是一个结构体指针类型,在objc/objc.h中定义如下
typedef struct objc_class *Class;
由于要和Runtime系统协同工作,所以这里被视为“对象”。不过用户不需要关心怎么定义和实现的,因为基本所有对象都是继承自NSObject和NSProxy。Apple已经帮我们处理好了。
这里总结下,之前提到每一个类有两部分结构:①指向父类的指针superclass
。②类调度表,记录选择器和与之对应实现方法的地址。
对象有一个指针isa
指向类结构。因此可以从一个对象通过isa
指针可以访问它的类,它的类通过superclass
指针访问父类,以此类推。最终一个对象可以访问到所有祖先类和它对应的类。而每个类都有自己的类调度表,因此一个对象和一个消息选择器就决定了实现方法的物理地址。因此完成了消息传递。
两个隐含参数self和_cmd
另外objc_msgSend除了将指向接受者的self
指针传递过去之外,还有一个隐含参数为_cmd
。它表示当前方法的selector。举例如下:
-(void)draw {
id target = getTheTarget();
SEL sel = getTheMethod();
if(target == self || sel == _cmd){
return;
}
[target performSelector: sel];
}
获取方法地址
由于Objective-C具有动态绑定特性,很多东西不是用户能够在编译时决定的。如果非得避开动态绑定特性,那么获取一个方法的物理地址,并且直接调用该方法,避开消息传递的过程这样就可以实现了。NSObject
提供了methodForSelector:
的方法,根据消息选择器参数,获取具体实现方法的地址指针。如下案例中,setter在类的定义中是-(void)setFilled:(BOOL)filled
,由于消息传递函数最终会变成id procedure(id receiver, SEL selector, arg0, arg1,...)
的形式,因此通过methodForSelector:
方法获得的具体实现方法指针类型应该是void (*)(id, SEL, BOOL)
类型,于是定义了该类型的变量setter,用来存储地址。使用的时候就和普通函数一样调用,带入三个参数(id, SEL, BOOL)
。
void (*setter)(id, SEL, BOOL);
int i;
setter = (void (*)(id, SEL, BOOL))[target
methodForSelector:@selector(setFilled:)];
for ( i = 0; i < 1000, i++ )
setter(targetList[i], @selector(setFilled:), YES);
PS: setter就是一个指向void xxx (id, SEL, BOOL)函数类型的指针。
methodForSelector:
方法是Cocoa runtime system的一部分不是Objective-C runtime system的一部分。
动态方法决议
Objective-C中的所有方法实际上就是至少带有id(消息接受者receiver)参数,以及SEL(消息选择器)参数的C函数。因此在程序运行的过程中可以向已存在的类动态的增加新的方法或者提供对应selector的动态实现。利用class_addMethod
C函数在以下两种决议方法中动态增加实例方法或者类方法。
- 在实例方法决议中增加实例方法
// 方法的实际实现
void dynamicMethodIMP(id self, SEL _cmd) {
printf("hello world!");
}
// 在实例方法决议中添加新的方法
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel==@selector(resolveThisMethodDynamically)) {
class_addMethod([self class], sel, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end
客户端测试
MyClass *myClass = [[MyClass alloc]init];
[myClass performSelector:@selector(resolveThisMethodDymanically)];
//绝对不能直接发送消息调用,因为直接调用会在编译阶段检测方法是否存在,如果不存在是不能到执行阶段的。而performSelector:是运行时阶段检测方法是否存在。
- 在类方法决议中增加新的类方法
void dynamicMethodIMP(id self, SEL _cmd) {
printf("hello world");
}
@implementation MyClass
+(BOOL)resolveClassMethod:(SEL)sel
{
if (sel==@selector(dynamicAddClassMethod)) {
class_addMethod(object_getClass(NSClassFromString(@"MyClass")), sel, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveClassMethod:sel];
}
客户端测试
[MyClass performSelector:@selector(dynamicAddClassMethod)];
消息转发
当给一个对象发送消息,但是对象无法“识别”消息的时候,会抛出unrecognized selector异常。但是在抛出异常之前,Runtime系统会再给消息接受对象一次机会去处理消息,也就是给对象发送forwardInvocation:
消息,该消息有一个NSInvocation
类型的参数。其中NSInvocation
对象封装了最初的消息以及消息所带的参数。你可以实现forwardInvocation:
方法,当被调用的时候,给予一个回应或者利用其它方法避开错误。但是,就如这个方法名字所示,通常是将消息转发给其它对象处理的。
PS:
forwardInvocation:
消息继承自NSObject,默认实现是doesNotRecognizeSelector:
,可以通过重写这个方法来实现自定义的转发内容。
为了转发消息,要明确两点:
- 确定消息如何走
- 转发消息的时候将参数也带走
一个NSInvocation
对象可以调用invokeWithTarget
方法来确定转发对象。
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([someOtherObject respondsToSelector: [anInvocation selector]])
[anInvocation invokeWithTarget:someOtherObject];
else
[super forwardInvocation:anInvocation];
}
forwardInvocation
其实是一个“无家可归”的消息的分派中心,将那些为不能别识别的消息,通过方法内的逻辑代码,转发到其他对象去或者进行响应处理。亦或者是,消息的集中中心,将所有未识别消息发送到同一个目的地去。总之forwardInvocation
就是管理未识别消息的中心。
PS:
forwardInvocation
只会在消息无法识别的时候调用,如果一个消息是已经定义好的,那么不会触发forwardInvocation
方法。
消息转发机制有两个很重要的用途。
第一个就是,当A类与B类不处于同一个继承树的时候,即A类与B类不构成祖先和孙子关系,A类要向B类引用一个方法,则可以用消息转实现。例如我要调用B类的play
方法,则给A类发送play
消息,由于A类没有定义play
,则调用forwardInvocation
将消息转发给B类处理,这样就形成了A类向B类“借”了play
方法的场景。当然不建议这么设计,如果程序能够用普通的组合方式设计类,那么用组合调用。
第二个是,作为代理对象的时候。如果A类有个代理对象a,去代理B类对象b的某个deal
方法,通常设计模式中,利用类的组合关联就可以解决这个方案。但是在有时候A类又得增加一些新方法,而利用消息转发,则不需要增加这些方法。当要调用b对象deal
方法的时候,给a发消息,在forwardInvocation
里进行一些代理设置(例如权限控制,缓存加载)等之后转发给b对象处理。
在第一个用途中,其实可以看做是类的横向“继承“,通过消息转发机制,可以从不同的类簇中”继承“到方法,从而使得形成一个跨越不同继承树的横向”继承树“。
类型编码
为了协助Runtime系统工作,编译器将一个方法的返回值类型、参数类型编码成一串C字符串(char *类型)并且将C字符串和与之对应的消息选择器关联起来。除了编译器要用到这个功能,有时候开发者也需要用到类型编码,所以Objective-C提供了一个指令@encode()
,这个指令可以对一般的数据类型(如int,double,short)、指针类型(int **,double *)、结构体类型(Retangle, MyStruct)、联合类型(IntegerUnion)、类,只要能够被C语言的sizeof()
操作的数据类型,都可以。
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);
下面这个列表展示了常见数据类型中被编码后的字符串
数据类型 | 编码字符串 |
---|---|
char | c |
int | i |
short | s |
long | l |
long long | q |
unsigned char | C |
unsigned int | I |
unsigned short | S |
unsigned long | L |
unsigned long long | Q |
float | f |
double | d |
bool(C++), _Bool(C99) | B |
void | v |
char * | * |
对象(包括id类型的) | @ |
Class(类对象) | # |
SEL | : |
数组 | [数组元素类型] 如果是int类型的则为[i] |
结构体 | {结构体名字=结构体元素类型} 例如{Rectangle=dd} |
联合体类型 | (联合体名字=联合体元素类型) 例如(IntegerUnion=ii) |
位域 | b+位的数目 例如5位域b5 |
指针 | ^+指针指向的类型 例如int*则为^int |
未知类型 | ? |
PS: Objective-C不支持
long double
类型。@encode(long double)
返回d
字符串,这和double
一样的。另外当你自己为自定义类型的设计字符串编码的时候,千万别和以上的重复。
这里再详细说明下数组的类型编码案例
// 如下double (*) [12] 是指指向大小为12个double类型元素数组的指针。
char *parrayEcode = @encode(double (*) [12])
// 如下double* [12] 是指大小为12个double指针类型的元素的数组
char *arraypEncode = @encode(double* [12]);
其编码结果分别为:
parrayEcode: ^[12d]
arraypEncode: [^12d]
再看看结构体编码案例
typedef struct example {
id anObject;
char *aString;
int anInt;
} Example;
char *structExm = @encode(example);
其编码结果为
structExm: {example=@*i}
更加复杂的编码内容可以查看苹果的官方文档《Objective-C Runtime Programming Guide》> Type encoding
声明属性
当编译器遇到属性声明的时候,它会在类、类别、协议的类型编码内生成相关的描述性元数据。用户可以访问这些元数据利用相关的api函数。
首先介绍Property结构体,其定义如下:
typedef struct objc_property *Property;
用户可以利用class_copyPropertyList
和
两个函数来获取类(包括已经被加载的类别)和协议中的相关属性列表。
protocol_copyPropertyList
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount) objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)
例如,某个类定义如下:
@interface Lender : NSObject {
float alone;
}
@property float alone;
@end
你可以按照如下方式获取属性列表:
id LenderClass = objc_getClass("Lender");
unsigned int outCount;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
获取的属性列表存在properties指针中,你可以用property_getName
函数,通过遍历properties属性列表,来获取所有属性名。获得属性名后,可以通过class_getProperty
和protocol_getProperty
获得对应的属性引用。另外通过属性引用可以利用property_getAttributes
来获得属性的一些attributes的编码类型,例如是否只读啊,是否原子性啊等。其中以上四个函数声明如下:
const char *property_getName(objc_property_t property);
objc_property_t class_getProperty(Class cls, const char *name);
objc_property_t protocol_getProperty(Protocol *proto, const char *name, BOOL
isRequiredProperty, BOOL isInstanceProperty);
const char *property_getAttributes(objc_property_t property);
使用案例:
id LenderClass = objc_getClass("Lender");
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(LenderClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property),
property_getAttributes(property));
}
下面是属性的attributs和对应的编码类型
attributs说明 | 编码字符串 |
---|---|
readonly | R |
copy | C |
retain | & |
nonatomic | N |
自定义的getter | G+getter名字 |
自定义的setter | S+setter名字 |
@dynamic | D |
__weak | W |
属性可以被垃圾回收 | P |
利用旧的编码方式 | t |
如下案例说明了属性attributs的编码,首先定义四个自定义类型
enum FooManChu { FOO, MAN, CHU };
struct YorkshireTeaStruct { int pot; char lady; };
typedef struct YorkshireTeaStruct YorkshireTeaStructType;
union MoneyUnion { float alone; double down; };
以下表格显示的是当调用property_getAttributs
函数得到的字符串结果
属性声明 | 结果字符串 |
---|---|
@property char charDefault; | Tc,VcharDefault |
@property double doubleDefault; | VdoubleDefault |
@property enum FooManChu enumDefault; | VenumDefault |
@property float floatDefault; | Tf,VfloatDefault |
@property int intDefault; | Ti,VintDefault |
@property long longDefault; | Tl,VlongDefault |
@property short shortDefault; | Ts,VshortDefault |
@property signed signedDefault; | Ti,VsignedDefault |
@property struct YorkshireTeaStruct structDefault; | T{YorkshireTeaStruct=”pot”i”lady”c},VstructDefault |
@property YorkshireTeaStructType typedefDefault; | T{YorkshireTeaStruct=”pot”i”lady”c},VtypedefDefault |
@property union MoneyUnion unionDefault; | T(MoneyUnion=”alone”f”down”d),VunionDefault |
@property int (*functionPointerDefault)(char *); | T^?,VfunctionPointerDefault |
@property void *voidPointerDefault; | T^v,VvoidPointerDefault |
@property id idDefault; Note: the compiler warns: no ‘assign’, ‘retain’, or ‘copy’ attribute is specified - ‘assign’ is assumed” | T@,VidDefault |
@property(getter=intGetFoo,setter=intSetFoo:) int intSetterGetter; | Ti,GintGetFoo,SintSetFoo:,VintSetterGetter |
@property(nonatomic, readonly, copy) id idReadonlyCopyNonatomic; | T@,R,C,VidReadonlyCopyNonatomic |
更多声明属性的编码内容可以查看苹果的官方文档《Objective-C Runtime Programming Guide》> declared properties