感谢 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);
}