对于题目一,Objective-C的运行时编程 与运行时编程相关的函数及头文件在iOSSDK——usr/include——objc文件夹。 作为一个软件开发者,我们需要知道我们编写的程序从我们开始编辑一行一行的代码到程序运行起来,期间经历了哪些过程,具体的就是编译器为我们做了些什么? 在C程序里,一个程序从编辑——编译——链接——运行,期间经历了4个过程,对于OC程序的运行过程,这点还需查阅相关资料,但可以肯定的是,最重要的编译与运行阶段肯定会在其中。 对于运行时编程,相信很多人都比较陌生,查阅资料及自己的整理,有如下的解释。 1)Objective-C 2.0 的运行时环境叫做 Morden Runtime,iOS 和 Mac OS X 64-bit 的程序都运行在 这个环境,也就是说 Mac OS X 32-bit 的程序运行在旧的 Objective-C 1.0 的运行时环境 为Legacy Runtime。 Morden Runtime同运行时交互主要在三个不同的地方,分??是 A.Objective-C 源码(譬如:你定义的 Category 中的新方法会在运行时自动添加到原始类)、B.NSObject 的方法(isMemberClassOf 等动态判 定的方法)、C.运行时函数。 2)isa 指针 OC中对于Class的定义如下: /// An opaque type that represents an Objective-C class. typedef struct objc_class *Class; 对objc_class的定义如下 struct objc_class { Class isa OBJC_ISA_AVAILABILITY; #if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; const char *name OBJC2_UNAVAILABLE; long version OBJC2_UNAVAILABLE; long info OBJC2_UNAVAILABLE; long instance_size OBJC2_UNAVAILABLE; struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; struct objc_method_list **methodLists OBJC2_UNAVAILABLE; struct objc_cache *cache OBJC2_UNAVAILABLE; struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; #endif } OBJC2_UNAVAILABLE; /* Use `Class` instead of `struct objc_class *` */ 可知isa是一个指向结构体的指针,具体的结构体的构成如 objc_class所示。 NSObject 中有一个 Class isa 的指针类型的成员变量,因为我们的对象大都直接或者间接的从 NSObject 继承而来,因此都会继承这个 isa 成员变量,isa 在运行时会指向对象的 Class 对象, 一个类的所有对象的 Class 对象都是同一个(JAVA 也是如此),这保证了在内存中每一个类 型都有唯一的类型描述。这个 Class 对象中也有个 isa 指针,它指向了上一级的父类的 Class 对象。 在明白了这个 isa 之后,你就可以明白在继承的时候,A extends B,你调用 A 的方法 a(),首 先 A 的 isa 到 A 的 Class 对象中去查找 a()方法,找到了就调用,如果没找到,就驱使 A 的 Class 对象中的 isa 到父类 B 的 Class 对象中去查找。 3) SEL 与 IMP: 方法选择器 SEL,它可以通过如下两种方式获得: (SEL) @selector(方法的名字) (SEL) NSSelectorFromString(方法的名字的字符串) 另外,你还可以通过(NSString*) NSStringFromSelector(SEL)函数来获取 SEL 所指定的方法名称 字符串。 其实 Objective-C 在编译的时候,会依据每一个定义的方法的名字、参数序列,生成一个唯 一的整数标识,这个标识就是 SEL。因此,在运行时查找方法都是通过这个唯一的标识,而 不是通过方法的名字。 Objective-C 又提供了 IMP 类型,IMP 表示指向实现方法的指针(函数指针),通过它,你可 以直接访问一个实现方法,从而避免了[xxx message]的静态调用方式,需要首先通过 SEL 确 定方法,然后再通过 IMP 找到具体的实现方法,最后再发送消息所带来的执行效率问题。 一般,如果你在多次循环中反复调用一个方法,用 IMP 的方式,会比直接向对象发送消息高效一些。
这里我们看到要获得 IMP 的指针,可以通过 NSObject 中的 methodForSelector: (SEL)方法,访 问这个指针函数,我们使用 imp(id,SEL,argument1,... ...),第一个参数是调用方法的对象,第 二个方法是方法的选择器对象,第三个参数是可变参数,表示传递方法需要的参数。 4) 消息的传递 objc_msgSend 函数: 通过 isa 指针的讲解,我们知道 Objective-C 中的方法调用是在运行时才去绑定的,再进一步 看,编译器会把对象消息发送[xxx method]转换为 objc_msgSend(id receiver,SEL selector,参数...) 的函数调用。因此上面例子中的 print 方法你也可以像下面这样调用: objc_msgSend(person,print_sel,@"++++++++"); 当然,这是编译器要做的事情,你在写代码的时候,是不需要直接使用这种写法的。 综合 isa、SEL、IMP 的讲解,实际上 objc_msgSend 的调用过程就应该是这样的。 A.首先通过第一个参数的 receiver,找到它的 isa 指针,然后在 isa 指向的 Class 对象中使用 第二个参数 selector 查找方法; B.如果没有找到,就使用当前 Class 对象中的新的 isa 指针到上一级的父类的 Class 对象中查 找; C.当找到方法后,再依据 receiver 的中的 self 指针找到当前的对象,调用当前对象的具体实 现的方法(IMP 指针函数),然后传递参数,调用实现方法。 D.假如一直找到 NSObject 的 Class 对象,也没有找到你调用的方法,就会报告不能识??发送 消息的错误。 5)动态方法解析 Objective-C 2.0 中增加了@dynamic 指令,表示变量对应的属性访问器方法,是动态实 现的,你需要在 NSObject 中继承而来的+(BOOL) resolveInstanceMethod:(SEL) sel 方法中指定 动态实现的方法或者函数。 例:
对于接口中的height在实现类中使用了@dynamic指令,紧接着,你需要指定一个函 数或者其他类的方法作为height的setter、getter方法的运行时实现。为了简单,我们指定 了Person.m中定义的函数(注意这是C语言的函数,不是Objective-C的方法)dynamicMethod 作为height的setter方法的运行时实现。被指定为动态实现的方法的dynamicMethod的参数 有如下的要求: A.第一个、第二个参数必须是id、SEL; B.第三个参数开始,你可以按照原方法(例如:setHeight:(float))的参数定义。 再接下来,你需要覆盖 NSObject 的类方法 resolveInstanceMethod,这个方法会把需要动态 实现的方法(setHeight:)的选择器传递进来,我们判断一下是否是需要动态实现的选择器, 如果是就把处理权转交给 dynamicMethod。如何转交呢?这里我们就要用到运行时函数 class_addMethod(Class,SEL,IMP,char[])。 运行时函数位于 objc/runtime.h,正如名字一样,这里面都是 C 语言的函数。按照这些函数 的功能的不同,主要分为如下几类:操作类型、操作对象、操作协议等。大多数的函数都可 以通过名字看出是什么意思,例如:class_addProtocol 动态的为一个类型在运行时增加协议、 objc_getProtocol 把一个字符串转换为协议等。具体这些运行时函数都是做什么用的,你可 以参看 Apple 官方页面: http://developer.apple.com/library/ios/documentation/Cocoa/Reference/ObjCRuntimeRef/Refer ence/reference.html#//apple_ref/doc/uid/TP40001418 言归正传,我们来解释一下这里需要用到的 class_addmethod 方法,这个方法有四个参数, Class 表示你要为哪个类型增加方法,SEL 参数表示你要增加的方法的选择器,IMP 表示你要 添加的方法的运行时的具体实现的函数指针。其实在这里你能够看出 SEL 并不能在运行时找 到真正要调用的方法,IMP 才可以真正的找到实现方法的。 在讲解第四个参数 char[]之前,我们先看一下第一篇文档中提到的@encode 指令,在把任意 非 Objective-C 对象类型封装为 NSValue 类型的时候使用到了@encode 指令,但当时我们没 有详细说明这个指令的??义。实际上@encode()可以接受任何类型,Objective-C 中用这个指令做类型编码,它可以把任何一个类型转换为字符串,譬如:void 类型被编码之后为 v,对 象类型为@,SEL 类型为:等,具体的你可以参看 Apple 官方页面关于 Type Encoding 的描述: http://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/A rticles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW 现在我们来正式的看以下第四个参数 v@:f 的??义,它描述了 IMP 指向的函数的描述信息, 按照@encode 指令编译之后的字符说明,第一个字符 v 表示返回值为 void,剩余的字符为 dynamicMethod 函数的参数描述,@表示第一个参数 id,:自然就是第二个参数 SEL,f 就是 第三个参数 float。由于前面说过动态方法的实现的前两个参数必须是 id、SEL,所以第四个 参数中的字符串的第二、三个字符一定是@:。 我们看到 resolveInstanceMethod 方法的返回值为 BOOL,也就是这个方法返回 YES 表示找到 了动态方法的具体实现,否则就表示没有在运行时找到真实的实现,程序就汇报错。 经过了上面的处理,Objective-C 的运行时只要发现你调用了@dynamic 标注的属性的 setter、 getter 方法,就会自动到 resolveInstanceMethod 里去寻找真实的实现。这也就是说你在 main.m 中调用 peson.height 的时候,实际上 dynamicMethod 函数被调用了。 实际上除了@dynamic 标注的属性之外,如果你调用了类型中不存在的方法,也会被 resolveInstanceMethod 或者 resolveClassMethod 截获,但由于你没有处理,所以会报告不能 识??的消息的错误。 5)消息转发 在前面的 objc_msgSend()函数的最后,我们总结了 Objective-C 的方法调用过程,在最后一步 我们说如果一路找下来还是没有找到调用的方法,就会报告错误,实际上这里有个细节,那 就是最终找不到调用的方法的时候,系统会调用-(void) forwardInvocation: (NSInvocation*) invocation 方法,如果你的对象没有实现这个方法,就调用 NSObject 的 forwardInvocation 方 法,那句不能识??消息的错误,实际就是 NSObject 的 forwardInvocation 抛出来的异常。 我们这里告诉你这个系统内部的实现过程,实际是要告诉你,你可以覆盖 forwardInvocation 方法,来改变 NSObject 的抛异常的处理方式。譬如:你可以把 A 不能处理的消息转发给 B 去处理。 NSInvocation 是一个包??了 receiver、selector 的对象,也就是它包??了向一个对象发送消息 的所有元素:对象、方法名、参数序列,你可以调用 NSInvocation 的 invoke 方法将这个消息 激活。 例:
下面我们来详细分析一下如果你想把不能处理的消息转发给其他的对象,需要经过哪个几个 步骤: A.首先,你要覆盖NSObject中的methodSignatureForSelector方法。这是因为你如果想把消 息fly从Person转发给Bird处理,那么你必须将NSInvocation中包??的Person的fly的方法签 名转换为Bird的fly的方法签名,也就是把方法签名纠正一下。 由此,你也看出来NSInvocation的创建,内部使用了两个对象,一个是receiver,一个是 NSMethodSignature,而NSMethodSignature是由SEL创建的。NSInvocation确实存在一个类方 法invocationWithMethodSignature返回自身的实例。 B.然后我们覆盖forwardInvocation方法,使用的不是invoke方法,而是invokeWithTarget方法, 也就是把调用权由self转交给bird。 实际上消息转发机制不仅可以用来处理找不到方法的错误,你还可以变相的实现多继承。假 如我们的 Person 想要拥有 Bird、Fish 的所有功能,其实你可以尽情的用 Person 的实例调用 Bird、Fish 的方法,只要在 Person 的 forwardInvocation 里,把消息的响应权转交给 Bird 或者 Fish 的实例就可以了。 [ 此帖被YiManFly在2015-03-01 17:29重新编辑 ]
|
|