Effective Objective-C(第11-14条)动态绑定、objc_msgSend、消息转发机制

第11条:理解objc_msgSend的作用

    在对象上调用方法是OC中经常使用的功能。用OC术语来说这叫做:“传递消息”(pass a message)。消息有“名称”(name)或者“选择子”(selector),可以接收参数,而且可能还有返回值。

    由于OC是C的超集,所以最好理解C语言的函数调用方式。C语言使用“静态绑定”,就是说在编译期就能决定运行时所应调用的函数。以下列代码为例:

#import <stdio.h>
void printHello(){
    printf("Hello world\n");
}
void printGoodBye(){
    printf("Goodbye world\n");
}
void doTheThing(int type){
    if(type == 0){
        printfHello();
    }else{
        printGoodbye();
}
    return 0;   
}

    编译器在编译代码的时候就已经知道程序中有printHello与printGoodBye这两个函数了,于是直接生成调用这些函数的指令。而函数地址实际上是硬编码在指令之中的。若将刚才那段代码写成下面这样,会如何呢?

#import <stdio.h>
void printHello(){
    printf("Hello world\n");
}
void printGoodBye(){
    printf("Goodbye world\n");
}
void doTheThing(int type){
    void (*fun)();
    if(type == 0){
        fun = printfHello();
    }else{
        fun = printGoodbye();
}
    fun();
    return 0;   
}

    这时就得使用“动态绑定”(dynamic binding),因为所要用的函数知道运行时才能确定。编译器在这个环境下生成的指令与刚才哪个例子不同,在第一个例子中if和else语句中都有函数调用指令。而第二个例子中,只有一个函数调用指令,不过待调用的地址无法硬编码在指令之中,而是要运行期读出来。

    在OC中,如果向某对象传递消息,那就会使用动态绑定机制来决定需要调用的方法。在底层,所有方法都是普通C语言函数,然而对象收到消息后,究竟该掉哪个方法则完全于运行期决定,甚至可以在程序运行时改变,这些特性使得OC称为一门真正的动态语言。

    给对象发消息可以这样写:

    id returnValue = [someObject messageName:parameter];

    本例中,someObject叫做“接收者”,messageName叫做“选择子”。选择子与参数合起来称为“消息”。编译器看到此消息后,将其转换为一条标准的C语言函数调用,所调用的函数乃是消息传递机制的核心函数,叫做objc_msgSend,其原型如下:

    void objc_msgSend(id self ,SEL cmd,...);

    这是个参数个数可变长的函数,经过转化,刚才的函数被转化为这样:

    id returnValue = objc_msgSend(someObject,@selector(messageName),parameter);

    objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法。为了完成此操作,该方法需要在接收者所属的类中搜索其“方法列表”(list of methods)如果能找到与选择子名称相符的方法,就跳至其实现代码。若找不到,那就沿着继承体系继续向上查找,等找到名称相符的方法之后再跳转。如果最终还是找不到相符的方法,那就执行“消息转发”

    这么说来,想调用一个方法似乎需要很多步骤。所幸objc_msgSend会将匹配结果缓存在“快速映射表”里面。实际上,消息派发(message dispatch)并非应用程序的瓶颈所在。

    前面讲的这部分内容只描述了部分消息的调用过程,其他“边界情况”(edge case)则需要交由OC运行环境中的另一些函数来处理:

objc_msgSend_stret 待发的消息要返回结构体

objc_msgSend_fpret 消息返回的是浮点数

objc_msgSend_super 要给超类发消息。

    刚才曾提到,objc_msgSend等函数一旦找到应该调用的方法实现之后,就会“跳转过去”。之所以能这样做,是因为OC对象的每个方法都可以视为简单的C函数,原型如下:

    <return_type> class_selector(id self,SEL _cmd,...)

   每个类里都有一张表格(参考下面第14条关于isa的描述),其中的指针都会指向这个函数,而选择子的名称则是查表时所用的“键”。objc_msgSend等函数正是通过这张表格来寻找应该执行的方法并跳至其实现的。请注意,原型的样子和objc_msgSend很像。这不是巧合,而是利用“尾调用优化”(tail-call optimization)技术,令“跳至方法实现”这一操作变得更简单。

    在实际编写OC时,无须担心这些问题,开发者应该了解其底层工作原理。代码究竟是如何执行的,而且能理解为何在调试的时候,栈信息中总是出现objc_msgSend

【本节要点】

● 消息由接收者、选择子及参数构成。给某对象“发送消息”也就相当于在该对象上“调用方法”

● 发给某对象的去全部消息都要由“动态消息派发系统”(dynamic message dispatch system)来处理。该系统会查出对应的方法,并执行其代码

第12条:理解消息转发机制

    第11条讲了对象的消息传递机制,并强调了其重要性。本节讲另外一个重要的问题,就是对象在接收到无法解读的消息之后会发生什么情况。
    若想令类能理解某条消息,我们必须实现方法才行。如果没有实现就会启动“消息转发”(message forwarding)机制。在控制台看到这样的错误,说明启动了消息转发:
-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87
*** Terminating app due to uncaught exception 'NSInvalidArgumentException',reason:'-[__NSCFNumber lowercaseString]:unrecognized selector sent to instance 0x87'
    上面这段异常信息是由NSObject的“doesNotRecognizedSelector:”方法所抛出的,次异常表明:消息接收者__NSCFNumber,其无法理解lowercaseString的选择子。在本利中,消息转发过程以应用程序崩溃而告终,开发者可于转发过程中设置挂钩,用于执行预订的逻辑,而不实用程序崩溃。
    消息转发分为两大阶段。第一阶段先征询接收者,所属的类,看其是否能动态添加方法,已处理当前这个“未知选择子”,这叫做“动态方法解析”。第二阶段涉及“完整的消息转发机制”

    动态方法解析

    对象在收到无法解读的消息后,首先将调用其所属类的下列类方法:
    +(BOOL) resolveInstanceMethod:(SEL)selector
    该方法的参数就是哪个未知的选择子,其返回值为Boolean类型,表示这个类是否新增一个实例方法处理此选择子。在继续往下执行转发机制之前,本类有机会新增一个处理此选择子的方法。假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另外一个方法,“resolveClassMethod”
    使用这种办法的前提是:相关方法的实现代码已经写好,只等待运行的时候动态插在类里面就可以了。此方案通常用来实现@dynamic属性,下面代码展示了如何使用“resolveInstanceMethod:”来实现@dynamic属性:
id autoDictionaryGetter(id self,self _cmd);
void autoDictionarySetter(id self, SEL _cmd,id value);
+(BOOL)resolveInstanceMethod:(SEL)selector{
    NSString *selectorString = NSStringFromSelector(selector);
    if(/*selector is from a @dynamic property*/)
    {
        if([selectorString hasPrefix:@"set"]){
            class_addMethod(self.selector,(IMP)autoDictionarySetter,"v@:@");
        }else{
            class_addMethod(self.selector,(IMP)autoDictionaryGetter,"v@:@");
        }
    return YES;
    }
return [super resolveInstanceMethod:selector];
}
    首先将选择子化为字符串,然后检测其是否表示设置方法。若前缀为set,则表示“设置方法”,否则就是“获取方法”。

备援接收者

    当前接收者还有第二次机会处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来注册。
-(id) forwardingTargetForSelector:(SEL)selector
    方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到返回nil

完整的消息转发

    如果转发算法已经来到这一步,那么唯一能做的就是启动完整的消息转发机制了。首先创建NSInvacaton对象,把与尚未处理的那条消息有关的全部细节都封于其中,此对象包含选择子、目标(target)及参数。在触发NSInvocation对象时,“消息派发系统”将亲自出马,把消息指派给目标对象。

    此步骤会调用下列方法转发消息:

    -(void) forwardInvocation:(NSInvocation*)invocation

    这个方法可以实现得很简单:只需要改变调用目标,使消息在新目标上得以调用即可。然而这样实现出来的方法与“备援接收者”方案所实现的方法等效,所以很少有人采用这么简单的实现方式。比较有用的实现方式为:在触发消息前,先以某种方式改变消息内容,比如追加另外一个参数,或是该换选择子,等等。

    实现此方法时,若发现某调用操作不应本类处理,则需要调用超类的同名方法。这样的话,继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继而调用“doesNotRecognizeSelector:”以抛出异常,次异常表示选择子最终未能得到处理。

消息转发全流程

    下图描述了消息转发的各个步骤:

Effective Objective-C(第11-14条)动态绑定、objc_msgSend、消息转发机制_第1张图片

    接收者在每一步均有机会处理消息。步骤越往后,消息处理的代价就越大。

第13条:用“方法调配技术”测试“黑盒方法”

    第11条中解释过:OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。你也许会问:于给定的选择子名称对应的方法是不是可以在运行期改变呢?没错,就是这样。若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。次方案经常称为“方法调配”(method swizzling)。
    类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够肇东啊应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:
    id(*IMP)(id,SEL,..)
    NSString类可以相应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择字都映射到了不同的IMP之上。如下图
Effective Objective-C(第11-14条)动态绑定、objc_msgSend、消息转发机制_第2张图片

    OC运行期系统提供的几个方法都能够用来操作这张表。开发者可以向其新增选择子,也可以改变某选择子对应的方法实现,还可以交换选择子所映射到的指针。经过几次操作之后,类的方法表就会标称如图这个样子

Effective Objective-C(第11-14条)动态绑定、objc_msgSend、消息转发机制_第3张图片

    在新的映射表中,多了一个名为newSelector的选择子,capitalizedString的实现也变了,而lowercaseString与uppercaseString的实现则互换了。上述修改均无需编写子类,只要修改了“方法表”的布局,就会反映到程序中所有的NSString实例之上。这下大家见识到此特性的强大之处了吧

    下面看一下如何互换两个方法实现。想交换方法实现,可用下列函数:

void method_exchangeImplementations(Method m1,Method m2)

    此函数的两个参数表示待交换的两个方法实现,而方法实现则可通过下列函数获得:

Method class_getInstanceMethod(Class aClass,SEL aSelector)

    具体操作如下:

Method originalMethod = class_getInstanceMethod([NSStringclass],@selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass],@selector(uppercaseString));
method_exchangeImplementations(originalMethod,swappedMethod);

    实际应用,可以为那些黑盒方法增加日志技术功能。比如NSString 的lowercaseString接口,想在lowercaseString中添加log,该如何做呢

@interface NSString (EOCMyAddtions)
-(NSString*)eoc_myLowercaseString;
@end
@implementation NSString(EOCMyAdditions)
-(NSString*) eoc_myLowercaseString{
    NSString *lowercase = [self eoc_mylowercaseString];
    NSLog(@"%@ =>%@",self,lowercase);
    return lowercase;
}
@end
    这段代码看上去好像会陷入递归调用的死循环,不过大家要记住,此方法是准备和lowercaseString方法互换的。所以,在运行期,eoc_myLowercaseString选择子实际上对应于原有的lowercaseString方法实现。最后,通过下列代码叫魂这两个方法实现:

Method originalMethod = class_getInstanceMethod([NSStringclass],@selector(lowercaseString));
Method swappedMethod = class_getInstanceMethod([NSStringclass],@selector(roc_myLowercaseString));
method_exchangeImplementations(originalMethod,swappedMethod);

    执行完上诉代码之后,只要在NSString实例上调用lowercaseString方法,就会输出log了。

【本节要点】

● 在运行期间,可以向类中新增或者替换选择子所对应的方法实现。

● 使用另一份实现来替换缘由的方法实现,这道工序叫:“方法调配”,开发者常用此技术向原有实现中添加新功能。

● 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

第14条:理解“类对象”的用意

    OC实际上是一门及其动态的语言。第11条讲解了运行期系统如何查找并调用某方法的实现代码,第12条则讲述了消息转发的原理:如果类无法立即响应某个选择子,那么就会启动消息转发流程。然而,消息的接收者究竟是何物?是对象本身么?运行期系统如何知道某个对象的类型呢?对象类型并非在编译器就绑定好了,而是要在运行期查找。而且还有个特殊类型id,它能够指代任意的OC对象类型。

    我们先讲一些基础知识,看看OC对象本质是什么。每个OC对象都是指向某块内存数据的指针。所以在声明变量时,类型后面都要跟一个星号(*)

NSString *pointerVariable = @"Some thing";

    编过C语言程序的人都知道什么意思。该变量“指向”(point to)NSString实例。所有OC对象都是如此,如果想把OC对象声明在栈上,编译器会报错:

Sting stackVariable = @"Some thing";
//error: interface type cannot be statically allocated

    对于通用id类型,由于其本身已经是指针了,所以我们能够这样写:

id genericTypeString = @"Some thing";

    描述OC对象所用的数据结构定义在运行期程序库的头文件里,id类型本身也在定义这里:

typedef struct objc_object{
    Class isa;
} *id;

    由此可见,每个对象结构体的首个成员是Class类的变量。该变量定义了对象所属的类,通常称为is a指针。例如,刚才的例子中所有的对象is a NSString,所以其“is a”指针就指向NSString。Class对象也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class{
    Class isa;
    Class super_class;
    const char* name;
    long version;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols
};
    此结构图存放类的“元数据”(metadata),例如类的实例实现了几个方法,具备多少实例变量等信息。此结构体的首个变量也是isa指针,这说明Class本身亦为OC对象。结构体里还有个变量叫做super_class,它定义了本类的超类。类对象所属的类型(也就是isa指针所指向的类型)是另外一个类,叫做“元类”(metaclass),用来表述类对象本身所具备的元数据。“类方法”就定义于此处,因为这些方法可以理解成类对象的实例方法。

    假设有个名为SomeClass的子类从NSObject中继承而来,则其继承体系如下图所示

Effective Objective-C(第11-14条)动态绑定、objc_msgSend、消息转发机制_第4张图片

    super_class指针确立了继承关系,而isa指针描述了实例所属的类。通过这张布局关系图即可执行“类型信息查询”。我们可以查出对象是否能响应某个选择子,是否遵从某项协议,并且能看出此对象位于“类继承体系”的那一部分。每个类仅有一个”类对象”,而每个“类对象”仅有一个与之相关的“元类”。

下图可以证明:多个实例中的isa就是同一个:

Effective Objective-C(第11-14条)动态绑定、objc_msgSend、消息转发机制_第5张图片

在类继承体系中查询类型信息

    可以用类型信息查询方法来检视类继承体系。“isMemberOfClass:”能够判断出对象是否为某个特定类的实例。而“isKindOfClass:”则能够判断出对象是否为某类或其派生的实例。

NSMutableDictionary *dict = [NSMutableDictionary new];
[dict isMemberOfClass:[NSDictionary class]];//no
[dict isMemberOfClass:[NSMutableDictionary class]];//yes
[dict isKindOfClass:[NSDictionary class]];//yes
[dict isKindOfClass:[NSArray class]];//no
    由于OC使用“动态类型系统”(dynamic typing),所以用于查询对象所属类的类型信息查询非常有用。从collecting中获取对象时,通常是id类型,那就可以使用查询类型信息方法了。

【本节要点】

● 每个实例都有一个指向Class对象的指针,用以表明其类型。从这些Class对象则构成了类的继承体系。

● 如果对象类型无法在编译期确定,那么就应该使用类型查询方法来探知

● 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象,因为某些对象可能实现了消息转发功能。


你可能感兴趣的:(运行时,类对象,消息转发,objc_msgSend,查询类型信息)