用 NSProxy 实现面向切面编程

在 Objective-C 中,有一个十分特殊的类,严格意义上讲它并不属于一个 OC 类,因为它压根就不是一个 NSObject,这个类就是 NSProxy。

我们来看看它的声明:


可以看到,它遵守了 NSObject 协议,并且第一个 Ivar 是一个 isa 指针,因此它完全是可以拿来当一个 NSObject 或其派生类来使用的。

NSProxy 的使用也非常简单,通常,你只需要实现两个方法:

- (void)forwardInvocation:(NSInvocation *)invocation;
- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

下面,我们通过一个例子来演示一下如何使用 NSProxy。
首先,我们需要准备一个实体类,它可以是任意一个类,甚至是系统类库所提供的类,这里我们用 NSURL 类来开刀。

@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 打印出来。

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

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 去使用,输出结果如下:

也就是说,系统用 NSURLabsoluteURL 属性来获取真正的 URL 数据,至此我们就已经可以跟踪已有类的行为了,甚至还可以通过 [NSThread callStackSymbols] 来跟踪调用改方法的函数调用栈:

并借此来跟踪一些系统行为。

Hook 返回值

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

- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;

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

NSURL *retValue;
[invocation getReturnValue:&retValue];

注意,这个方法是拷贝指针所指向的数据,所以要传递 NSURL 指针的指针,这样才能把 retValue 设置为返回值的地址,这点不要混淆了。相对应的,设置返回值也是类似的,这里就不赘述了。

Wrap Up

本文零零散散扯了一下 OC 中原生实现 AOP 的方式,没什么深刻的原理知识,涉及 runtime 的细节也没有说,如果大家感兴趣可以研究研究。AOP 的在平时开发中的利用率还是挺高的,知名的 JSPatch、平时做的一些代码插桩都用到了 AOP 这一范式,而且 OC 天生就对这方面的支持十分友好。但并不是说 AOP 是万能的,滥用也会造成很多问题,导致代码复杂度上升,维护性下降。AOP 只是用来弥补设计上的不足或失误的,并不是一切问题的解决方法,谨慎使用才能更好地提高开发效率,降低维护成本。

推荐阅读更多精彩内容