【问题标题】:How do I observe the invocation of a selector如何观察选择器的调用
【发布时间】:2013-12-10 02:45:47
【问题描述】:

我们有时想观察选择器的调用,以便我们可以自动对调用执行预处理或后处理。

如果目标是观察中的自定义类,这并不难。有多种方法可以实现这一目标。但是,对于任何课程都很难做到这一点。例如,观察 [NSViewController -initWithNibName:bundle:], [NSObject -init]

我试图做的是:

void observe(Class clazz, SEL op, isClassMethod, CALLBACK pre, CALLBACK post);

为了做到这一点,我需要定义:

id replaceMethod(id target, SEL op, ...);

void replaceMethod2(id target, SEL op, ...);

...

然后在上面的观察函数内部,我得到了该类的原始实现并将其放入地图中。

WFFuncWrap *wrap;
NSString *key;
...
wrap = [[WFFuncWrap alloc] init];
wrap->_isClassMethod = isClassMethod;
if (isClassMethod) {
    wrap->_method = class_getClassMethod(clazz, op);
}else{
    wrap->_method = class_getInstanceMethod(clazz, op);
}
wrap->_func = method_getImplementation(wrap->_method);
[_dict setObject:wrap forKey:key];
...

然后,我使用“class_replaceMethod”函数将类的原始实现替换为上述“replacedMethod”函数之一。

在这些“replacedMethod”函数中,我在开始和结束时调用回调。理论上,我只需要查找原始实现并将其放在“pre”和“post”之间即可实现目标。但是,这是我无法解决的困难部分。这很困难,因为签名原始实现不固定。

我没有找到在另一个 gunc(id, SEL, ...) 旁边调用 func(id, SEL, ...) 的方法。实际上,我相信汇编代码将有助于解决问题,但那是我不熟悉的东西。我试过了,但是太复杂了。

id replacedMethod(id obj, SEL op, ...){
    CALLBACK pre, post;
    IMP originalImp;
    id retObj;

    ...
    pre(obj, op);
    //call original implementation, HOW ?
    post(obj, op);
    return retObj;
}

有什么办法可以解决这个问题吗?非常感谢!!

【问题讨论】:

    标签: objective-c .net-assembly sendmessage forwarding


    【解决方案1】:

    一种完全不同的方法来做你想做的事情,所以我不知道这是否会有所帮助,是创建一个代理类,可以将调用它的所有方法转发到目标类(查看 NSProxy) ,然后您可以使用 -[NSObject forwardInvocation:] 它为您提供打包在 NSInvocation 对象中的所有参数。在运行时,您的代理对象可以传递几乎任何其他类的自身,它们可能是一些边缘情况,因为您的代理类可以覆盖 -[NSObject isKindOfClass:]、-[NSObject respondsToSelector:] 等方法。

    然后你可以在方法调用之前和之后做任何你想做的事情。

    【讨论】:

    • 感谢您的回复。我相信 NSProxy 可能是我实现最终目标的解决方案。但是,与“class_replaceMethod”相比,我认为 NSProxy 对性能的影响更大。那是因为 NSProxy 方法将观察观察类的任何方法,但替换方法只影响那些被替换的方法。无论如何,如果我无法解决问题,我可以使用 NSProxy 方法。再次感谢。
    • 如果你实现forwardingTargetForSelector:,它将允许你以更低的开销转发你没有覆盖的方法。请参阅mikeash.com/pyblog/…,了解有关调度工作原理的一些有用讨论。
    【解决方案2】:

    感谢 Nathan Day,他给了我使用 NSProxy 的想法。如果不需要认真考虑对性能的影响,这是对我的问题的一个干净的解决方案。我没有将 NSProxy 作为我的解决方案,因为我的项目中非常重视性能。

    在不知道其签名的情况下将调用转发到另一个函数在 c 语言中是很困难的。实际上,没有办法调用真正不知道其签名的函数,因为在这种情况下,我们将无法知道在哪里读取以及如何传递参数。

    我发现,不同的 cpu 架构和编译器有不同的调用约定。 32位cpu上的gcc编译器更容易处理,因为参数是按顺序一一存储的。一旦你知道它们的类型、顺序和基本地址,你就可以阅读它们并传递它们。

    但是,64 位 cpu 的 gcc 以复杂的方式存储参数。整数、流点和溢出的参数存储在内存中的 3 个不同的段中。 structs 参数可以在不同情况下存储在所有三个段中。仅知道类型、顺序和基本地址还不足以找出参数的地址,还需要确切的约定规则。我找到了约定的规则。但是,没有文件证明我发现的实际上是编译器所做的。此外,这需要处理运行时方法描述,例如:'@32@0:8@16{?=id}24',不易解析。

    如果有一种方法可以告诉参数的确切偏移量,那么我的问题就可以解决。有一个函数'method_getArgumentInfo',看起来像我在找的。但是,它在 OBJC2 中不可用

    幸运的是,我终于发现 NSMethodSignature 类的调试描述可以通过提供标记参数偏移量的字符串描述来帮助我找出参数所在的位置:

    number of arguments = 3
    frame size = 248
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 8, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 2: -------- -------- -------- --------
        type encoding ({) '{?=[3q]}'
        flags {isStruct}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 0, size adjust = 0}
        memory {offset = 0, size = 24}
            type encoding ([) '[3q]'
            flags {isArray}
            modifiers {}
            frame {offset = 224, offset adjust = 0, size = 24, size adjust = 0}
            memory {offset = 0, size = 24}
                type encoding (q) 'q'
                flags {isSigned}
                modifiers {}
                frame {offset = 224, offset adjust = 0, size = 8, size adjust = 0}
                memory {offset = 0, size = 8}
                type encoding (q) 'q'
                flags {isSigned}
                modifiers {}
                frame {offset = 232, offset adjust = 0, size = 8, size adjust = 0}
                memory {offset = 8, size = 8}
                type encoding (q) 'q'
                flags {isSigned}
                modifiers {}
                frame {offset = 240, offset adjust = 0, size = 8, size adjust = 0}
                memory {offset = 16, size = 8}
    

    在 NSInvocation 和 class_replaceMethod 的帮助下,我终于找到了解决方案。缺点是不能保证 NSMethodSignature 的描述总是描述准确的信息。

    以下代码段是解决方案的详细实现。 笔记 1.这不是一个完整的实现,只是展示这样做的想法。 2. 这段代码只用 x64 cpu 测试过 3. 为了处理任何选择器,它需要更多的代理函数来处理不同的返回类型

    #import "WFObserveMessageException.h"
    #import <objc/runtime.h>
    
    @interface WFImplementationInfo : NSObject{
        @package
        Class _class;
        IMP _func;
        Method _method;
        BOOL _isClassMethod;
        NSInvocation *_invoke;
        int _countArg;
        int *_argOffsets;
        _wf_message_observer_t _observer;
    }
    @end
    
    
    @implementation WFImplementationInfo
    -(void)dealloc{
        if (_argOffsets) {
            free(_argOffsets);
            _argOffsets = NULL;
        }
    }
    @end
    
    
    
    NSMutableDictionary *getFunctionDict(){
        static NSMutableDictionary *dict;
        if(!dict){
            dict = [NSMutableDictionary dictionary];
        }
        return dict;
    }
    
    NSString *keyForClassAndOp(NSString *className, NSString *opName, BOOL isClassMethod){
        return [NSString stringWithFormat:@"%c[%@ %@]", (isClassMethod ? '+' : '-'), className, opName];
    }
    
    WFImplementationInfo *prepareInvocation(id obj, SEL op, va_list ap){
        Class clazz;
        WFImplementationInfo *func;
        BOOL isMetaClass;
        NSString *key;
        NSDictionary *dict;
    
        clazz = [obj class];
        dict = getFunctionDict();
        //search and see if the this class or its parent classes is observed, there must at lest one of them is observed
        while (clazz) {
            isMetaClass = class_isMetaClass(clazz);
            key = keyForClassAndOp(NSStringFromClass(clazz), NSStringFromSelector(op), isMetaClass);
            func = [dict objectForKey:key];
            if (func) {
                break;
            }
            clazz = class_getSuperclass(clazz);
        }
    
        func->_invoke.target = obj;
        for (int i = 2; i < func->_countArg; i++) {
            //set up the arguments of the invocation.
            [func->_invoke setArgument:ap->reg_save_area + func->_argOffsets[i] atIndex:i];
        }
        return func;
    }
    
    
    id agent(id obj, SEL op, ...){
        va_list ap;
        WFImplementationInfo *func;
        id retObj;
    
        //the va_list could tell where is the base of the arguments
        va_start(ap, op);
        func = prepareInvocation(obj, op, ap);
        va_end(ap);
        class_replaceMethod(func->_class, op, func->_func, nil);
        func->_observer(func->_invoke, &retObj);
        class_replaceMethod(func->_class, op, (IMP)agent, nil);
        return retObj;
    }
    
    void wf_observe_message(NSString *className, NSString *opName, _wf_message_observer_t observer){
        WFImplementationInfo *func;
        NSMethodSignature *signature;
        NSScanner *scanner;
        NSString *desc, *key;
        SEL op;
        int sign, count;
    
        func = [[WFImplementationInfo alloc] init];
        func->_observer = observer;
        func->_class = NSClassFromString(className);
    
        sign = [opName characterAtIndex:0];
        opName = [opName substringFromIndex:1];
        op = NSSelectorFromString(opName);
        switch (sign) {
            case '-':
                func->_isClassMethod = NO;
                func->_method = class_getInstanceMethod(func->_class, op);
                signature = [func->_class instanceMethodSignatureForSelector:op];
                break;
            case '+':
                func->_isClassMethod = YES;
                func->_method = class_getClassMethod(func->_class, op);
                signature = [func->_class methodSignatureForSelector:op];
                break;
            default:
                WFThrow WFObserveMessageExceptionA(@"Selector name MUST start with '-' or '+' for indicating whether it is a instance method");
                break;
        }
    
        key = keyForClassAndOp(className, opName, func->_isClassMethod);
    
        func->_func = method_getImplementation(func->_method);
        func->_countArg = method_getNumberOfArguments(func->_method);
        func->_argOffsets = malloc((func->_countArg) * sizeof(int));
        func->_invoke = [NSInvocation invocationWithMethodSignature:signature];
        func->_invoke.selector = op;
        func->_argOffsets[0] = 0; //offset of id
        func->_argOffsets[1] = 8; //offset of SEL
        count = 2;
    
        desc = [signature debugDescription];
        scanner = [NSScanner scannerWithString:desc];
        [scanner scanUpToString:@"argument 2" intoString:nil];
    
        //scan the offsets of the arguments
        while (!scanner.isAtEnd) {
            [scanner scanUpToString:@"offset = " intoString:nil];
            scanner.scanLocation = scanner.scanLocation + 9;
            [scanner scanInt:&func->_argOffsets[count]];
            if(func->_argOffsets[count] == 0){  //if the offset is 0, that means the argument is a struct, the offset of the struct is the offset of the its first member
                [scanner scanUpToString:@"offset = " intoString:nil];
                scanner.scanLocation = scanner.scanLocation + 9;
                [scanner scanInt:&func->_argOffsets[count]];
            }
            [scanner scanUpToString:@"argument" intoString:nil];
            count++;
        }
    
        [getFunctionDict() setObject:func forKey:key];
    
        //check if the method is valid
        if (!func->_method) {
            WFThrow WFObserveMessageExceptionA(@"Class has no selector '%@' for class '%@'", opName, className);
        }
        class_replaceMethod(func->_class, op, (IMP)agent, nil);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-02-11
      • 1970-01-01
      • 2012-07-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多