iOS NSProxy详解

2,744 阅读5分钟

一 NSProxy介绍

众所周知,OC是单继承的语言,但是基于运行时的机制,却有一种方法让它来实现一下"伪多继承",就是利用NSProxy这个类。通过代码发现NSProxy是一个实现了NSObject协议的根类。

NS_ROOT_CLASS
@interface NSProxy <NSObject>{
    Class   isa;
}
  • NSProxy 是NSProxy 是一个抽象类,跟 NSObject 一样的基类,都遵守NSObject协议
  • NSProxy是一个抽象类,必须继承实例化其子类才能使用。
  • NSProxy从类名来看是代理类专门负责代理对象转发消息的。相比NSObject类来说NSProxy更轻量级,通过NSProxy可以帮助Objective-C间接的实现多重继承的功能。

NSProxy的使用巧妙的运用了消息转发机制,核心方法有2个。

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

通过methodSignatureForSelector:方法获取一个NSMethodSignature类型的对象,调用forwardInvocation:方法。该方法传入一个封装了NSMethodSignatureNSInvocation对象。然后该对象通过invakeWithTarget:方法将消息转发给其它对象。

二 NSProxy的作用

1. 模拟多重继承

直接上代码

InheritProxy.h 文件

#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN

@interface InheritProxy : NSProxy
@property(strong,nonatomic)id target;//在内部hold住一个要hook的对象
- (id)transformToTarget:(NSObject *)target;
@end


@interface Dog : NSObject
-(NSString *)barking:(NSString*)param;
@end

@interface Cat : NSObject
-(void)eat;
@end
NS_ASSUME_NONNULL_END

InheritProxy.m 文件

#import "InheritProxy.h"

@implementation InheritProxy
- (id)transformToTarget:(NSObject *)target
{
    self.target = target;
    return self.target;
}
//1.查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    if ([self.target methodSignatureForSelector:sel]) {
        return [self.target methodSignatureForSelector:sel];
    }
    return [super methodSignatureForSelector:sel];
}
//id objc_msgSend(id self, SEL _cmd, ...)
- (void)forwardInvocation:(NSInvocation *)invocation{
    if ([self.target respondsToSelector:[invocation selector]]) {
        /* 如果打开这个注释,打印结果是===> 狗说:不想吃骨头了,猫吃鱼
        if ([self.target isKindOfClass:[NSClassFromString(@"Dog") class]]) {
            //在这里拦截参数
            //Argument:表示的是objc_msgSend的参数  index:表示的是方法参数的下标
            //objc_msgSend的第一个参数是接受消息的target,第二个参数是要执行的selector,也就是我们要调用的方法,后面可以接若干个要传给selector的参数
            NSString *str = @"不想吃骨头了";
            [invocation setArgument:&str atIndex:2];
        }*/

        [invocation invokeWithTarget:self.target];
    }
}
- (BOOL)respondsToSelector:(SEL)aSelector{
    return [self.target respondsToSelector:aSelector];
}
@end

@implementation Dog
-(NSString *)barking:(NSString*)param{
    NSString*result = [@"狗说:" stringByAppendingString:param];
    NSLog(@"%@",result);
    return result;
}
@end

@implementation Cat
-(void)eat{
    NSLog(@"猫吃鱼");
}
@end

使用示例

-(void)multipleInherit{
    InheritProxy *proxy = [InheritProxy alloc];
    
    [proxy transformToTarget:[Dog alloc]];
    [proxy performSelector:@selector(barking:) withObject:@"我想吃骨头"];
    
    [proxy transformToTarget:[Cat alloc]];
    [proxy performSelector:@selector(eat)];
    
    //=============打印结果如下==========
    //  狗说:我想吃骨头
    //  猫吃鱼
    //=============打印结果如上==========
}

2. AOP面向切片编程

AOP(Aspect Oriented Programming),它是可以通过预编译方式和运行期动态代理实现再不修改源代码的情况下给程序动态添加功能的一种技术。因为OC的动态语言特性,iOS中面向切片编程一般有两种方式,一个是直接基于Runtime 的method-Swizzling.还有一种就是基于NSProxy。

OC的动态语言的核心部分应该就是objc_msgSend方法的调用了。该函数的声明大致如下:

/**
 * 参数 1:接受消息的 target
 * 参数 2:要执行的 selector
 * 参数 3:要调用的方法
 * 可变参数:若干个要传给 selector 的参数 
 */
id objc_msgSend(id self, SEL _cmd, ...)

那只要我们能够Hook到对某个对象的objc_msgSend的调用,并且可以修改其参数甚至于修改成任意其他selector的IMP,我们就实现了AOP。

请看示例

AOPProxy.h文件

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AOPProxy : NSProxy
@property(strong,nonatomic)id target;
+(instancetype)proxyWithTarget:(id)target;
@end

@interface Teacher : NSObject
-(NSString *)teach:(NSString*)language;
@end
NS_ASSUME_NONNULL_END

AOPProxy.m 文件

#import "AOPProxy.h"
@implementation AOPProxy

+(instancetype)proxyWithTarget:(id)target
{
    AOPProxy * proxy = [AOPProxy alloc];
    // 持有要 hook 的对象
    proxy.target = target;
    // 注意返回的值是 Proxy 对象
    return proxy;
}

//1.查询该方法的方法签名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
{
    if ([self.target methodSignatureForSelector:sel]) {
        return [self.target methodSignatureForSelector:sel];
    }
    return [super methodSignatureForSelector:sel];
}

- (void)forwardInvocation:(NSInvocation *)invocation
{
    if([self.target respondsToSelector:invocation.selector]){
        NSString *selectorName = NSStringFromSelector(invocation.selector);
        NSLog(@"%@转发前",selectorName);
        [invocation retainArguments];
        NSMethodSignature *sig = [invocation methodSignature];
        // 获取参数个数,注意在本例里这里的值是 3,因为 objc_msgSend 隐含了 self、selector 参数
        NSUInteger cnt = [sig numberOfArguments];
        // 本例只是简单的将参数和返回值打印出来
        for (int i = 0; i < cnt; i++) {

            // 参数类型
            const char * type = [sig getArgumentTypeAtIndex:i];
            if(strcmp(type, "@") == 0){
                NSObject *obj;
                [invocation getArgument:&obj atIndex:i];
                // 这里输出:"第0个参数是AOPProxy"
                NSLog(@"第%d个参数是%@", i, [obj class]);
            }
            else if(strcmp(type, ":") == 0){
                SEL sel;
                [invocation getArgument:&sel atIndex:i];
                // 这里输出:第1个参数是teach:
                NSLog(@"第%d个参数是%@", i, NSStringFromSelector(sel));
            }
            else if(strcmp(type, "q") == 0){
                NSString * arg;
                [invocation getArgument:&arg atIndex:i];
                // 这里输出:第2个参数是__NSCFConstantString
                NSLog(@"第%d个参数是%@", i, arg);
            }
        }
        // 消息转发
        [invocation invokeWithTarget:self.target];
        const char *retType = [sig methodReturnType];
        if(strcmp(retType, "@") == 0){
            NSObject *ret;
            [invocation getReturnValue:&ret];
            //这里输出-->teach:方法的返回值是:老师教语文
            NSLog(@"%@方法的返回值是:%@",selectorName,ret);
        }
        NSLog(@"%@转发后", selectorName);
    }
}
@end

@implementation Teacher
-(NSString *)teach:(NSString*)language
{
    NSString*result = [@"老师教" stringByAppendingString:language];
    NSLog(@"%s------%@",__FUNCTION__,result);
    return result;
}
@end

应用示例

-(void)AOP{
    Teacher * teacher = (Teacher*)[AOPProxy proxyWithTarget:[Teacher alloc]];
    [teacher teach:@"语文"];
    //=============打印结果如下==========
    //  teach:转发前
    //  第0个参数是AOPProxy
    //  第1个参数是teach:
    //  第2个参数是__NSCFConstantString
    //  -[Teacher teach:]------老师教语文
    //  teach:方法的返回值是:老师教语文 
    //  teach:转发后
    //=============打印结果如上==========
}

上面的代码中,可以任意更改参数、调用的方法,甚至转发给其他类型的对象,这确实达到了 Hook 对象的目的,也就是可以实现 AOP 的功能了。

3. 解决NSTimer,CADisplayLink等强引用target引起的无法释放问题。

例如NSTimer导致的循环引用,利用消息转发来断开NSTimer与Controller之间的强引用关系。初始化NSTimer时把触发事件的target替换成一个单独的对象,然后这个对象中NSTimer的SEL方法触发时让这个方法在当前的视图self中实现。

示例:使用NSProxy来解决循环引用

//TimerProxy.h文件
#import <Foundation/Foundation.h>
@interface TimerProxy : NSProxy
//通过实例方法创建对象
- (instancetype)initWithTarget:(id)target;
//通过类方法创建创建
+ (instancetype)proxyWithTarget:(id)target;
@end
//TimerProxy.m文件
#import "TimerProxy.h"
@interface TimerProxy()
@property (nonatomic, weak) id target;
@end

@implementation TimerProxy
- (instancetype)initWithTarget:(id)target {
    self.target = target;
    return self;
}
+ (instancetype)proxyWithTarget:(id)target {
    return [[self alloc] initWithTarget:target];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
    if ([self.target respondsToSelector:invocation.selector]) {
        [invocation invokeWithTarget:self.target];
    }
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    return [self.target methodSignatureForSelector:sel];
}
@end

使用示例

#import "ViewController.h"
#import "TimerProxy.h"

@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    TimerProxy *proxy = [[TimerProxy alloc] initWithTarget:self];
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:proxy selector:@selector(timerHandle) userInfo:nil repeats:YES];
}

//定时触发的事件
- (void)timerHandle {
     NSLog(@"计时ing");
}
- (void)dealloc {
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"%s",__func__);
}
@end

通过NSProxy这个伪基类(相当于ViewController的复制类),避免直接让timer和viewController造成循环。页面消失时会成功走到delloc方法

NSProxy解决循环引用原理图 image.png

三 NSProxy 和 NSObject 的比较

相比NSObject,NSProxy更轻量级, 做消息转发效率更高,寻找方法对比

  • NSObject寻顺序:本类->(父类-)>(动态方法解析)-> 消息转发;
  • NSproxy顺序:本类->消息转发