Objective-C-从NSProxy谈起

Page content

id< NSObject >面向协议编程

NSProxy类

介绍

NSProxy类是最近偶然发现的一个类,平时基本没有用过这个类。

但是在OC中,NSproxy是一个十分特殊的类,严格来讲,NSProxy不是一个NSObject,也不属于OC类。

我们看一下NSproxy的API:

~~~objective-c
@class NSMethodSignature, NSInvocation;
NS_ASSUME_NONNULL_BEGIN
NS_ROOT_CLASS
@interface NSProxy <NSObject> {
    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 NS_UNAVAILABLE;
- (BOOL)retainWeakReference NS_UNAVAILABLE;
// - (id)forwardingTargetForSelector:(SEL)aSelector;
@end
~~~

@interface NSProxy 说明NSproxy遵守NSObject协议,而且有一个 Class isa 指针,可以猜想它完全可以当做NSObject类来使用。

对于NSProxy的使用,主要集中在两个方法:

~~~objective-c
- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
~~~

使用

~~~objective-c
@interface MyProxy : NSProxy {
    id _object;
}
+ (id)proxyForObject:(id)obj;
@end
@implementation MyProxy
+ (id)proxyForObject:(id)obj {
    MyProxy *instance = [MyProxy alloc];
    instance->_object = obj;
    return instance;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [_object methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([_object respondsToSelector:invocation.selector]) {
        NSString *selectorName = NSStringFromSelector(invocation.selector);
        NSLog(@"Before calling \"%@\".", selectorName);
        [invocation invokeWithTarget:_object];
        NSLog(@"After calling \"%@\".", selectorName);
    }
}
@end
~~~

这是我们的 Proxy 简单实现,我们需要持有一个被代理对象的引用,然后将消息转发到这个对象上,在转发之前和以后我们就可以做自己想做的事情了。

methodSignatureForSelector: 方法需要获取一个方法签名,用来生成 NSInvocation,我们直接将这个调用转发到被代理对象中。

紧接着,forwardInvocation: 会被调用,将 NSInvocation 用被代理对象调用。我们就可以在这个方法里做一些手脚,比如埋点计数等。在这个例子中,我只是简单地将对象所调用的方法的 selector 打印出来。

然后我们看看用于测试的主函数:

~~~objective-c
int main(int argc, char *argv[]) {
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    NSURL *url = [MyProxy proxyForObject:[NSURL URLWithString:@"https://www.google.com"]];
    NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
        dispatch_semaphore_signal(sem);
    }];
    [task resume];
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    return 0;
}
~~~

就是简单构造一个 NSURL,只不过我们先用了 MyProxy 封装代理后传给 NSURLSession 去使用;

系统用 NSURL 的 absoluteURL 属性来获取真正的 URL 数据,这样,我们就已经可以跟踪已有类的行为了,甚至还可以通过 [NSThread callStackSymbols] 来跟踪调用该方法的函数调用栈,并借此来跟踪一些系统行为。

Hook 返回值

在消息发送过程中的拦截调用,曾经出现一个方法:

- (void)forwardInvocation:(NSInvocation *)anInvocation;

该方法将所调用的未找到的method,打包成NSInvocation(即anInvocation),在该方法中进行自定义处理后,调用invokeWithTarget:方法,让另一个实例来触发;

对于 NSInvocation,它封装了一个方法调用的全部信息,包括参数和返回值。既然是 Hook,我们就应该可以拦截方法的返回值并作加工。

~~~objective-c
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;
~~~

就是处理返回值的两个方法,我们可以用下面的语句来获取被代理对象的方法返回值:

~~~objective-c
NSURL *retValue;
[invocation getReturnValue:&retValue];
~~~

这是OC 中原生实现 AOP 的方式,更多的AOP在Runtime处,AOP 的在平时开发中的利用率还是挺高的,知名的 JSPatch、平时做的一些代码插桩都用到了 AOP 这一范式,而且 OC 天生就对这方面的支持十分友好;

但并不是说 AOP 是万能的,滥用也会造成很多问题,导致代码复杂度上升,维护性下降;

AOP 只是用来弥补设计上的不足或失误的,并不是一切问题的解决方法,谨慎使用才能更好地提高开发效率,降低维护成本。

从NSProxy谈起:id类型 NBObject类,与id< NSObject >

id类型

id类型简单申明了指向对象的指针,没有给编译器任何类型信息,因此编译器不会进行类型检查;

所以可以给id类型的对象发送任何信息。这也是初始化方法,+alloc返回id类型,调用[[Foo alloc] init]不会产生编译错误的原因。

id类型是运行时的动态类型,它是一个OC对象,但是并不都指向NSObject对象,即使这个类型和NSObject对象有很多共同的方法,像retain和release。

要让编译器知道这个类继承自NSObject,一种解决办法就是使用NSObject静态类型,当你发送NSObject没有的方法,像length或者count时,编译器就会给出警告。这也意味着,你可以安全地使用像retain,release,description这些方法。

NSObject类型

虽然可以申明一个通用的NSObject对象指针,但并不是所有的Foundation/Cocoa对象都继承息NSObject,比如NSProxy就不从NSObject继承,所以你无法使用NSObject*指向这个对象,即使NSProxy对象有release和retain这样的通用方法;

为了解决这个问题,这时候,你就需要一个指向拥有NSObject方法对象的指针,id< NSObject >就可以这样做到。

id< NSObject >

id< NSObject >告诉编译器,你不关心对象是什么类型,但它必须遵守NSObject协议(protocol),编译器就能保证所有赋值给id< NSObject >类型的对象都遵守NSObject协议(protocol)。

这样的指针可以指向任何NSObject对象,因为NSObject对象遵守NSObject协议(protocol),而且,它也可以用来保存NSProxy对象,因为它也遵守NSObject协议(protocol)。这是非常强大,方便且灵活,你不用关心对象是什么类型,而只关心它实现了哪些方法。