《Effective Objective-C 2.0》—(第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,...)

每隔类里都有一张表格,其中的指针都会指向这个函数,而选择子的名称则是查表时所用的“键”。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,"[email protected]:@");
        }else{
            class_addMethod(self.selector,(IMP)autoDictionaryGetter,"[email protected]:@");
        }
    return YES;
    }
return [super resolveInstanceMethod:selector];
}

首先将选择子化为字符串,然后检测其是否表示设置方法。若前缀为set,则表示“设置方法”,否则就是“获取方法”。

备援接收者

当前接收者还有第二次机会处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转给其他接收者来注册。

-(id) forwardingTargetForSelector:(SEL)selector

方法参数代表未知的选择子,若当前接收者能找到备援对象,则将其返回,若找不到返回nil

完整的消息转发

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

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

    -(void) forwardInvocation:(NSInvocation*)invocation

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

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

消息转发全流程

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

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

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

第11条中解释过:OC对象收到消息之后,究竟会调用何种方法需要在运行期才能解析出来。你也许会问:于给定的选择子名称对应的方法是不是可以在运行期改变呢?没错,就是这样。若能善用此特性,则可发挥出巨大优势,因为我们既不需要源代码,也不需要通过继承子类来覆写方法就能改变这个类本身的功能。这样一来,新功能将在本类的所有实例中生效,而不是仅限于覆写了相关方法的那些子类实例。次方案经常称为“方法调配”(method swizzling)。

类的方法列表会把选择子的名称映射到相关的方法实现之上,使得“动态消息派发系统”能够肇东啊应该调用的方法。这些方法均以函数指针的形式来表示,这种指针叫做IMP,其原型如下:

    id(*IMP)(id,SEL,..)

NSString类可以相应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每隔选择字都映射到了不同的IMP之上。如下图

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

在新的映射表中,多了一个名为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中继承而来,则其继承体系如下图所示

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

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

可以用类型信息查询方法来检视类继承体系。“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对象则构成了类的继承体系。

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

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

《Effective Objective-C 2.0》—(第11-14条)—运行时动态绑定、objc_msgSend、消息转发机制

时间: 2024-07-30 13:53:08

《Effective Objective-C 2.0》—(第11-14条)—运行时动态绑定、objc_msgSend、消息转发机制的相关文章

Effective Objective-C 2.0 — 第12条:理解消息转发机制

11 条讲解了对象的消息传递机制 12条讲解对象在收到无法解读的消息之后会发生什么,就会启动“消息转发”(message forwarding)机制, 若对象无法响应某个选择子,则进入消息转发流程. 1,通过运行期的动态方法解析功能,可以在需要用到某个方法时再将其加入类中. 2,对象可以把其无法解读的某些选择子转交给其他对象来处理. 经过上述两步之后,如果还是没办法处理选择子,那就启动完整的消息转发机制. 动态方法解析 对象在收到无法解读的消息之后,首先调用其所属类的下列类方法 + (BOOL)

从0开始学Java:运行时异常与一般异常有何异同?

无限互联从0开始学Java系列之JAVA相关基础知识,Java基础培训,String 和StringBuffer的区别 1.Java基础学习,从0开始学Java:String 和StringBuffer的区别? JAVA平台提供了两个类:String和StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据.这个String类提供了数值不可改变的字符串.而这个StringBuffer类提供的字符串进行修改.当你知道字符数据要改变的时候你就可以使用StringBuffer.典型

Jvm(11),运行时数据---独占区---本地方法栈

本地方法栈主要是来处理native的方法的,我们来看一下什么是native的方法. Java无法直接访问到操作系统底层(如系统硬件等),为此Java使用native方法来扩展Java 程序的功能. 其实在java中我们通用的虚拟机HotSpot中,本地方法栈和虚拟机栈是同一块区域在这里讲的一般是通用的虚拟机. 原文地址:https://www.cnblogs.com/qingruihappy/p/9691301.html

Objective-O Runtime 运行时初体验

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理.这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等. 这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码.对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行.这个运行时系统即Objc Runtime.Objc Runtime其实是一个Runtime

Android 6.0版本以后运行时权限提醒

Android发布6.0以后对app运行所需要的权限提示进行了友好的提示,类似于苹果系统,比如在某个页面要用到打电话的权限,会弹出一个提示框,提示你是否需要同意这个权限,如果同意则app就有了打电话的权限,既可以拨打电话了,不同意则不能拨打电话,只能去设置中勾选,在6.0以前,权限配置都是在AndroidManifest.xml文件中添加例如: <?xml version="1.0" encoding="utf-8"?> <manifest xml

iOS学习之Objective-C 2.0 运行时系统编程

0 导言 本主主要内容包括: 1.概述2.参考3.运行时系统的版本和平台4.和运行时系统的交互5.消息6.动态方法解析7.消息转发8.类型编码9.属性声明 1 概述 Objective-C语言将决定尽可能的从编译和链接时推迟到运行时.只要有可能,Objective-C总是使用动态的方式来解决问题.这意味着Objective-C语言不仅需要一个编译器,同时也需要一个运行时系统来执行编译好的代码.这里的运行时系统扮演的角色类似于 Objective-C语言的操作系统,Objective-C基于该系统

Objective-C 2.0的运行时编程

Objective-C 2.0 的运行时环境叫做Morden Runtime,iOS 和Mac OS X 64-bit 的程序都运行在这个环境,也就是说Mac OS X 32-bit 的程序运行在旧的Objective-C 1.0 的运行时环境LegacyRuntime,这里我们只讲解Morden Runtime.  同运行时交互主要在三个不同的地方,分别是A.Objective-C 源码(譬如:你定义的Category中的新方法会在运行时自动添加到原始类).B.NSObject 的方法(isM

Android数据存储之Android 6.0运行时权限下文件存储的思考

前言: 在我们做App开发的过程中基本上都会用到文件存储,所以文件存储对于我们来说是相当熟悉了,不过自从Android 6.0发布之后,基于运行时权限机制访问外置sdcard是需要动态申请权限,所以以往直接sdcard根目录上直接新建了一个xxx/cache/目录来做文件存储就会不是那么容易控制了,所以有必要重新认识一下Android文件存储的相关知识了. 背景: 有关外置sdcard的读写权限 <uses-permission android:name="android.permissi

Oracle database 11.2.0.3.0 升级至 11.2.0.3.14

下载PSU p20299017_112030_Linux-x86-64(DATABASE PATCH SET UPDATE 11.2.0.3.14 (INCLUDES CPUAPR2015)).zip 及Opatch p6880880_112000_Linux-x86-64(OPatch patch of version 11.2.0.3.10 for Oracle software releases 11..zip 下载地址 http://yunpan.cn/cHACmBerMb526 访问密