關(guān)于 NSProxy 的理解與運用

什么是 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,NSInvocationNSMethodSignature 類規(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)用,我們直接將 invocationtarget 設(shè)置為我們復制的對象。實質(zhì)上是利用了 runtime 的消息轉(zhuǎn)發(fā)機制

使用場景

  1. 解決 NSTimer/CADisplayLink 的循環(huán)引用問題

在之前的文章 iOS 中關(guān)于 NSTimer 的強引用分析 有過分析為什么會有循環(huán)引用的問題,這里不再啰嗦了。

  1. 模擬多繼承
@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)了復制 LCTeacherLCStudent,可以分別調(diào)用各個類的方法了。

NSProxy 和 NSObject 的區(qū)別

NSProxyNSObject 都定義了 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)用方法的方式一般使用兩種方法

  • 使用 NSObjectperformSelector:.. 的方法,但是這個方法的弊端是傳遞的參數(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)用 invocationinvoke 方法,就代表需要執(zhí)行 NSInvocation 對象中指定對象的指定方法,并且傳遞指定的參數(shù)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容