什么是 NSProxy
首先,我們看下官方給出的定義

NSProxy 是一個為對象定義接口的抽象父類,它為充當其他對象或尚不存在的對象的替代對象的對象定義API。通常,發(fā)送給代理的消息會轉(zhuǎn)發(fā)到真實對象,或使代理加載(或?qū)⒆陨磙D(zhuǎn)換為)真實對象。 NSProxy 的子類可用于實現(xiàn)透明的分布式消息傳遞(例如 NSDistantObject)或用于懶惰實例化創(chuàng)建成本高昂的對象。NSProxy 實現(xiàn)了根類所需的基本方法,包括在 NSObject 協(xié)議中定義的方法。但是,作為抽象類,它不提供初始化方法,并且在收到任何未響應的消息時會引發(fā)異常。因此,具體的子類必須提供初始化或創(chuàng)建方法,并覆蓋 forwardInvocation: 和 methodSignatureForSelector:方法以處理自身未實現(xiàn)的消息。子類的 forwardInvocation 的實現(xiàn):應該執(zhí)行處理調(diào)用所需的任何事情,例如通過網(wǎng)絡(luò)轉(zhuǎn)發(fā)調(diào)用或加載實際對象并將其傳遞給調(diào)用。 methodSignatureForSelector: 需要為給定消息提供參數(shù)類型信息;子類的實現(xiàn)應能夠確定需要轉(zhuǎn)發(fā)的消息的參數(shù)類型,并應相應構(gòu)造一個 NSMethodSignature 對象。有關(guān)更多信息,請參見 NSDistantObject,NSInvocation 和 NSMethodSignature 類規(guī)范。
NSProxy是和NSObject同級的一個類(即常說的虛基類),它只是實現(xiàn)了<NSObject>的協(xié)議。是一個抽象類,沒有指定初始化方法,子類化后需要提供初始化方法;使用時需要實現(xiàn)基類以及NSObject協(xié)議的一些方法(forwardInvocation:、methodSignatureForSelector:)
NS_ROOT_CLASS
@interface NSProxy <NSObject> {
__ptrauth_objc_isa_pointer Class isa;
}
+ (id)alloc;
+ (id)allocWithZone:(nullable NSZone *)zone NS_AUTOMATED_REFCOUNT_UNAVAILABLE;
+ (Class)class;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
- (void)dealloc;
- (void)finalize;
@property (readonly, copy) NSString *description;
@property (readonly, copy) NSString *debugDescription;
+ (BOOL)respondsToSelector:(SEL)aSelector;
- (BOOL)allowsWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);
// - (id)forwardingTargetForSelector:(SEL)aSelector;
@end
可以看到,它的第一個成員變量是 isa,因此它可以拿來當一個 NSObject 或其派生類來使用。
NSProxy 的使用
從上述知道,使用 NSProxy 必須通過它的子類實現(xiàn),且子類需要提供初始化方法,并且重寫 forwardInvocation: 和 methodSignatureForSelector: 方法來處理自己沒有實現(xiàn)的消息。(即負責把消息轉(zhuǎn)發(fā)給真正的 target 的代理類)
- 創(chuàng)建一個
LCProxy類,繼承自NSProxy
/**------.h------*/
@interface LCProxy : NSProxy
- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTransformObject:(id)target;
@end
/**------.m------*/
@interface LCProxy ()
@property (nonatomic, weak) NSObject *target;
@end
@implementation LCProxy
+ (instancetype)proxyWithTransformObject:(id)target {
return [[self alloc] initWithTarget:target];
}
- (instancetype)initWithTarget:(id)target {
self.target = target;
return self;
}
// 方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
if (self.target && [self.target respondsToSelector:sel]) {
return [self.target methodSignatureForSelector:sel];
}
return [super methodSignatureForSelector:sel];
}
// 調(diào)用方法實現(xiàn)
- (void)forwardInvocation:(NSInvocation *)invocation {
SEL sel = invocation.selector;
if (self.target && [self.target respondsToSelector:sel]) {
[invocation invokeWithTarget:self.target];
} else {
[super forwardInvocation:invocation];
}
}
@end
可以看到,我們重寫了 methodSignatureForSelector: 和 forwardInvocation: 兩個方法。第一個方法是用來做實現(xiàn)方法簽名,將我們復制的對象進行方法簽名,然后生成 NSInvocation 對象;第二個方法接著就會被調(diào)用,我們直接將 invocation 的 target 設(shè)置為我們復制的對象。實質(zhì)上是利用了 runtime 的消息轉(zhuǎn)發(fā)機制
使用場景
- 解決 NSTimer/CADisplayLink 的循環(huán)引用問題
在之前的文章 iOS 中關(guān)于 NSTimer 的強引用分析 有過分析為什么會有循環(huán)引用的問題,這里不再啰嗦了。
- 模擬多繼承
@interface LCTeacher : NSObject
- (void)teacherSomething;
@end
@implementation LCTeacher
- (void)teacherSomething {
NSLog(@"%s", __func__);
}
@end
@interface LCStudent : NSObject
- (void)studySomething;
@end
@implementation LCStudent
- (void)studySomething {
NSLog(@"%s", __func__);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
LCTeacher *teacher = [[LCTeacher alloc] init];
LCStudent *student = [[LCStudent alloc] init];
LCProxy *teacherProxy = [LCProxy proxyWithTransformObject:teacher];
[teacherProxy performSelector:NSSelectorFromString(@"teacherSomething") withObject:nil];
LCProxy *studentProxy = [LCProxy proxyWithTransformObject:student];
[studentProxy performSelector:NSSelectorFromString(@"studySomething") withObject:nil];
}
通過 LCProxy 這個類實現(xiàn)了復制 LCTeacher 和 LCStudent,可以分別調(diào)用各個類的方法了。
NSProxy 和 NSObject 的區(qū)別
NSProxy 和 NSObject 都定義了 forwardInvocation: 和 methodSignatureForSelector: 方法,但這兩個方法并沒有在 protocol NSObject 中聲明;兩者對這倆方法的調(diào)用邏輯更是完全不同
對于 NSObject,接收到消息后先去類的 methodlist 方法列表里找匹配的 selector,如果找不到,會沿著它的繼承鏈去 superclass 的方法列表中查找;繼承鏈中也找不到,依次會調(diào)用 +resolveInstanceMethod: 和 -forwardingTargetForSelector: 處理,如果還處理不了,才會走到 -methodSignatureForSelector:/-forwardInvocation: 給最后一次機會。
對于 NSProxy,接收 unknown selector 后,會回調(diào) -forwardingTargetForSelector:,如果處理不了就會繼續(xù)回調(diào) -methodSignatureForSelector:/-forwardInvocation:,消息轉(zhuǎn)發(fā)過程比 NSObject 要簡單得多。
相對于 NSObject,NSProxy 的另外一個非常重要的不同點也值得注意:NSProxy 會將自省相關(guān)的 selector 直接 forward 到 -forwardInvocation: 回調(diào)中,這些自省方法包括:
- (BOOL)isKindOfClass:(Class)aClass;
- (BOOL)isMemberOfClass:(Class)aClass;
- (BOOL)conformsToProtocol:(Protocol *)aProtocol;
- (BOOL)respondsToSelector:(SEL)aSelector;
簡單來說,這 4 個方法的實際接收者是 target 對象,而不是 NSProxy 對象本身。但是,NSProxy 并沒有將 performSelector 系列方法也 forward 到 -forwardInvocation:,也就是說,[proxy performSelector:someSelector] 的真正處理者仍然是 proxy 自身,只是后續(xù)會將一些方法給 forward 到 -forwardInvocation: 回調(diào),然后經(jīng)由 target 對象處理。
關(guān)于 NSInvocation
在 iOS 中直接調(diào)用方法的方式一般使用兩種方法
- 使用
NSObject的performSelector:..的方法,但是這個方法的弊端是傳遞的參數(shù)有限制 - 使用
NSInvocation
NSInvocation 的相關(guān)概念
NSMethodSignature:簽名:在創(chuàng)建 NSMethodSignature 的時候,必須傳遞一個簽名對象,簽名對象的作用:用于獲取參數(shù)的個數(shù)和方法的返回值
NSInvocation: 包裝了一次消息傳遞的所有內(nèi)容,包括target,select,argument、返回值等,會在運行時找到目標發(fā)送消息
注意這里傳的參數(shù)是地址
API 接口
@interface NSInvocation : NSObject
// 通過 NSMethodSignature 對象創(chuàng)建 NSInvocation 對象,NSMethodSignature 為方法簽名類
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
// 獲取 NSMethodSignature 對象
@property (readonly, retain) NSMethodSignature *methodSignature;
// 保留參數(shù),它會將傳入的所有參數(shù)以及 target 都 retain 一遍
- (void)retainArguments;
// 判斷參數(shù)是否還存在,調(diào)用 retainArguments 之前,值為 NO,調(diào)用之后值為 YES
@property (readonly) BOOL argumentsRetained;
// 設(shè)置消息調(diào)用者
@property (nullable, assign) id target;
// 設(shè)置要調(diào)用的消息
@property SEL selector;
// 獲取消息返回值
- (void)getReturnValue:(void *)retLoc;
// 設(shè)置消息返回值
- (void)setReturnValue:(void *)retLoc;
// 獲取消息參數(shù)
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
// 設(shè)置消息參數(shù)
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
// 發(fā)送消息,即執(zhí)行方法
- (void)invoke;
// target 發(fā)送消息,即 target 執(zhí)行方法
- (void)invokeWithTarget:(id)target;
@end
NSInvocation 的使用
- (void)viewDidLoad {
[super viewDidLoad];
// 創(chuàng)建簽名對象
NSMethodSignature *methodSignature = [ViewController instanceMethodSignatureForSelector:@selector(sendMessageWithArgument1:argument2:)];
// 創(chuàng)建 NSInvocation 對象
NSInvocation *objInvocation = [NSInvocation invocationWithMethodSignature:methodSignature];
// 指定 target 對象
objInvocation.target = self;
// 執(zhí)行的方法
objInvocation.selector = @selector(sendMessageWithArgument1:argument2:);
// 給執(zhí)行的方法設(shè)置參數(shù)
NSString *argument1 = @"11111";
NSString *argument2 = @"22222";
[objInvocation setArgument:&argument1 atIndex:2];
[objInvocation setArgument:&argument2 atIndex:3];
// 執(zhí)行方法
[objInvocation invoke];
// 獲取返回值
NSString *result = nil;
[objInvocation getReturnValue:&result];
NSLog(@"The Result is : %@", result);
}
- (NSString *)sendMessageWithArgument1:(NSString *)argument1 argument2:(NSString *)argument2 {
NSLog(@"參數(shù)1:%@, 參數(shù)2:%@", argument1, argument2);
return @"返回結(jié)果";
}
運行,打印結(jié)果如下
2020-12-07 18:06:15.439405+0800 Test[4511:281661] 參數(shù)1:11111, 參數(shù)2:22222
2020-12-07 18:06:15.439593+0800 Test[4511:281661] The Result is : 返回結(jié)果
創(chuàng)建簽名對象的時候不是使用
NSMethodSignature這個類創(chuàng)建,而是方法屬于誰就用誰來創(chuàng)建
設(shè)置參數(shù)的索引時不能從 0 開始,因為 0 已經(jīng)被self占用,1 已經(jīng)被_cmd占用
傳遞參數(shù)時,接收的是一個指針,即傳遞值的時候需要傳遞地址,傳遞返回值也是傳遞地址
調(diào)用invocation的invoke方法,就代表需要執(zhí)行NSInvocation對象中指定對象的指定方法,并且傳遞指定的參數(shù)