1. 目标
工作中,需要解决这样一种问题,测试每一个目标接口的调用时间和返回值。逻辑较为通用,所以想使用反射来实现这种功能。
2. 背景知识
网上查了下,发现反射有两种实现方式,分别为 runtime中的objc_msgSend 和 NSInvocation。
2.1 objc_msgSend
具体可参考这篇文章:https://www.jianshu.com/p/ce00728204ed?utm_campaign
试用了下,发现可以实现函数调用,但不太好额外加入其他逻辑,所以不太适用于解决我的问题。
2.2 NSInvocation
具体可参考这篇文章:https://juejin.cn/post/6844903542851895304 (虽然文中提到了NSProxy,但看实际代码里并未使用到) 和 https://ace.re/2017/objective-c-nsinvocation.html
试用了下,发现基本可以达成我的目标。但有一个问题,系统提供的performSelector函数,最多仅支持传两个参数。这显然是不够的。所以我们需要把这块优化下。我尝试了两种解决方式。
2.2.1 传参使用NSArray
具体可参考这个篇文章:https://blog.csdn.net/a158337/article/details/50915245
2.2.2 使用可变参数传参
具体可参考这篇文章:https://www.jianshu.com/p/1dc0d05cb766。基本可用,但文章中获取返回值处需要优化下。看网上的说法,文章中获取返回值处可能会造成崩溃。
3. 我的实现
3.1 使用可变参数传参
@implementation InterfaceProxy //方法签名 - (id)methodSignatureForSelector:(SEL)sel { NSMethodSignature *signature = nil; if (self.obj) { return signature; } signature = [self.obj methodSignatureForSelector:sel]; return signature ? signature : [super methodSignatureForSelector:sel]; } - (NSDictionary *)performSelector:(NSObject *)obj sel:(SEL)sel withObject:(id)object,...NS_REQUIRES_NIL_TERMINATION { self.obj = obj; NSMethodSignature *signature = [self methodSignatureForSelector:sel]; //根据类名以及SEL 获取方法签名的实例 if (signature == nil) { NSLog(@"--- 未找到符合条件的方法 ---"); return nil; } //NSInvocation是一个消息调用类,它包含了所有OC消息的成分:target、selector、参数以及返回值。 NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; invocation.target = self.obj; invocation.selector = sel; NSUInteger argCount = signature.numberOfArguments; // 参数必须从第2个索引开始,因为前两个已经被target和selector使用 argCount = argCount > 2 ? argCount - 2 : 0; NSMutableArray *objs = [NSMutableArray arrayWithCapacity:0]; if (object) { [objs addObject:object]; va_list args; va_start(args, object); while ((object = va_arg(args, id))){ [objs addObject:object]; } va_end(args); } if (objs.count != argCount){ NSLog(@"proxy --- 传参有误! please check it! ---"); return nil; } //设置参数列表 for (NSInteger i = 0; i < objs.count; i++) { id obj = objs[i]; [invocation setArgument:&obj atIndex:i+2]; } NSTimeInterval start = [TimeUtil currentTimeStr]; [invocation invoke]; NSTimeInterval stop = [TimeUtil currentTimeStr]; NSString *selName = NSStringFromSelector(invocation.selector); NSLog(@"proxy selName: %@, start: %f stop:%f interval: %f", selName, start, stop, (stop - start)); //获取返回值 NSUInteger length = [signature methodReturnLength]; const char *retType = signature.methodReturnType; id retVal; if (signature.methodReturnLength != 0 && signature.methodReturnLength) { if (!strcmp(retType, @encode(id))) { [invocation getReturnValue:&retVal]; } else { void *buf = (void *)malloc(length); [invocation getReturnValue:buf]; retVal = [self transRet:buf retType:retType]; } } NSDictionary *dic = [[NSDictionary alloc] initWithObjectsAndKeys:selName,SELECTOR_NAME, objs, INTERFACE_PARAMS, [NSNumber numberWithDouble:start], START_TIME_STAMP, [NSNumber numberWithDouble:stop], STOP_TIME_STAMP, retVal, RET, nil]; return dic; } - (id)transRet:(void *)returnValue retType:(const char *)returnType { if (!strcmp(returnType, @encode(int))) { return [NSNumber numberWithInteger:*((NSInteger*)returnValue)]; } if (!strcmp(returnType, @encode(BOOL))) { int tmp = *((BOOL *)returnValue) ? 0 : -1; return [NSNumber numberWithInt:tmp]; } return nil; } @end
调用方法:
InterfaceProxy proxy = [InterfaceProxy alloc]; NSDictionary *dic = [proxy performSelector:self->_member sel:@selector(funcName:param1:param2:) withObject:@"param0",@"param1",@"param2", nil];
4. 待优化的问题
此方法可适用于大多数场景,但有些地方仍不尽如人意。其中之一,就是传参必须是NSObject的子类。如果是C++的类,就传不进去。因为无法将其转为id。其二,不能方便的调用C语法的函数。只有成员方法或类方法,可以这样方便的调用。若开发中需调用的接口为C语法的接口,则调用起来会麻烦很多,不如将C语法的接口,包装成OC的风格。