iOS 通过反射的方式调用目标函数
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的风格。

浙公网安备 33010602011771号