oc消息转发机制
2016-05-11 16:30
274 查看
转载于-----http://wxgbridgeq.github.io/blog/2015/07/09/effective-oc-note-second/
Class 类型对象
OC本身是一种强类型语言,但其运行时功能让它又有了动态语言的特点。OC中对象的类型和对象所执行的方法都是在运行时阶段进行查找并确认的,这种机制被称为动态绑定。想要弄清楚运行时如何能够实现动态绑定机制,首先要了解OC中对象的本质。OC是C语言的超集,所以OC中面向对象的功能在底层也是使用C语言来实现。我们在OC中使用的对象,通常指的是储存该对象内存地址的一个指针变量(Java中称为引用),因此我们在OC中声明对象时通常使用类型名称加一个
*号,稍微了解C语言的人都知道
*号代表该变量是一个指针变量。OC中还有一个特殊的类型
id,它可以表示通用类型的OC对象,因为它本身就被定义为一种特殊的指针变量,所以不需要在id后面再加一个
*号。
1 2 3 4 5 | NSString *someString = 4000 @"Some String"; id otherString = @"Other String"; [someString count]; // 编译期报错 [otherString count]; // 运行时报错 |
我们可以在苹果官方的运行时库的头文件中查看id类型的定义:
1 2 3 4 | struct objc_object { Class isa; }; typedef struct objc_object *id; |
isa(取意is
a,是一个),代表着对象所属的具体类型。其实在NSObject类的头文件中,同样声明有一个这样的实例变量isa。因此,可以说OC中任何对象,都会默认带有一个实例变量isa用来储存对象的具体类型信息。
Class的定义也可以在运行时库的头文件中查看:
1 2 3 4 56 | struct objc_class { Class isa; Class super_class; const char *name; long version; long info; long instance_size; struct objc_ivar_list *ivars; struct objc_method_list **methodLists; struct objc_cache *cache; struct objc_protocol_list *protocols; }; typedef struct objc_class *Class; |
元数据(metadata)。该结构体也有一个Class类型的成员isa,说明Class本身也是一个OC对象(被称为类对象或类型对象),而它的对象类型(isa所指向的类型)被称为
元类(metaclass),元类中储存的是类对象的元数据,比如类方法就储存在这里。每个类可以有无数个对象,但仅有一个类对象,也仅有一个与之对应的元类。
对象、类对象和元类的关系如下图所示:
![](http://7xk1wz.com1.z0.glb.clouddn.com/eoc01.png)
由于类对象和isa指针的存在,OC中的所有对象都可以在运行时查找自己的真实类型,并确定自己所能执行的方法。当真正给对象发送一条消息(或称为调用方法)时,运行时机制会对该消息进行一系列复杂的处理,接下来我们就继续讨论运行时的消息处理。
Message Dispatch 消息派发
调用对象的某个方法(或称为给对象发送某个消息)是面向对象编程中最常使用的功能。在OC中,由于动态绑定机制使得程序直到运行时才能清楚那个方法需要被执行,甚至通过使用底层的运行时函数,就可以更改调用的方法或改变方法内部的功能实现,这些特性使得OC成为一门真正的动态语言。1 | id returnValue = [someObject messageName:param]; |
objc_msgSend,该函数在头文件中的声明如下:
1 | id objc_msgSend(id self, SEL op, ...) |
1 | id returnValue = objc_msgSend(someObject, @selector(messageName:), param); |
Message Forward 消息转发
消息转发流程比较复杂,主要分三个步骤,首先我们来看一张消息转发完整的流程图。![](http://7xk1wz.com1.z0.glb.clouddn.com/eoc02.png)
第一步
当消息派发流程最终在对象的类和父类中都没有找到对应选择器的方法时,就会开启消息转发流程。首先,第一步会先调用消息接收者所在类的resolveInstanceMethod:方法,该方法返回一个BOOL值,表示是否动态添加一个方法来响应当前消息选择器。如果发送的消息是一个类方法,则会调用另一个类似的方法
resolveClassMethod:。
12 | + (BOOL)resolveInstanceMethod:(SEL)sel; + (BOOL)resolveClassMethod:(SEL)sel; |
resolveInstanceMethod:方法并返回YES,也就意味着想要动态添加一个方法来响应当前的消息选择器,可以在重写的方法内使用
class_addMethod函数来为当前类添加方法。
1 | BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) |
Runtime Programming Guide)。
第二步
如果上一步过程中,并没有新方法能响应消息选择器,则会进入消息转发流程的第二步。在第二步中系统会调用当前消息接收者所在类的forwardingTargetForSelector:方法,用以询问能否将该条消息发送给其他接收者来处理,方法的返回值就代表这个新的接收者,如果不允许将消息转发给其他接收者则返回nil。
1 | - (id)forwardingTargetForSelector:(SEL)aSelector; |
forwardingTargetForSelector:方法,根据这些属性所能响应的消息选择器返回对应的属性对象,这样在外界看起来,该类的对象就好像是能够处理多种不同类型的方法了。
第三步
如果forwardingTargetForSelector:方法的返回值为nil,那么消息转发机制还要继续进行最后一步。在这一步中,系统会将尚未处理的消息包装成一个
NSInvocation对象,其内部包含与该消息相关的所有信息,比如消息的选择器、目标接收者、参数等。之后系统会调用消息接收者所在类的
forwardInvocation:方法,并将生成的
NSInvocation对象作为参数传入。
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation; |
forwardInvocation:方法同样声明在NSObject类中,我们可以重写该方法的实现。比如将
NSInvocation对象的
target属性设置为其他接收者,此操作可以实现与上一步操作同样的效果,但明显在效率上没有第二步的操作高,所以很少有人在这一步中仅仅只是改变消息的接收者。
NSInvocation类中还提供了许多属性和方法用于修改其对应方法的信息,比如可以修改方法的参数和返回值,或者直接更改消息选择器转而调用其他方法。
如果消息接收者在这一步中仍然无法响应消息选择器,那么系统会自动调用
doesNotRecognizeSelector:方法,该方法默认实现为抛出异常,也就是我们在开发中经常见到的unrecognized
selector sent to instance。
1 | -[ViewController count]: unrecognized selector sent to instance |
消息转发示例
现在再回头看我们之前消息转发完整的流程图,应该能够更清晰地了解系统执行每一步操作的目的和作用了。接下来我们用一个示例来演示如何利用消息转发机制来自定义一个字典类,该字典类的对象可以直接使用属性方式来存取内容。完整的示例代码如下。1 2 3 4 56 | // WXGAutoDictionary.h #import <Foundation/Foundation.h> @interface WXGAutoDictionary : NSObject // 可供存储的属性,可以为任意OC对象 @property (nonatomic, strong) id obj; @end // WXGAutoDictionary.m #import "WXGAutoDictionary.h" #import <objc/runtime.h> @interface WXGAutoDictionary () @property (nonatomic, strong) NSMutableDictionary *backStore; // 后台存储用字典 @end @implementation WXGAutoDictionary @dynamic obj; // 禁止编译器自动生成getter和setter方法 - (instancetype)init { if (self = [super init]) { _backStore = @{}.mutableCopy; // 初始化字典 } return self; } // 重写此方法,允许动态添加方法来响应指定的消息选择器 + (BOOL)resolveInstanceMethod:(SEL)sel { NSString *selString = NSStringFromSelector(sel); // 类型编码:v->void @->OC对象 :->SEL选择器 // 响应setter方法的选择器 if ([selString hasPrefix:@"set"]) { class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@"); } else { // 响应getter方法的选择器 class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:"); } return YES; } // 处理setter方法的函数 void autoDictionarySetter(id self, SEL sel, id value) { WXGAutoDictionary *autoDict = (WXGAutoDictionary *)self; NSMutableDictionary *backStore = autoDict.backStore; NSString *selString = NSStringFromSelector(sel); NSMutableString *key = selString.mutableCopy; [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)]; [key deleteCharactersInRange:NSMakeRange(0, 3)]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:[[key substringToIndex:1] lowercaseString]]; if (value) { [backStore setObject:value forKey:key]; } else { [backStore removeObjectForKey:key]; } } // 处理getter方法的函数 id autoDictionaryGetter(id self, SEL sel) { WXGAutoDictionary *autoDict = (WXGAutoDictionary *)self; NSMutableDictionary *backStore = autoDict.backStore; NSString *key = NSStringFromSelector(sel); return [backStore objectForKey:key]; } @end |
1 2 3 4 56 | // main.m #import <Foundation/Foundation.h> #import "WXGAutoDictionary.h" int main(int argc, const char * argv[]) { @autoreleasepool { WXGAutoDictionary *dict = [[WXGAutoDictionary alloc] init]; dict.obj = [NSDate date]; NSLog(@"%@", dict.obj); // 控制台输出当前日期 } return 0; } |
resolveInstanceMethod:方法中返回YES,并为不同选择器指定了不同的方法去处理,从而实现通过属性的setter和getter方法对字典进行存取操作。当有另一个类型的属性需要使用同样的功能时,只需在
WXGAutoDictionary类中添加属性,并将属性声明为
@dynamic即可,属性的存取操作会由运行时系统动态指定方法来完成。
Method Swizzing 方法调配
我们已经了解了OC中对象的类型和消息处理机制,这些有助于我们进一步了解OC运行时的其他功能和特性。接下来就介绍其中一种叫做Method Swizzing(方法调配)的技术,该技术经常被称为iOS开发中的黑魔法。
在介绍方法调配技术之前,我们首先来了解一下OC中方法和消息选择器之间的关系,因为我们经常会将他们混为一谈。在运行时头文件中,我们可以找到方法的底层结构定义。
1 2 3 4 5 | struct objc_method { SEL method_name; char *method_types; IMP method_imp; } |
首先,当对象接收到某个消息时,编译器首先将代码转换为objc_msgSend函数,并将消息的接收者和选择器当做函数的参数传入,接下来系统会根据接收者的isa指针找到它所对应的类,在类的元数据信息中找到该类所拥有的方法列表,然后遍历方法列表,将每一个方法内部的SEL选择器同传入的消息选择器进行匹配,当找到相同的选择器后,就根据方法内部的IMP函数指针跳转到方法的具体实现。当然,为了提高方法多次执行的效率,系统会将遍历查询的结果缓存起来,储存在类的元数据信息中,此处就不再继续深入讨论。
了解清楚选择器和方法实现之间的一对一关系后,我们接下来开始介绍方法调配技术,它其实就是利用运行时提供的函数来动态修改选择器和方法实现之间的对应关系的一种技术。利用这种技术,我们可以在运行时为某个类添加选择器或更改选择器所对应的方法实现,甚至可以更换两个已有选择器所对应的方法实现,从而实现一种极其诡异的效果。下面就写一段示例程序,通过方法调配技术来更换NSString类的大小写转换方法的实现(仅供娱乐使用)。
1 2 3 4 56 | // main.m #import <Foundation/Foundation.h> #import <objc/runtime.h> int main(int argc, const char * argv[]) { @autoreleasepool { Method lowercase = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method uppercase = class_getInstanceMethod([NSString class], @selector(uppercaseString)); method_exchangeImplementations(lowercase, uppercase); NSLog(@"%@ -- %@", [@"AbCd" lowercaseString], [@"AbCd" uppercaseString]); // 输出结果:ABCD -- abcd } return 0; } |
lowercaseString方法返回的是大写字母,而
uppercaseString方法返回了小写字母。
方法调配技术的作用肯定不在于此,那么开发者通常如何使用这种技术呢?在总结方法调配技术的用处之前,我们先再来看一个示例程序。同样以NSString类为例,我们为其
lowercaseString方法增加一些日志输出功能(不改变方法名,只是更改方法的实现)。你可能第一时间想到用继承来实现该需求,然而当项目中有多个类需要同样需求时,你需要每个类都去继承一下,然后还要保证别人都是去用你的子类而不是原本的父类,这样显然并不是一种很好的解决办法。此时我们就可以尝试使用方法调配技术,完整的示例代码如下。
1 2 3 4 56 | // NSString+Logging.h #import <Foundation/Foundation.h> @interface NSString (Logging) - (NSString *)lowercaseStringWithLogging; @end // NSString+Logging.m #import "NSString+Logging.h" @implementation NSString (Logging) - (NSString *)lowercaseStringWithLogging { NSString *lowercaseString = [self lowercaseStringWithLogging]; NSLog(@"%@ -> %@", self, lowercaseString); return lowercaseString; } // main.m #import <Foundation/Foundation.h> #import <objc/runtime.h> #import "NSString+Logging.h" int main(int argc, const char * argv[]) { @autoreleasepool { Method lowercase = class_getInstanceMethod([NSString class], @selector(lowercaseString)); Method lowercaselogging = class_getInstanceMethod([NSString class], @selector(lowercaseStringWithLogging)); method_exchangeImplementations(lowercase, lowercaselogging); [@"AbCd" lowercaseString]; // 输出结果:AbCd -> abcd } return 0; } @end |
[self lowercaseStringWithLogging],这看上去应该会使程序陷入死循环,但不要忘了,我们在main方法中利用方法调配技术来交换原有类的方法和分类方法的实现,所以这句代码实际上执行的是原本的类中的实现,并不会造成死循环。
通过上文的示例程序,我们可以为那些完全不知道具体实现的方法(也称为黑盒方法)增加日志输出功能,这常用于程序的调试。实际上,还有很多与此类似的需求,既要增加功能,又需要与原有方法联系很紧密,例如增加权限验证和缓存功能,这类需求常被人们称为
Aspect(切面),与之对应的编程概念叫做
Aspect Oriented Programming(面向切面编程)。面向切面编程的概念有许多优点,它将那些琐碎的事物从主逻辑中分离出来,并将它们附加在与主逻辑相对应的横向切面中连带执行,是对面向对象编程的一种补充。在OC中,我们可以利用运行时特性和方法调配技术来实现这类面向切面编程的需求。
相关文章推荐
- 李想:霸道总裁——80后亿万富翁的发家史!
- 浅谈SQL语句执行顺序分析
- 2 Tables and Table Clusters读书笔记
- 自定义view 之 继承
- 蓝桥杯:最大最小公倍数
- 递归
- 安装过程中出现PKG_CONFIG_PATH的问题解决方法
- 【HUSTOJ】1106: 回文素数
- oracle环境安装一些我的bugs
- 欢迎使用CSDN-markdown编辑器
- java集合类深入分析之PriorityQueue
- 屏蔽优酷、土豆等视频网站15秒广告的最全最简单方法
- 二叉树的深度优先遍历与广度优先遍历
- MFC重绘关闭按钮,并给图片添加点击事件
- 在jsp页面中对浏览器类型判断~
- 必看的 jQuery性能优化的38个建议
- Failed to set (keyPath) user defined inspected property on (UIView): [<UIView 0x7994f790> setValue:f
- 欢迎使用CSDN-markdown编辑器
- 踢掉某个li
- 对动态规划算法思想的一些理解