NSProxy 与 respondsToSelector:

NSProxy概述

An abstract superclass defining an API for objects that act as stand-ins for other objects or for objects that don’t exist yet.

正如Apple对NSPorxy的描述,NSPorxy是一个虚类。它不继承于NSObject,却实现了NSObject的Protocol。相比于我们常见的各种继承于NSObject的类,它自身的方法很少,不需要实例化init,也没有kvc等各种乱七八糟的协议实现。但是它具有消息转发的功能,即可以通过继承它,重写 -forwardInvocation: 和 -methodSignatureForSelector: 方法,来实现消息的转发。正如它的名字,它是实现代理模式的利器。
常见的利用场景有两种:

  • 构造代理类,实现对原始类中方法的hook
  • 代理类避免循环引用,如nstimer、cadisplaylink
  • 实现多重继承,通过消息转发,将多个类的调用转发到具体实现的类

可以参照网上的一些指导文章,不再赘述。这里着重讲一个可能存在问题的需求场景:

问题描述

场景: 我们很多使用NSPorxy的场景,是通过继承NSPorxy,来hook实现某些Protocol的delegate。然后通过Protocol描述的方法,来调用这个代理类proxyDelegate。
问题: 可能会存在一种需求,我们的proxyDelegate,实现了这个Protocol中的某些optional方法,而被代理的delegate类,并没有实现。那么会发现,proxyDelegate中对这些optional方法的实现,无法被调用。

show me the code :

// delegate protocol
@protocol BProtocol <NSObject>
@required
- (void)showName;
@optional
- (void)show;
@end
// a delegate implement protocol
@interface BTarget : NSObject <BProtocol>

- (void)showName;
@end
@implementation BTarget

- (void)showName {
    NSLog(@"%s", __func__);
}
@end
// a proxy class
@interface AProxy : NSProxy <BProtocol>
@property (nonatomic, weak, readonly, nullable) id target;

- (instancetype)initWithTarget:(id)target;
+ (instancetype)proxyWithTarget:(id __nullable)target;
@end
@implementation AProxy

- (instancetype)initWithTarget:(id)target {
    _target = target;
    return self;
}

+ (instancetype)proxyWithTarget:(id)target {
    return [[self alloc] initWithTarget:target];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    return [self.target methodSignatureForSelector:selector];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation invokeWithTarget:self.target];
}

- (void)showName {
    NSLog(@"%s", __func__);
    if ([self.target respondsToSelector:@selector(showName)]) {
        [self.target showName];
    }
}

- (void)show {
    NSLog(@"%s", __func__);
    if ([self.target respondsToSelector:@selector(show)]) {
        [self.target showName];
    }
}
@end

如上代码,BTarget是实现BProtocol的delegate类,我们用代理类AProxy实现对BTarget的hook。然后是调用代码(这里为了简单,只模拟对delegate的调用):

BTarget *aDelegate = [BTarget new];
    AProxy *proxyDelegate = [AProxy proxyWithTarget:aDelegate];
    
    if ([proxyDelegate respondsToSelector:@selector(showName)]) {
        [proxyDelegate showName];
    }
    if ([proxyDelegate respondsToSelector:@selector(show)]) {
        [proxyDelegate show];
    }

执行的结果:

-[AProxy showName]
-[BTarget showName]

showName正常调用到,而show没有被调用。

原因&结论:

原因很简单,NSProxy的respondsToSelector:返回了NO!虽然proxyDelegate中的确有show方法的实现
实际上,考察我们对NSObject类常用的respondsToSelector:和isKindOfClass:两个方法,NSProxy的处理方式跟NSObject是不同的。
即便我们在AProxy中加入respondsToSelector:

//... in AProxy
- (BOOL)respondsToSelector:(SEL)aSelector {
    return [super respondsToSelector:aSelector];
}
//...

然后在methodSignatureForSelector:打断点:po selector,我们会发现会打印两次respondsToSelector:,也就是说,respondsToSelector:这个方法消息被转发了!另外一点,[super respondsToSelector:aSelector]两次调用(showName和show)都会返回NO!
对继承于NSObject的类来说,respondsToSelector:的调用不会转发,也会正确返回是否含有selector,而不会直接返回NO然后走转发。

解决

一种解决方案,是在respondsToSelector:加入method白名单,即特定实现的method的SEL,返回YES:

- (BOOL)respondsToSelector:(SEL)aSelector {
    if (aSelector == @selector(show)) {
        return YES;
    }
    return [super respondsToSelector:aSelector];
}

添加以上代码,执行答应出结果:

-[AProxy showName]
-[BTarget showName]
-[AProxy show]

参考:
使用NSProxy和NSObject设计代理类的差异

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。