• iOS 消息转发以及 NSProxy 实战


    最后更新: 2018-01-17

    一、消息派发机制-NSObject

    在 iOS 开发中, 调用对象的方法就是给对象发送一个消息。了解消息的派发机制对于iOS开发来说是一个很实用且强大的工具, 下面我将对其详细说明;

    当实例化一个Person对象 p, 调用 [p run], 那么派发机制是什么样的呢?

    Step1:
    在当前对象 p 中寻找是否有 run 方法,如果有,执行; 如果没有, 接着往下执行;

    Step2:
    p 的父类中寻找是否有 run方法, 有则执行,没有的话,接着往上找, 如果还没找到,接着往下执行;

    Step3: 系统调用如下方法:

    // 类方法
    + (BOOL)resolveClassMethod:(SEL)sel;
    
    // 实例化方法
    + (BOOL)resolveInstanceMethod:(SEL)sel
    

    可以在这个方法中动态的为对象添加对应的方法,例如我添加run方法:

    void run(id self, SEL _cmd) {
        NSLog(@"%@ -- %s", self, sel_getName(_cmd));
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel {
    
        if (sel == @selector(run)) {
            class_addMethod(self, sel, (IMP)run, "v@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    

    Step4: 调用 - (id)forwardingTargetForSelector:(SEL)aSelector来进行转发

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        NSLog(@"forwardingTargetForSelector: %@", NSStringFromSelector(aSelector));
        return [[Car alloc] init];
    }
    

    注意:转发机制,不能用于返回自身对象,否则会进入一个死循环

    Step5: 系统会调用如下方法.

    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        NSLog(@"forwardInvocation");
        SEL selector = [anInvocation selector];
        
        Car *car = [[Car alloc] init];
        if ([car respondsToSelector:selector]) {
            [anInvocation invokeWithTarget:car];
            return;
        }
        return [super forwardInvocation:anInvocation];
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        NSLog(@"methodSignatureForSelector");
        
        NSString *selName = NSStringFromSelector(aSelector);
        if ([selName isEqualToString:@"run"]) {
            return [NSMethodSignature signatureWithObjCTypes:"v@:"];
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    

    注意: 如果 methodSignatureForSelector: 返回为nil, 那么 forwardInvocation: 将不会调用, 直接抛出错误;

    Step6: Crash

    *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person run]: unrecognized selector sent to instance 0x6040000194c0'
    

    以上就是 OC 中的 NSObject 对象中的完整的消息派发机制.

    关于swift 的派发机制可以参考这篇文章-深入理解 Swift 派发机制;

    值得提醒的是,在Swift中, Step5的两个方法是 OBJC_SWIFT_UNAVAILABLE("")的。具体原因可以查看官方的这篇文章-What Happened to NSMethodSignature?


    二、 NSObject 与 NSProxy

    在 Foundation框架中开发中, NSObject 是大多数类的基类, 而基类 NSProxy 却鲜为人知;

    NSob
    查看 NSProxy文档, 它遵循着 protocol NSObject; 与 NSObject 相比, NSProxy更加的简洁; 没有 init 或者 new 方法。 在消息转发的实现上, 仅存在如下两个方法:

    - (void)forwardInvocation:(NSInvocation *)invocation;
    
    // swift 还不能用。
    - (nullable NSMethodSignature *)methodSignatureForSelector:(SEL)sel NS_SWIFT_UNAVAILABLE("NSInvocation and related APIs not available");
    
    1. NSObject 对象与 NSProxy 对象均会对 performSelector方法进行转发 这里与张不坏-NSProxy 所得出的结论不同;
    2. 相比于NSObject, NSProxy会将自省的方法直接forward -forwardInvocation:中, 其中包括:
      - (BOOL)isKindOfClass:(Class)aClass;
      - (BOOL)isMemberOfClass:(Class)aClass;
      - (BOOL)conformsToProtocol:(Protocol *)aProtocol;
      
      - (BOOL)respondsToSelector:(SEL)aSelector;
      
    3. 对于 category 方法, 经过测试, NSObject的category部分是无法进行转发的,但是自定义的category里面的方法,是可以进行转发, 而对于 NSProxy, 目前在系统中没有找到对应的 Category方法,自定义的是可以进行转发的;

    上述的测试内容,你可以可以在这里下载到对应的方法


    三、实践

    关于消息转发, 显然NSObject 与 NSProxy 均能完成此动作,而且大多数由于 NSObject 开源, 而且日常接触比较多, 可控, 很多人偏向使用 NSObject 来完成消息转发的行为。 我个人认为, 苹果设计出了 NSProxy, 而且经过这么多年, 在消息转发上, NSProxy 应该更适合,正如官方文档所说:

    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

    网上关于 NSProxy 的资料不算多, 有这篇-实战: 利用NSProxy实现消息转发-模块化的网络接口层设计, 最后作者也去使用 Category 来设计了, 囧, 还有这篇-利用NSProxy解决NSTimer内存泄漏问题

    在使用场景上, 我司在设计iOS客户端统计上面, 集成和很多的统计. 在设计这个结构的时候, 就使用了 NSProxy, 通过任务分发的方式, 将统计的数据分发至各个平台

    @interface BehaviorCollectorDispatcher : NSProxy <BehaviorCollector>
    
    - (instancetype)initWithBehaviorCollector:(NSArray<id<BehaviorCollector>> *)collectors;
    
    @end
    
    @implementation BehaviorCollectorDispatcher {
        
        Protocol *_protocol;
        NSArray<id<BehaviorCollector>> *_collectors;
    }
    
    - (instancetype)initWithBehaviorCollector:(NSArray<id<BehaviorCollector>> *)collectors {
        _protocol = @protocol(BehaviorCollector);
        _collectors = collectors;
        return self;
    }
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
        NSMethodSignature *signature;
        struct objc_method_description theDescription = protocol_getMethodDescription(_protocol, selector, YES, YES);
        signature = [NSMethodSignature signatureWithObjCTypes:theDescription.types];
        return signature;
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        
        SEL selector = anInvocation.selector;
        
        const char* name = sel_getName(selector);
        NSString *selectorName = [NSString stringWithUTF8String:name];
        
        if ([selectorName hasPrefix:@"collect"]) {
            for (id<BehaviorCollector> collector in _collectors) {
                [anInvocation invokeWithTarget:collector];
            }
        } else {
            [super forwardInvocation:anInvocation];
        }
    }
    
    @end
    
    
  • 相关阅读:
    Docker删除某个容器时失败解决方案
    Docker搭建redis
    Django优雅集成MongoDB
    MongoDB学习笔记:文档Crud Shell
    MongoDB学习笔记:MongoDB 数据库的命名、设计规范
    MongoDB学习笔记:快速入门
    MongoDB学习笔记:Python 操作MongoDB
    在Docker中安装MongoDB
    Linux 挂载盘
    java中Array/List/Map/Object与Json互相转换详解(转载)
  • 原文地址:https://www.cnblogs.com/gaox97329498/p/11915777.html
Copyright © 2020-2023  润新知