NSProxy类的了解

 

概念

NSProxy是一个类似于NSObject的根类,看代码:

NS_ROOT_CLASS
@interface NSProxy <NSObject>{
    Class   isa;
}

上面我们可以看到NSProxy是一个实现了NSObject协议的根类。这2个方法必须重写

- (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel;

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

苹果的官方文档是这样描述它的:
NSProxy 是一个抽象基类,它为一些表现的像是其它对象替身或者并不存在的对象定义API。一般的,发送给代理的消息被转发给一个真实的对象或者代理本身引起加载(或者将本身转换成)一个真实的对象。NSProxy的基类可以被用来透明的转发消息或者耗费巨大的对象的lazy 初始化。

NSProxy实现了包括NSObject协议在内基类所需的基础方法,但是作为一个抽象的基类并没有提供初始化的方法。它接收到任何自己没有定义的方法他都会产生一个异常,所以一个实际的子类必须提供一个初始化方法或者创建方法,并且重载forwardInvocation:方法和methodSignatureForSelector:方法来处理自己没有实现的消息。

一个子类的forwardInvocation:实现应该采取所有措施来处理invocation,比如转发网络消息,或者加载一个真实的对象,并把invocation转发给他。methodSignatureForSelector:需要为给定消息提供参数类型信息,子类的实现应该有能力决定他应该转发消息的参数类型,并构造相对应的NSMethodSignature对象。详细信息可以查看NSDistantObject, NSInvocation, and NSMethodSignature的类型说明。
相信看了这些描述我们应该能对NSProxy有个初步印象,它仅仅是个转发消息的场所,至于如何转发,取决于派生类到底如何实现的。比如我们可以在内部hold住(或创建)一个对象,然后把消息转发给该对象。那我们就可以在转发的过程中做些手脚了。甚至也可以不去创建这些对象,去做任何你想做的事情,但是必须要实现他的forwardInvocation:和methodSignatureForSelector:方法。

用途

  1. 现在比较流行的说法是用它来模拟多重继承,大致过程就是让它Hold住你要实现多继承的类的对象,然后被hold住的对象的行为定义在接口中,并让Proxy去实现这些接口。然后再转发的时候把消息转发到实现了该接口的对象去执行,这样就好像实现了多重继承一样。注意:这个真不是多重继承,只是包含,然后把消息路由到指定的对象而已,其实完全可以用NSObject类来实现。
  2. 另外一个功能也是我们要重点介绍的功能就是AOP(Aspect Oriented Programming),它是可以通过预编译方式和运行期动态代理实现再不修改源代码的情况下给程序动态添加功能的一种技术。因为OC的动态语言特性,所以再OC里实现AOP也有多种方式,比如使用Runtime的swizzle method机制来实现方法替换从而达到Hook的目的(后面尽量抽时间写一篇文章来介绍,也当自己给自己备注一下)。

 

AOP与NSProxy

OC的动态语言的核心部分应该就是objc_msgSend方法的调用了。该函数的声明大致如下:
id objc_msgSend(id self, SEL _cmd, ...)
其中第一个参数是接受消息的target,第二个参数是要执行的selector,也就是我们要调用的方法,后面可以接若干个要传给selector的参数。
那只要我们能够Hook到对某个对象的objc_msgSend的调用,并且可以修改其参数甚至于修改成任意其他selector的IMP,我们就实现了AOP。
让我们来做一个简单的demo看看来实现AOP的效果。

#import <Foundation/Foundation.h>

typedef void(^actionBlock)(id target , SEL sel);

@interface AopProxy : NSProxy

@property (nonatomic,strong)id obj;

+(id)proxyWithObj:(id)object ;

- (void)addSelector:(SEL)sel withPreAction:(actionBlock)preAction afterAction:(actionBlock)afterAction ;

@end

再来看实现部分


#import "AopProxy.h"

@interface AopProxy ()

@property (nonatomic,strong) NSMutableDictionary * preDic ;
@property (nonatomic,strong) NSMutableDictionary * afterDic ;

@end

@implementation AopProxy



+(id)proxyWithObj:(id)object {
    return [[self alloc] initWithObj:object];
}
- (id)initWithObj:(id)object {
    // 要hook的对象
    self.obj = object;
    self.preDic = [NSMutableDictionary dictionary];
    self.afterDic = [NSMutableDictionary dictionary];
    return self;
}

- (void)addSelector:(SEL)sel withPreAction:(actionBlock)preAction afterAction:(actionBlock)afterAction {
    
    NSString * key = NSStringFromSelector(sel);
    if (preAction) {
        self.preDic[key] = preAction;
    }
    if (afterAction) {
        self.afterDic[key] = afterAction;
    }

}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
    //这里可以返回任何NSMethodSignature对象,你也可以完全自己构造一个
    return [self.obj methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
        
    if([self.obj respondsToSelector:invocation.selector] == NO){
        return;
    }
        // 打印invocation的信息,放开这行会崩
//    [self log_forwardInvocation:invocation];
    
    // 前面切一刀
    NSString * key = NSStringFromSelector(invocation.selector);
    if (self.preDic[key]) {
        actionBlock preAction = self.preDic[key];
        preAction(self.obj,invocation.selector);
    }
    
    //消息转发
    [invocation invokeWithTarget:self.obj];
    
    // 后面切一刀
    if (self.afterDic[key]) {
        actionBlock afterAction = self.afterDic[key];
        afterAction(self.obj,invocation.selector);
    }
    
}

/// 打印参数 , 不知道为什么 , 当我选择打印入参和返回值, 在打印结束后,就会报野指针错误
- (void)log_forwardInvocation:(NSInvocation *)invocation {
    
    NSString *selectorName = NSStringFromSelector(invocation.selector);
    NSLog(@"Before calling  %@",selectorName);
    NSMethodSignature *sig = [invocation methodSignature];
    if (invocation.argumentsRetained == NO) {
        [invocation retainArguments];        
    }
    //获取参数个数,注意再本例里这里的值是3,为什么呢?
    //因为objc_msgSend的前两个参数是隐含的
    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];
            //这里输出的是:"parameter (0)'class is MyProxy"
            //也证明了这是objc_msgSend的第一个参数
            NSLog(@"parameter (%d)'class is %@",i,[obj class]);
        }
        else if(strcmp(type, ":") == 0){
            SEL sel;
            [invocation getArgument:&sel atIndex:i];
            //这里输出的是:"parameter (1) is barking:"
            //也就是objc_msgSend的第二个参数
            NSLog(@"parameter (%d) is %@",i,NSStringFromSelector(sel));
        }
        else if(strcmp(type, "q") == 0){
            int arg = 0;
            [invocation getArgument:&arg atIndex:i];
            //这里输出的是:"parameter (2) is int value is 4"
            //稍后会看到我们再调用barking的时候传递的参数就是4
            NSLog(@"parameter (%d) is int value is %d",i,arg);
        }
    }
    
    const char *retType = [sig methodReturnType];
    if(strcmp(retType, "@") == 0){
        NSObject *ret;
        [invocation getReturnValue:&ret];
        //这里输出的是:"return value is wang wang!"
        NSLog(@"return value is %@",ret);
    }
    NSLog(@"After calling %@",selectorName);
   
}

- (void)dealloc {
    NSLog(@"%@ dealloc",self);   
}
@end

函数的调用如下:

    // 可以把这个hookedView的方法横着切了出去,切系统的方法很舒服的
    UIView * hookedView = [[UIView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)] ;
    hookedView.backgroundColor = [UIColor redColor];
    UIView * proxy = [AopProxy proxyWithObj: hookedView ];
    [(AopProxy *)proxy addSelector:@selector(viewWithTag:) withPreAction:^(id target, SEL sel) {
        NSLog(@"%@调用%@之前",[target class],NSStringFromSelector(sel));
    } afterAction:^(id target, SEL sel) {
        NSLog(@"%@调用%@之后",[target class],NSStringFromSelector(sel));
    }];
    [proxy viewWithTag:100];
    [proxy viewWithTag:222];
    
    // 不会调block,因为发消息的对象不对
    [hookedView viewWithTag:200];
    // 不会调block,因为没有注册这个sel
    [proxy canBecomeFirstResponder];

    [self.view addSubview:hookedView];

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

这个例子就实现了一个简单的AOP(Aspect Oriented Programming)面向切面编程。我们把一切功能"切"出去,与其他部分分开,这样可以提高程序的模块化程度。AOP能解耦也能动态组装,可以通过预编译方式和运行期动态代理实现在不修改源代码的情况下给程序动态统一添加功能。

 

解决Timer的强引用
  用NSTimer来实现每隔一定时间执行制定的任务,例如最常见的广告轮播图。如果我们在 timerWithTimeInterval:1 target:self 中指定target为当前控制器,控制器则会被timer强引用,而控制器对timer也是强引用的。一般,我们终止定时器往往在界面销毁时,即dealloc方法中写 [_timer invalidate];。基于上面的分析,由于循环引用的存在,控制器永远也不会走dealloc方法,定时器会一直执行方法,造成内存泄露。

解决方案:

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

背景知识:
  NSProxy:NSProxy 是一个抽象类,它接收到任何自己没有定义的方法他都会产生一个异常,所以一个实际的子类必须提供一个初始化方法或者创建方法,并且重载forwardInvocation:方法和methodSignatureForSelector:方法来处理自己没有实现的消息。
从类名来看是代理类,专门负责代理对象转发消息的。相比NSObject类来说NSProxy更轻量级,通过NSProxy可以帮助Objective-C间接的实现多重继承的功能。

 

代码:

#import <Foundation/Foundation.h>


@interface TimerProxy : NSProxy

@property (nonatomic,weak)id obj;

@end



#import "TimerProxy.h"

@implementation TimerProxy

/**
 这个函数让重载方有机会抛出一个函数的签名,再由后面的forwardInvocation:去执行
 为给定消息提供参数类型信息
 */
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSMethodSignature *sig = nil;
    sig = [self.obj methodSignatureForSelector:aSelector];
    return sig;
}

/**
 *  NSInvocation封装了NSMethodSignature,通过invokeWithTarget方法将消息转发给其他对象.这里转发给控制器执行。
 */
- (void)forwardInvocation:(NSInvocation *)anInvocation{
    [anInvocation invokeWithTarget:self.obj];
}

 


#import "SecondViewController.h"
#import "TimerProxy.h"


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

@end

@implementation SecondViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.title = @"proxy解决timer的强应用";
    
    
    TimerProxy * proxy = [TimerProxy alloc];
    proxy.obj = self;
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:proxy selector:@selector(run) userInfo:nil repeats:YES];
    
 
}

- (void)run {
    NSLog(@"scheduledTimerWithTimeInterval");
}


- (void)dealloc {
    // 这句invalidate必须写上,不然会崩溃 ,
    // timer强引用着proxy,会继续给 [proxy run] , proxy没有run方法,报[NSProxy doesNotRecognizeSelector:run]崩溃
    [self.timer invalidate];
    NSLog(@"%@ %@",self,NSStringFromSelector(_cmd));
}



@end

 

最后附上git地址 :  https://github.com/guochaoshun/AopProxy

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值