參考資料:
https://my.oschina.net/iq19900204/blog/411450
http://blog.csdn.net/devday/article/details/7418022
1. NSProxy和NSObject
基本所有的iOS中的類都是NSObject的字類,但是NSProxy不是。
NSProxy是一個虛類,你可以通過繼承它,并重載下面兩個方法以實現將消息轉發到另一個實體。
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
這里最好描述一下虛類的概念:虛類又叫做抽象類,這個類主要定義一些方法然后讓子類去實現。正如有人描述的,動物是一個大概念,但是通常情況下你不會去定義動物的對象;而是先產生繼承的字類貓啊,狗啊的,再去實例化。這個動物就可以用虛類來表示了,畢竟動物是有共性的。以上純屬個人理解,非官方語言描述!
NSObject既是對象的基類,又是一種協議。它的頭文件是這樣的:
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
而NSObject協議的定義是這樣的:
#include <objc/objc.h>
#include <objc/NSObjCRuntime.h>
@class NSString, NSMethodSignature, NSInvocation;
@protocol NSObject
- (BOOL)isEqual:(id)object;
@property (readonly) NSUInteger hash;
@property (readonly) Class superclass;
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'anObject.dynamicType' instead");
- (instancetype)self;
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
- (BOOL)isProxy;
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE;
- (struct _NSZone *)zone OBJC_ARC_UNAVAILABLE;
@property (readonly, copy) NSString *description;
@optional
@property (readonly, copy) NSString *debugDescription;
@end
很全很熟悉不是嗎?!
NSProxy一個虛類,但是同時它也實現了NSObject協議,它的定義是這樣的:
@interface NSProxy <NSObject> {
Class isa;
}
所以NSProxy的字類可以實現NSObject協議中的一些方法。
2. 方法重定向
比較優秀的iOS工程師應該都知道對象在調用方法時的機制,會尋找當前類的方法緩存,方法鏈表;然后依次向父類去尋找,一直到NSObject,到了NSobject還是找不到的話,我們有機會使用上面的方法來轉移方法的注意力了。
到這里我本來想引用另一個博客中定義了NSProxy父類的方法,但是博主不允許隨便轉載,大家有空自己去看吧。
http://blog.csdn.net/devday/article/details/7418022
我們先定義一個TestTool類。
//TestTool.h
@interface TestTool : NSObject
{
id star;
}
- (instancetype)initWithId:(id)obj;
//- (void)test;
@end
//TestTool.m
- (instancetype)initWithId:(id)obj
{
star = [obj copy];
return self;
}
請注意這里我們沒有定義test方法。如果我們對TestTool對象調用該方法,毫無疑問會崩潰的。(請注意這里編譯器如何不報錯,使用id對象)
這時重定向的方法可以起作用了,在TestTool.m中加入以下方法:
//如果可以在m文件內部捕捉到這些方法則這些都不會調用
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"調用了forwardInvocation方法");
[anInvocation invokeWithTarget:star];
}
//方法簽名需要和NSInvocation聯合使用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
//對于NSObject的字類,如果頭文件中沒有聲明該方法,那么直接編譯錯誤
//而如果頭文件聲明,但是在m文件中找不到實現,則會調用該方法,從而有機會使用別的對象來實現
NSLog(@"檢驗本類中該函數的簽名");
NSMethodSignature *sig;
if ([star methodSignatureForSelector:aSelector])
{
//如果這里不把方法簽名傳到star上也會報錯的,test方法找不到實現的目標
sig = [star methodSignatureForSelector:aSelector];
}
return sig;
}
很明顯,在methodSignatureForSelector:中,我們將試圖把star的方法簽名賦予Target;而forwardInvocation:則指定了方法實現的目標。兩者缺一不可。
接著我們試著去測試它,先定義重定向的目標類,當然它最好有test方法了。
//Another.h
@interface Another : NSObject<NSCopying>
- (void)test;
@end
- (void)test
{
NSLog(@"這里實現了test方法");
}
- (id)copyWithZone:(NSZone *)zone
{
return [[[self class] allocWithZone:zone] init];
}
OK,現在我們可以正式去看看重定向的結果了:
Another *another = [Another new];
id tool = [[TestTool alloc] initWithId:another];
[tool test];
可以看到打印的結果:這里實現了test方法.
3.注意
上面值得注意的一點是記得TargetProxy或者其他對象初始化時返回的是id,而不直接指定為該類的對象,否則編譯器將去該類的父類簇的方法鏈表中尋找該方法,導致直接編譯出錯,這樣連消息重定向的機會都沒有了。
4.再看看
本來研究到這里告一段落了,和我之前留下的印象差不多。但是看到NSObject中的一些方法,忍不住拿來玩了一下。
+ (BOOL)instancesRespondToSelector:(SEL)aSelector
{
NSLog(@"調用instancesRespondToSelector");
return YES;
}
+ (BOOL)conformsToProtocol:(Protocol *)protocol
{
NSLog(@"調用conformsToProtocol");
return YES;
}
- (IMP)methodForSelector:(SEL)aSelector
{
NSLog(@"調用methodForSelector");
IMP imp = [self methodForSelector:aSelector];
return imp;
}
+ (IMP)instanceMethodForSelector:(SEL)aSelector
{
NSLog(@"調用instanceMethodForSelector");
IMP imp = [self instanceMethodForSelector:aSelector];
return imp;
}
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"調用doesNotRecognizeSelector");
}
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"調用forwardingTargetForSelector");
return self;
}
+ (NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *sig = [self instanceMethodSignatureForSelector:aSelector];
NSLog(@"調用instanceMethodSignatureForSelector");
return sig;
}
大多數方法含義是明確的,但是確實發現forwardingTargetForSelector:這個方法會在消息轉移之前被調用。
查閱了一些資料后,它的作用是可以直接將要轉發的對象返回。你應該有辦法去嘗試這個方法的作用。
A如果想要把一封信交給B,可以直接把B叫到家里來;也可以給B送過去,這個機制可謂相當厲害了。
在以上3個方法都沒有找到消息接收者的情況下,系統大概也沒辦法了,只能給你最后一次補救的機會:
- (void)doesNotRecognizeSelector:(SEL)aSelector;
我沒能找到這個方法,你自己看著辦吧,我要拋出異常了。好吧,到這里應用基本上就會崩潰了。
5.小結
最后總結一下呢,就是調用方法的消息發出后,先去該類及類別的方法鏈表中去找,找不到就去找父類了;直到最后找到基類:NSObject.
然后會依次調用下面幾個方法(在沒有消息響應者的情況下):
- (id)forwardingTargetForSelector:(SEL)aSelector;
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (void)doesNotRecognizeSelector:(SEL)aSelector;
另外補一句,使用try...catch方法也可以不崩潰,不過個人不是特別感冒這個。問題隱藏了不代表不存在,不是嗎?