iOS之深入解析少见却神奇的NSProxy类的多种使用

Posted Forever_wj

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS之深入解析少见却神奇的NSProxy类的多种使用相关的知识,希望对你有一定的参考价值。

一、NSProxy 简介

  • NSProxy 是一个实现了 NSObject 协议类似于 NSObject 的抽象基类,是根类,与 NSObject 类似:
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 API_UNAVAILABLE(macos, ios, watchos, tvos);
- (BOOL)retainWeakReference API_UNAVAILABLE(macos, ios, watchos, tvos);

// - (id)forwardingTargetForSelector:(SEL)aSelector;
  • 苹果的官方文档描述如下:
Typically, a message to a proxy is forwarded to the real object or causes the proxy to load (or transform itself into) the real object. Subclasses of NSProxy can be used to implement transparent distributed messaging (for example, NSDistantObject) or for lazy instantiation of objects that are expensive to create.

NSProxy implements the basic methods required of a root class, including those defined in the NSObject protocol. However, as an abstract class it doesn’t provide an initialization method, and it raises an exception upon receiving any message it doesn’t respond to. A concrete subclass must therefore provide an initialization or creation method and override the forwardInvocation: and methodSignatureForSelector: methods to handle messages that it doesn’t implement itself. A subclass’s implementation of forwardInvocation: should do whatever is needed to process the invocation, such as forwarding the invocation over the network or loading the real object and passing it the invocation. methodSignatureForSelector: is required to provide argument type information for a given message; a subclass’s implementation should be able to determine the argument types for the messages it needs to forward and should construct an NSMethodSignature object accordingly. See the NSDistantObject, NSInvocation, and NSMethodSignature class specifications for more information.
  • 看完文档的介绍,我们应该能对 NSProxy 有个初步印象,它仅仅是个转发消息的场所,至于如何转发,取决于派生类的具体实现,比如可以在内部 hold 住(或创建)一个对象,然后把消息转发给该对象,那我们就可以在转发的过程中做些“手脚”了,甚至也可以不去创建这些对象,去做任何你想做的事情,但是必须要实现它的 forwardInvocation: 和 methodSignatureForSelector: 方法。

二、NSProxy 模拟多继承

  • 大致过程就是让它持有要实现多继承的类的对象,然后用多个接口定义不同的行为,并让 Proxy 去实现这些接口,然后在转发的时候把消息转发到实现了该接口的对象去执行,这样就好像实现了多重继承一样。注意:这个真不是多重继承,只是包含,然后把消息路由到指定的对象而已,其实完全可以用 NSObject 类来实现;
  • NSObject 寻找方法顺序:本类 -> 父类 -> 动态方法解析 -> 备用对象 -> 消息转发;
  • NSproxy 寻找方法顺序:本类 -> 消息转发;
  • 同样做“消息转发”,NSObject 会比 NSProxy 多做好多事,也就意味着耽误很多时间。
  • 首先新建两个基类如下:
@implementation classA
- (void)infoA 
    NSLog(@"classA");   

@end

@implementation classB
- (void)infoB 
    NSLog(@"classB");

@end
  • 代理实现如下:
@interface ClassProxy : NSProxy

@property(nonatomic, strong, readonly) NSMutableArray *targetArray;

- (void)target:(id)target;
- (void)handleTargets:(NSArray *)targets;

@end
  • NSProxy 必须以子类的形式出现,因为考虑到很可能还有其他类需要 ClassProxy 来代理,这里做一个数组来存放需要代理的类:
@interface ClassProxy()
@property (nonatomic, strong) NSMutableArray *targetArray; // 多个 targets 皆可代理
@property (nonatomic, strong) NSMutableDictionary *methodDic;
@property (nonatomic, strong) id target;
@end
  • 然后 target 和相对应的 method name 做了一个字典来存储,方便获取:
- (void)registMethodWithTarget:(id)target 
    unsigned int countOfMethods = 0;
    Method *method_list = class_copyMethodList([target class], &countOfMethods);
    for (int i = 0; i < countOfMethods; i++) 
        Method method = method_list[i];
        // 得到方法的符号
        SEL sel = method_getName(method);
        // 得到方法的符号字符串
        const char *sel_name = sel_getName(sel);
        // 得到方法的名字
        NSString *method_name = [NSString stringWithUTF8String:sel_name];
        self.methodDic[method_name] = target;
    
    free(method_list);

  • 然后就是最主要的两个方法:
- (void)forwardInvocation:(NSInvocation *)invocation 
    SEL sel = invocation.selector;
    NSString *methodName = NSStringFromSelector(sel);
    id target = self.methodDic[methodName];
    if (target) 
        [invocation invokeWithTarget:target];
     


- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel 
    NSMethodSignature *Method;
    NSString *methodName = NSStringFromSelector(sel);
    id target = self.methodDic[methodName];
    if (target) 
        Method = [target methodSignatureForSelector:sel];
     else 
        Method = [super methodSignatureForSelector:sel];
    
    return Method;

  • methodSignatureForSelector: 得到对应的方法签名,通过 forwardInvocation: 转发,调用和打印结果如下所示:
- (void)viewDidLoad 
    [super viewDidLoad];

    [self classInheritance];


/**
 * 多继承
 */
- (void)classInheritance 
    classA *A = [[classA alloc] init];
    classB *B = [[classB alloc] init];
    ClassProxy *proxy = [ClassProxy alloc];

    [proxy handleTargets:@[A, B]];
    [proxy performSelector:@selector(infoA)];
    [proxy performSelector:@selector(infoB)];


// 运行结果
classA 
classB
  • 这就利用 NSProxy 实现了多继承。

三、NSProxy 避免循环引用

  • 由于苹果在 iOS10 以上给出了 timer 的 block 方式,已经可以解决循环引用的问题。因此这里只说明利用 NSProxy 如何解决循环引用,实际情况可直接使用系统的方法。
  • 首先因为 NSTimer 创建的时候需要传入一个 target,并且持有它,而 target 本身也会持有 timer 所以会造成循环引用,因此将 target 用 NSProxy 的子类代替,如下所示:
- (void)viewDidLoad 
    [super viewDidLoad];
    self.timer = [NSTimer timerWithTimeInterval:1
                                         target:[WeakProxy proxyWithTarget:self]
                                       selector:@selector(invoked:)
                                       userInfo:nil
                                        repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];


- (void)invoked:(NSTimer *)timer 
    NSLog(@"1");

  • 在 WeakProxy 中设定 target 为弱引用:
@interface WeakProxy ()
@property (nonatomic, weak) id target;
@end

@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target 
    return [[self alloc] initWithTarget:target];


- (instancetype)initWithTarget:(id)target 
    self.target = target; 
    return self;


- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel 
    return [self.target methodSignatureForSelector:sel];


- (void)forwardInvocation:(NSInvocation *)invocation 
    SEL sel = invocation.selector;
    if ([self.target respondsToSelector:sel]) 
        [invocation invokeWithTarget:self.target];
    

@end
  • 然后同样利用上述两个方法进行消息转发即可。

四、NSProxy 实现 AOP

  • AOP(Aspect Oriented Programming),它是可以通过预编译方式和运行时动态代理实现在不修改源代码的情况下给程序动态添加功能的一种技术。iOS 中面向切片编程一般有两种方式 ,一种是直接基于 runtime 的 method-Swizzling 机制来实现方法替换从而达到 hook 的目的,另一种就是基于 NSProxy。
  • OC 的动态语言的核心部分应该就是 objc_msgSend 方法的调用,该函数的声明大致如下:
/**
 * 参数 1:接受消息的 target
 * 参数 2:要执行的 selector
 * 参数 3:要调用的方法
 * 可变参数:若干个要传给 selector 的参数 
 */
id objc_msgSend(id self, SEL _cmd, ...)
  • 只要能够 Hook 到对某个对象的 objc_msgSend 的调用,并且可以修改其参数甚至于修改成任意其它 selector 的 IMP,就可以实现 AOP:
@interface MyProxy : NSProxy 
     id _innerObject;  // 在内部持有要 hook 的对象

+ (instancetype)proxyWithObj:(id)object;
@end

@interface Dog : NSObject
- (NSString *)barking:(NSInteger)months;
@end
  • 具体实现:
@implementation MyProxy

+ (instancetype)proxyWithObj:(id)object 
    MyProxy * proxy = [MyProxy alloc];
    // 持有要 hook 的对象
    proxy->_innerObject = object;
    // 注意返回 Proxy 对象
    return proxy;


- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel 
    // 可以返回任何 NSMethodSignature 对象,也可以完全自己构造
    return [_innerObject methodSignatureForSelector:sel];


- (void)forwardInvocation:(NSInvocation *)invocation 
    if([_innerObject respondsToSelector:invocation.selector])
        NSString *selectorName = NSStringFromSelector(invocation.selector);
        NSLog(@"Before calling %@",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];
                // 这里输出的是:"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);
            
        
        // 消息转发
        [invocation invokeWithTarget:_innerObject];
        const char *retType = [sig methodReturnType];
        if(strcmp(retType, "@") == 0)
            NSObject *ret;
            [invocation getReturnValue:&ret];
            // 输出的是:"return value is wang!"
            NSLog(@"return value is %@", ret);
        
        NSLog(@"After calling %@", selectorName);
    

@end

@implementation Dog
- (NSString *)barking:(NSInteger)months 
    return months > 3 ? @"wang!" : @"Oh!";

@end
  • 函数的调用,如下:
Dog *dog = [MyProxy proxyWithObj:[Dog alloc]];
[dog barking:4];
  • 上面的代码中,可以任意更改参数、调用的方法,甚至转发给其它类型的对象,这确实达到了 Hook 对象的目的,也就是可以实现 AOP 的功能:
typedef void(^proxyBlock)(id target,SEL selector);

NS_ASSUME_NONNULL_BEGIN

@interface AOPProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
- (void)inspectSelector:(SEL)selector preSelTask:(proxyBlock)preTask endSelTask:(proxyBlock)endTask;
@end
@interface AOPProxy ()
@property (nonatomic, strong) id target;
@property (nonatomic, strong) NSMutableDictionary *preSelTaskDic;
@property (nonatomic, strong) NSMutableDictionary *endSelTaskDic;
@end
- (void)inspect 
    NSMutableArray *targtArray = [AOPProxy proxyWithTarget:[NSMutableArray arrayWithCapacity:1]];
    [(AOPProxy *)targtArray inspectSelector:@selector(addObject:) preSelTask:^(id target, SEL selector) 
        [target addObject:@"Begin"];
        NSLog(@"%@ 加进来之前", target);
     endSelTask:^(id target, SEL selector) 
        [target addObject:@"End"];
        NSLog(@"%@ 加进来之后", target);
    ];
    [targtArray addObject:@"第一个元素"];


( "Begin" ) 加进来之前
( "Begin", 
  "U662f\\U4e00\\U4e2a\\U5143\\U7d20", 
  "End" ) 
加进来之后

五、NSProxy 实现延迟初始化(Lazy Initialization)

  • 在 [SomeClass lazy] 之后调用 doSomthing,首先进入 forwardingTargetForSelector,_object 为 nil 并且不是 init 开头的方法的时候会调用 init 初始化对象,然后将消息转发给代理对象 _object;
  • 在 [SomeClass lazy] 之后调用 initWithXXX:,首先进入 forwardingTargetForSelector 返回 nil,然后进入 methodSignatureForSelector: 和 forwardInvocation: 保存自定义初始化方法的调用,最后调用 doSomthing,进入 forwardingTargetForSelector,_object 为 nil 并且不是 init 开头的方法的时候会调用自定义初始化方法,然后将消息转发给代理对象 _object。
SomeClass *object = [SomeClass lazy];

// other thing ...

[object doSomething];  // 在这里 object 才会调用初始化方法,然后调用 doSomething

以上是关于iOS之深入解析少见却神奇的NSProxy类的多种使用的主要内容,如果未能解决你的问题,请参考以下文章

iOS之深入解析分类Category的底层原理

iOS之深入解析类加载的底层原理:分类如何加载到类以及分类和类的配合使用

iOS之深入解析WKWebView的WebKit源码调试与分析

iOS之深入解析类方法+load与+initialize的底层原理

iOS经典面试题之深入解析分类Category的本质以及如何被加载

猿创征文|iOS经典面试题之深入解析分类Category的本质以及如何被加载