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的风格。

 

posted @ 2021-01-18 12:52  myLittleGarden  阅读(478)  评论(0编辑  收藏  举报