消息转发机制入门篇
一.何时处发消息转发机制?
解:当对象接收到无法解读的消息后,就会启动“消息转发”(message forwarding)机制,程序员可经由此过程告诉对象应该如何处理未知消息。
如:-[__NSCFNumber lowercaseString] :unrecognized selector sent to instance 0x87
上面这段异常信息是由NSObjc 的”doesNotRecognizeSelector”方法所抛出的,此异常表明:消息接收者的类型是_ _NSCFNumber,而该接收者无法理解名为lowercaseString的选择子。
二.为什么要用消息转发?
解:1.Obj-C用起来真是各种happy,比如现在有这样一种情况:有一个类,我们希望它能响应一个消息(message),但是这个类没有相应的方法(method),而你又偏偏不能重写/继承这个类。这时我们可能会想到,能不能动态地给类添加一个方法呢?感谢Obj-C,仅需简单几步就能实现。
2.当用@property定义,用@dynamic实现的话,会用的消息转发。
3.当某个方法没有实现时,会调用消息转发机制。
调用前:
2016-04-17 00:05:42.074 MessageForwarding[927:70955] -[MessageForwarding running]: unrecognized selector sent to instance 0x7fee615169c0 2016-04-17 00:05:42.076 MessageForwarding[927:70955] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[MessageForwarding running]: unrecognized selector sent to instance 0x7fee615169c0' *** First throw call stack: ( 0 CoreFoundation 0x00000001106dfe65 __exceptionPreprocess + 165 1 libobjc.A.dylib 0x0000000110158deb objc_exception_throw + 48 2 CoreFoundation 0x00000001106e848d -[NSObject(NSObject) doesNotRecognizeSelector:] + 205 3 CoreFoundation 0x000000011063590a ___forwarding___ + 970 4 CoreFoundation 0x00000001106354b8 _CF_forwarding_prep_0 + 120 5 MessageForwarding 0x000000010fc590d3 -[ViewController viewDidLoad] + 99 6 UIKit 0x0000000110c22f98 -[UIViewController loadViewIfRequired] + 1198 7 UIKit 0x0000000110c232e7 -[UIViewController view] + 27 8 UIKit 0x0000000110af9ab0 -[UIWindow addRootViewControllerViewIfPossible] + 61 9 UIKit 0x0000000110afa199 -[UIWindow _setHidden:forced:] + 282 10 UIKit 0x0000000110b0bc2e -[UIWindow makeKeyAndVisible] + 42 11 UIKit 0x0000000110a84663 -[UIApplication _callInitializationDelegatesForMainScene:transitionContext:] + 4131 12 UIKit 0x0000000110a8acc6 -[UIApplication _runWithMainScene:transitionContext:completion:] + 1760 13 UIKit 0x0000000110a87e7b -[UIApplication workspaceDidEndTransaction:] + 188 14 FrontBoardServices 0x000000011345b754 -[FBSSerialQueue _performNext] + 192 15 FrontBoardServices 0x000000011345bac2 -[FBSSerialQueue _performNextFromRunLoopSource] + 45 16 CoreFoundation 0x000000011060ba31 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17 17 CoreFoundation 0x000000011060195c __CFRunLoopDoSources0 + 556 18 CoreFoundation 0x0000000110600e13 __CFRunLoopRun + 867 19 CoreFoundation 0x0000000110600828 CFRunLoopRunSpecific + 488 20 UIKit 0x0000000110a877cd -[UIApplication _run] + 402 21 UIKit 0x0000000110a8c610 UIApplicationMain + 171 22 MessageForwarding 0x000000010fc59a1f main + 111 23 libdyld.dylib 0x0000000112e1b92d start + 1 ) libc++abi.dylib: terminating with uncaught exception of type NSException
调用后:
2016-04-17 00:09:22.577 MessageForwarding[950:73397] message.string=昆昆,message.number=29,message.date=2033-04-25 08:58:42 +0000
三.消息转发的过程?
1.过程
解:第一阶段:先征询接收者,所属的类,看其是否能动态添加方法,以处理当前这个“未知的选择子”(unknown selector)这叫做动态方法解析(dynamic method resolution)。
第二阶段:涉及“完整的消息转发机制”(full forwarding mechanism)如果运行期系统已经把第一阶段执行完了,那么接收者自己就无法再以动态新增方法来响应包含该选择子的消息了。此时,运行期系统会请求接收者以其他手段来处理与消息相关的方法调用。这细分两小步,首先,请接收者看看有没有其他对象能处理这消息。若有,则运行期系统会
把消息转给那个对象,于是消息转发结束,一切如常。若没有“备援的接收者”(replacement recever),则启动完整的消息转发机制,运行期系统会把与消息有关的全部细节都封装到NSInvocation 对象中,再给接收者最后一次机会,令其设法解决当前还未处理的这条消息。
2.方法如下:
+ (BOOL)resolveInstanceMethod:(SEL)sel;
该方法的参数就是那个未知的选择子,其返回值为Boolean类型,表示这个类是否能新增一个实例方法用以处理此选择子。
假如尚未实现的方法不是实例方法而是类方法,那么运行期系统就会调用另个一个方法,该方法与+ (BOOL)resolveInstanceMethod:(SEL)sel类似叫做:+(BOOL)resolveClassMethod:(SEL)sel。
3.使用前提
相关方法的实现代码已经写好,只等着运行的时候动态插在类里面就可以了。此方案常用来实现@dynamic 属性,比如要访问CoreData框架中NSManagedObjects 对象属性时就可以这么做,因为实现这些属性所需的存取方法在编译期就能确定。
4.代码先行
下面代码演示了如何用“resolveInstanceMethod:”来实现@dynamicnt属性。
#import <Foundation/Foundation.h> @interface MessageForwarding : NSObject id autoDictionaryGetter(id self,SEL _cmd); //setter 方法 void autoDictionarySetter(id self,SEL _cmd,id value); //getter 方法 /** * 动态解析方法 * * @param sel 未知的选择子 * * @return 这个类是否能新增一个实例方法用以处理此选择子 */ + (BOOL)resolveInstanceMethod:(SEL)sel; @property (nonatomic, strong) NSString *string; @property (nonatomic, strong) NSNumber *number; @property (nonatomic, strong) NSDate *date; @property (nonatomic, strong) id opaqueObject; @end #import "MessageForwarding.h" #import <objc/runtime.h> @interface MessageForwarding() @property(nonatomic, strong)NSMutableDictionary *backingStore; @end @implementation MessageForwarding @dynamic string,date,number,opaqueObject; #pragma mark getter and setter id autoDictionaryGetter(id self,SEL _cmd){ //Get the backing store from the object MessageForwarding *typeSelf = (MessageForwarding *)self; NSMutableDictionary *backingStore = typeSelf.backingStore; //the key is simply the selector name NSString *key = NSStringFromSelector(_cmd); //return the value return [backingStore objectForKey:key]; } void autoDictionarySetter(id self,SEL _cmd,id value){ //back the backing store from the object MessageForwarding *kunkunSelf = (MessageForwarding *)self; NSMutableDictionary *backStore = kunkunSelf.backingStore; /** The selector will be for example ,"setOpaqueObject" //以这个选择器为例,“setopaqueobject” we need to remove the "set",":" and lowercase the first //我们必须删除set和:和第一个字母小写 letter of the remainder */ NSString *selectorString = NSStringFromSelector(_cmd); NSMutableString *key = [selectorString mutableCopy]; //Remove the : at the end //在最后删除: [key deleteCharactersInRange:NSMakeRange(key.length -1, 1)]; //Remove the "set" prefix [key deleteCharactersInRange:NSMakeRange(0, 3)]; //lowercase the first character NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString]; [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar]; if (value) { [backStore setObject:value forKey:key]; }else{ [backStore removeObjectForKey:key]; } } #pragma init - (instancetype)init{ if (self = [super init]) { _backingStore = @{}.mutableCopy; } return self; } #pragma mark message forwarding 消息转发机制 + (BOOL)resolveInstanceMethod:(SEL)sel{ //消息转化为字符串 NSString *selectorString = NSStringFromSelector(sel); // if(/**selector is from a @dynamic property*/){ if ([selectorString hasPrefix:@"set"]) { class_addMethod (self,sel,(IMP)autoDictionarySetter,"v@:@"); }else{ class_addMethod(self, sel,(IMP)autoDictionaryGetter,"@@:"); } return YES; // }// // return [super resolveInstanceMethod:sel]; } @end
5.备援接收者
当前接收者还有第二次机会能处理未知的选择子,在这一步中,运行期系统会问它:能不能把这条消息转发给其他接收者处理,方法如下:
/** * 备援接收者,当第一次未处理时,才会调用它未调用时resolveInstanceMethod:(SEL)sel,请注意,我们无法操作由这一步所转发的消息,若是想在发送给备援接收者之前先修改消息内容,那就得通过完整的消息转发机制来做了。 * * @param aSelector 消息 * * @return 若当前接收者能找到备援对象,则将其返回,若找不到,就返回nil */
6.完整的消息转发
- (id)forwardingTargetForSelector:(SEL)aSelector; /** * 完整消息转发,在触发NSInvocation 对象时,“消息派发系统”(message dispatch system)将亲自出马,把消息指派给目标对象 * * @param anInvocation <#anInvocation description#> */
消息转发全流程
resolveInstanceMethod —>返回YES -->消息已处理
—> forwardingTargetForSelector -->接收相应的值 --->消息已处理
-->返回nil-->forwardInvocation -->消息已处理
--->消息未处理