Objective-C-从NSProxy谈起
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)。这是非常强大,方便且灵活,你不用关心对象是什么类型,而只关心它实现了哪些方法。