Skip to content

尝试手写一个更好用的performSelector msgSend

Awhisper edited this page Dec 31, 2015 · 4 revisions

###————这其实是一个NSInvocation练习作业

##引子

  • 工作中难免会遇到一些场景,开发的时候不想引入整个头文件,但是又想调用一些方法
  • 动态创建,动态调用看起来比较酷
  • 这种使用场景确实不常见,导入了头文件最省事,最直接,但是这种方式我觉得能搞出很多好玩的东西

一个群里聊天的时候聊到了一个场景,tableView内的cell有N种样式,在cellForRow的时候,通过NSClassFromString从字符串创建对象,然后挨个对Cell的UI赋值,接下来问题就来了。

实在不想import如此繁多cell.h头文件应该怎么办?

  • 有一个办法,所有cell都有个基类,基类统一所有UI赋值的接口,子类重载这些UI赋值,这样创建出来的对象强转成基类,调用基类的接口。这样只需要import一个基类头文件就够了
    • 这样要求子类的接口必须和基类完全一致
    • 如果子类设计很多样,赋值UI的元素更多,就会不太合理
  • 还有一个办法performSelector,恩说实话,我觉得很不好用
  • 会有人说用运行时Objc_msgSend,恩,这个靠谱,听起来也挺易用的
  • 老老实实引入各种头文件,别搞什么动态创建,动态调用的花样了

##聊聊performSelector 这里不是说performSelector中关于异步调用的那一部分,而是单说同步的:

- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;

这个是NSObject系统开放的performSelector同步接口,这个好用么?我以前觉得很不好用

  • 参数类型:我凑,不需要参数的接口用起来最直观,我也觉得还算好用,一旦需要参数,withObject:id是什么鬼?我传BOOL,传NSInteger怎么传啊?我包装成NSNumber对面能认识么?
  • 参数个数:为毛只能不带参数,1个参数,2个参数呢?我想调用的东西含参数特别多咋办啊?
  • 调用写法:每个参数还得用withObject来传,写出来一点都不酷

就像我说的,以前我几乎只会去用performSelector调用无参数的函数,一旦有参数,我都不爱用performSelector

##聊聊objc_msgSend 大家都知道OC的消息机制,函数调用其实都是发送消息,这个太多的地方有讲了,我就不多说了。

一个我们想要调用的函数

- (int) doSomething:(int) x { ... }

在32位的时代,想要实现我要的效果,可以直接使用objc_msgSend

objc_msgSend(self,@selector(doSomething:), 0);

但是一旦在64位设备上执行,就会产生崩溃,原因参见苹果Converting Your App to a 64-Bit Binary,中Take Care with Functions and Function Pointers,这一部分。

简单的说,64位下runtime调用和32位变化十分大,尤其是读取函数参数列表,进行传参这部分,所以苹果列出了一句话

###Always Define Function Prototypes

###Function Pointers Must Use the Correct Prototype

直接的调用C函数指针的时候必须先进行严格的类型匹配强转,不能直接使用Imp这个通用型的指针。

而objc_msgSend的内部实现也是一个这样的过程,objc_msgSend学习

  • 先从runtime method cache里面查找selector,
  • 找不到再从 method list里查找,
  • 找到selector,获取具体实现的ImpC函数,
  • 调用Imp

所以在64位下,直接使用objc_msgSend一样会引起崩溃,必须进行一次强转

((void(*)(id, SEL,int))objc_msgSend)(self, @selector(doSomething:), 0);

所以以前32位的时候objc_msgSend是我们最方便的做法,现在64位了,他已经不是那么方便了,毕竟使用起来还需要人自行手写这部分强转工作

###本着程序员偷懒大法,这部分能不能也省略了?变得更方便一些? ##设计我的callSelector的接口

我希望我设计的接口是这样的

Class cls = NSClassFromString(@"testClassA");
id<vk_msgSend> abc = [[cls alloc]init];
NSError *err;
NSString *return1 = [abc vk_callSelector:@selector(testfunction:withB:) error:&err,4,3.5f];
  • 它是一个NSObject的Category,只要你对强转成遵从<vk_msgSend>的id对象,就能直接调用
  • 它像performSelector一样输入SEL做参数执行,但是传参非常容易,基础类型,struct都支持,不需要withObject,不需要转成id,只需要像NSLog()一样,按顺序输入可变参数就好。
  • 有一个error指针可以用来返回错误信息,也可以填nil不传
  • 它支持类方法
  • SEL参数还可以改传字符串

所以他的定义是这样的

+ (id)vk_callSelector:(SEL)selector error:(NSError *__autoreleasing *)error,...;

+ (id)vk_callSelectorName:(NSString *)selName error:(NSError *__autoreleasing *)error,...;

- (id)vk_callSelector:(SEL)selector error:(NSError *__autoreleasing *)error,...;

- (id)vk_callSelectorName:(NSString *)selName error:(NSError *__autoreleasing *)error,...;

##实现这样的callSelector

###可变参数接口透传的问题 既然接口设计的希望使用者怎么简单怎么来,使用者用可变参数的方式一字罗列所有参数,无需转id之类的。那我们也得按照可变参数去处理。

这里我遇到了一个问题,我一共设计4个接口,这4个接口其实大同小异,核心逻辑是一样的,所以我肯定是用一个公共的方法进行处理,但是,可变参函数怎么透传呢?

- (id)vk_callSelectorName:(NSString*)selName error:(NSError*__autoreleasing*)error,...{
    SEL selector = NSSelectorFromString(selName);
    [self vk_callSelector:selector error:error,...];

}

我希望这样就能搞定,把...原封不动的塞到下面那个函数,可是xcode不认呐亲╮(╯_╰)╭

后来公司讨论组里有位大神给出了建议,直接把va_list当做公共函数的参数,进行透传

设计公共方法的接口声明为,第一个参数就是va_list

static NSArray *vk_targetBoxingArguments(va_list argList, Class cls, SEL selector, NSError *__autoreleasing *error)

然后在调用的时候

va_list argList;
va_start(argList, error);
SEL selector = NSSelectorFromString(selName);
NSArray *boxingAruments = vk_targetBoxingArguments(argList, [self class], selector, error);
va_end(argList);

va_start获取va_list然后就可以一层层的透传给公共方法进行处理了

###参数包装 虽然输入接口可以支持任意的类型,基础类型,struct,id,但是我内部实现的时候,还是把它们统一转换成了id,方便后续传递处理,这个步骤就是包装一下所有传进来的参数,也就是上面提到的vk_targetBoxingArguments

这个包装的过程涉及到va_list的取值过程va_arg了,这里我也踩了个大坑。容我细细道来

  • 从va_list里面一个一个的取出参数需要明确知道,每一个参数的类型,但是我们想做的是一个通用型的方法,这块就不能写死,可是从哪知道参数类型呢? -- NSMethodSignature

NSMethodSignature我理解他其实就是SEL的typeEncode的对象封装,分别记录了这个SEL的返回值类型和各个参数类型

我们有调用对象,就能获取到对象的Class,我们有SEL,就能获取到NSMethodSignature

 methodSignature = [cls instanceMethodSignatureForSelector:selector];
  • 有了NSMethodSignature我们就能按着循环去获取每个参数类型,从而读取va_list了。

for (int i = 2; i < [methodSignature numberOfArguments]; i++) {
    const char *argumentType = [methodSignature getArgumentTypeAtIndex:i];
    switch (argumentType[0] == 'r' ? argumentType[1] : argumentType[0]) {
    	//抽取参数
    }        

NSMethodSignature中前两个分别代表返回值和reciever,我们在抽取参数,所以直接从[2]下标开始取值,剩下的就是一个根据typeEcode,从va_list取值,然后包装成id,塞入数组的过程了,具体到每一种类型的case,可以参见源码。

取基础类型int,va_arg(argList, int)取值,包装成NSNumber

case ‘i’: {
	int value = va_arg(argList, int);
	[argumentsBoxingArray addObject:@(value)];
	break; 
}

取CGSize,va_arg(argList, CGSize)取值,包装成NSValue

  CGSize val = va_arg(argList, CGSize);
  NSValue* value = [NSValue valueWithCGSize:val];
  [argumentsBoxingArray addObject:value];
  break;

取id,va_arg(argList, id),不包装,直接塞进去啦

id value = va_arg(argList, id);
if (value) {
	[argumentsBoxingArray addObject:value];
}else{
	[argumentsBoxingArray addObject:[vk_nilObject new]];
}
  • 遇到了一个va_arg()的坑

我在调试中,发现当我对typeEncode的f取参数的时候

va_arg(argList, float)

xcode报了个warning

/Users/Awhisper/Desktop/GitHub/vk_msgSend/vk_msgSend/NSObject+vk_msgSend.m:280:49: Second argument to 'va_arg' is of promotable type 'float'; this va_arg has undefined behavior because arguments will be promoted to 'double'

一开始我看到warning没管,就继续编码去了,结果运行的时候,参数里含有float,发现了大问题

正如warning所说,此处编译器是按着double实现的,但是我用va_arg()取的时候按着float取,就直接导致我取出来的float值不对,是0,(一个比较小的double值取了前面几位自然都是0)

而float后面那个参数,id用va_arg(argList, id)取的时候直接崩溃,(指针已经乱了,从double的中间开始,按着id的长度取id,直接崩溃)

老老实实的修掉warning,改成用va_arg(argList, double)处理f,一切正常。

###实现调用:NSInvocation 我们现在已经拿到了包装好的参数数组NSArray,可以开始调用函数了,使用NSInvocation

  • 首先先要生成NSInvocation

Class cls = [target class];
NSMethodSignature *methodSignature = vk_getMethodSignature(cls, selector);
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
  • 设置target和SEL

[invocation setTarget:target];
[invocation setSelector:selector];
  • 循环压入参数

具体过程和Boxing一样,遍历methodSignature,按着typeEncode来从数组中取出id类型的参数,还原参数,压入invocation

这里只拿一种基础类型举例,具体可以看源码

case ‘i’: {
int value = [valObj intValue];
[invocation setArgument:&value atIndex:i];
break; 
}

[invocation setArgument:&value atIndex:i];的作用就是压入参数

  • 执行NSInvocation

[invocation invoke];
  • 取出返回值

如同压入参数一样,还是通过typeEncode来判断返回类型

const char *returnType = [methodSignature methodReturnType];

从invocation按类型取出返回值,返回

case 'i': {
	int returnValue;
	[invocation getReturnValue:&returnValue];
	return @(returnValue);
	break;
}

##还有一点瑕疵

注意我的返回值被强迫指定成了id,也就是说,如果原函数返回的是NSInteger,我会返回一个NSNumber。

为什么会这样?我搞不定如何在声明函数的时候,用一个兼容基础和id,所有类型的符号来定义函数。。

参数之所以可以兼容id与基础类型,是因为我用可变参数...绕过去了。。

但是返回值我就搞不定了,有人说用void *但我的初衷是希望使用者直接拿到最终的值,目前的困难不是如何把值传出去。而是传出去一个使用者不需要手动转换的最终结果。

void *这么看和用id 其实也差不多,使用者拿到后都得转一下。。。

##持续扩展中

  • 目前还不支持block型的参数
  • 目前还不支持SEL型的参数
  • 目前还不支持id * 型的参数

应该好弄╮(╯_╰)╭,等有时间了扩展上去

Clone this wiki locally