如何观察选择器的调用

时间:2013-12-10 02:45:48

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

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

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

我试图做的事情是:

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

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

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

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

...

然后在上面的observe函数内部,我得到了类的原始实现并将其放入映射中。

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'函数用上面的'replacementMethod'函数替换该类的原始实现。

在这些'replacementMethod'函数中,我在开始和结束时调用回调。从理论上讲,我只需要查看原始实现并将其置于'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;
}

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

2 个答案:

答案 0 :(得分:1)

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

然后,您可以在方法调用之前和之后执行任何操作。

答案 1 :(得分:0)

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

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

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

但是,64位cpu的gcc以复杂的方式存储参数。整数,流点和溢出参数存储在内存中的3个不同段中。结构参数可以存储在不同情况下的所有三个段中。只知道类型,顺序和基本地址不足以找出参数的地址,但也需要约定的确切规则。我找到了惯例规则。但是,没有文件证明我发现的实际上是编译器的功能。而且,这需要处理运行时方法描述,例如:'@ 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);
}