首页 > 娱乐前沿 > 热点
尝试手写一个更好用的performSelector/msgSend(详细修改版)
佚名 2016-01-16 19:20:36

本文是投稿文章,作者:唯敬

这其实是一个NSInvocation练习作业

GitHub源码 vk_msgSend

引子

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

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

1.这样要求子类的接口必须和基类完全一致

2.如果子类设计很多样,赋值UI的元素更多,就会不太合理

聊聊performSelector

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

-(id)performSelector:(SEL)aSelector;
-(id)performSelector:(SEL)aSelectorwithObject:(id)object;
-(id)performSelector:(SEL)aSelectorwithObject:(id)object1withObject:(id)object2;

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

就像我说的,以前我几乎只会去用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学习

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

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

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

本着程序员偷懒大法,这部分能不能也省略了?变得更方便一些?

设计我的callSelector的接口

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

Classcls=NSClassFromString(@"testClassA");
idabc=[[clsalloc]init];
NSError*err;
NSString*return1=[abcvk_callSelector:@selector(testfunction:withB:)error:&err,4,3.5f];

所以他的定义是这样的

+(id)vk_callSelector:(SEL)selectorerror:(NSError*__autoreleasing*)error,...;
+(id)vk_callSelectorName:(NSString*)selNameerror:(NSError*__autoreleasing*)error,...;
-(id)vk_callSelector:(SEL)selectorerror:(NSError*__autoreleasing*)error,...;
-(id)vk_callSelectorName:(NSString*)selNameerror:(NSError*__autoreleasing*)error,...;

实现这样的callSelector

可变参数接口透传的问题

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

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

-(id)vk_callSelectorName:(NSString*)selNameerror:(NSError*__autoreleasing*)error,...{
SELselector=NSSelectorFromString(selName);
[selfvk_callSelector:selectorerror:error,...];
}

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

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

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

staticNSArray*vk_targetBoxingArguments(va_listargList,Classcls,SELselector,NSError*__autoreleasing*error)

然后在调用的时候

va_listargList;
va_start(argList,error);
SELselector=NSSelectorFromString(selName);
NSArray*boxingAruments=vk_targetBoxingArguments(argList,[selfclass],selector,error);
va_end(argList);

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

参数包装

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

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

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

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

methodSignature=[clsinstanceMethodSignatureForSelector:selector];
for(inti=2;i<[methodSignaturenumberOfArguments];i++){
constchar*argumentType=[methodSignaturegetArgumentTypeAtIndex:i];
switch(argumentType[0]=='r'?argumentType[1]:argumentType[0]){
//抽取参数
}

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

1)取基础类型int,va_arg(argList, int)取值,包装成NSNumber(只举一个例子,其他见源码)

intvalue=va_arg(argList,int);
[argumentsBoxingArrayaddObject:@(value)];
break;

2)取CGSize,va_arg(argList, CGSize)取值,包装成NSValue(只举一个例子,其他见源码)

CGSizeval=va_arg(argList,CGSize);
NSValue*value=[NSValuevalueWithCGSize:val];
[argumentsBoxingArrayaddObject:value];
break;

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

这里要注意,如果传入的参数为nil,需要特殊处理一下,nil无法放入数组,所以我创建了一个vk_nilObject对象,来表明这个位置传进来nil了

idvalue=va_arg(argList,id);
if(value){
[argumentsBoxingArrayaddObject:value];
}else{
[argumentsBoxingArrayaddObject:[vk_nilObjectnew]];
}

4)取SEL,va_arg(argList,SEL),处理成string

因为SEL本身的意义就是一个函数的名字类似string一样的键值,是用来查找函数用的,所以当成字符串处理啦

SELvalue=va_arg(argList,SEL);
NSString*selValueName=NSStringFromSelector(value);
[argumentsBoxingArrayaddObject:selValueName];

5)取block,其实block就是id,所以和id的处理一模一样

//同id

6)取id*,va_arg(argList, void**)

这里需要注意一下,因为我取出来的是一个pointer,是不能直接放入数组里的,所以我创建了一个vk_pointer对象,持有一个void*属性,然后就可以塞进数组了

void*value=va_arg(argList,void**);
vk_pointer*pointerObj=[[vk_pointeralloc]init];
pointerObj.pointer=value;
[argumentsBoxingArrayaddObject:pointerObj];

遇到了一个va_arg()的坑

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

va_arg(argList,float)

xcode报了个warning

/Users/Awhisper/Desktop/GitHub/vk_msgSend/vk_msgSend/NSObject+vk_msgSend.m:280:49:Secondargumentto'va_arg'isofpromotabletype'float';thisva_arghasundefinedbehaviorbecauseargumentswillbepromotedto'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

1.首先先要生成NSInvocation

Classcls=[targetclass];
NSMethodSignature*methodSignature=vk_getMethodSignature(cls,selector);
NSInvocation*invocation=[NSInvocationinvocationWithMethodSignature:methodSignature];

2.设置target和SEL

[invocationsetTarget:target];
[invocationsetSelector:selector];

3.循环压入参数

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

遍历的时候肯定是根据每个参数的typeEncode,去switch处理不同类型

for(inti=2;i<[methodSignaturenumberOfArguments];i++){
constchar*argumentType=[methodSignaturegetArgumentTypeAtIndex:i];
idvalObj=argsArr[i-2];
switch(argumentType[0]=='r'?argumentType[1]:argumentType[0]){
//switchcase
}
}

这里我会详细分类别举例如何压入各种不同类型的参数,从[2]下表开始的原因和前边一致

[invocationsetArgument:&valueatIndex:i];`的作用就是压入参数

1)int等基础类型参数,对应上文的参数包装(只举一个例子,其他见源码)

intvalue=[valObjintValue];
[invocationsetArgument:&valueatIndex:i];
break;

2)CGSize基础结构体参数,对应上文参数包装(只举一个例子,其他见源码)

CGSizevalue=[valCGSizeValue];
[invocationsetArgument:&valueatIndex:i];

3)id参数,对应上文参数包装

上文提到如果传入的id为nil,被上文包装成了vk_nilObject对象扔进数组的,所以这里要针对这个处理一下

不是vk_nilObject的照常处理

是vk_nilObject,证明这个位置的参数传入方为空,所以我准备了一个空指针

staticvk_nilObject*vknilPointer=nil;

把这个空指针传进去

if([valObjisKindOfClass:[vk_nilObjectclass]]){
[invocationsetArgument:&vknilPointeratIndex:i];
}else{
[invocationsetArgument:&valObjatIndex:i];
}

4)SEL参数,对应上文包装

上文提到,SEL被直接转成了string,所以我们这里要还原成SEL,然后直接压入参数

NSString*selName=valObj;
SELselValue=NSSelectorFromString(selName);
[invocationsetArgument:&selValueatIndex:i];

5)block参数,对应上文包装

上文提到block和id是一回事

//同id

6)id*的处理,对应上文包装,这里极其恶心,我会专门写一篇详细说一下,这里只写个大概吧

上文已经把void*包装成了 vk_pointer,所以我们取出vk* 然后压入参数

vk_pointer*value=valObj;
void*pointer=value.pointer;
[invocationsetArgument:&pointeratIndex:i];

你以为这样就可以了么?你太天真了

如果断点调试,整个call_selector的过程完全走完都不会有事,但是一旦放开断点,彻底走完就崩溃。

为啥呢?因为在使用invocation的时候 invoke的过程中,如果对象在invoke内被创建初始化了,invoke结束后,在下一个autorelease的时间点就会产生zombie的crash,send release to a dealloc object

为什么会这样,简单的说下我的理解不细说吧,invoke和直接函数调用不太一样,如果发生了alloc对象,那么这个对象系统会额外多一次autorelease,所以,不会立刻崩溃,但当autoreleasepool释放的时候,就会发生过度release。

给几个LINK有兴趣大家可以深入探讨一下:栈溢出1,栈溢出2

看一下我的解决办法

vk_pointer*value=valObj;
void*pointer=value.pointer;
idobj=*((__unsafe_unretainedid*)pointer);
if(!obj){
if(argumentType[1]=='@'){
if(!_vkNilPointerTempMemoryPool){
_vkNilPointerTempMemoryPool=[[NSMutableDictionaryalloc]init];
}
if(!_markArray){
_markArray=[[NSMutableArrayalloc]init];
}
[_markArrayaddObject:valObj];
}
}
[invocationsetArgument:&pointeratIndex:i];

我会先判断一下 void*指向的对象是否存在,如果传入的是一个已经alloc init 好了的 mutableArray之类的对象,我会直接压入参数,因为invoke过程内,只是往mutableArray里面执行操作,并没有在void*指针处重新new的操作的话,是安全的不会崩溃的。

如果void*指向的对象不存在,相当于我传入了一个 NSError*,等着由invoke内部去创建,这样外面可以捕获,这种使用场景,就会导致crash,是因为过度release,那我的思路就是先把他持有一下。。。因为多了个release,那我再arc下不能强制retain,那我就add到一个字典里,让他被arc retain一下。

if([_markArraycount]>0){
for(vk_pointer*pointerObjin_markArray){
void*pointer=pointerObj.pointer;
idobj=*((__unsafe_unretainedid*)pointer);
if(obj){
@synchronized(_vkNilPointerTempMemoryPool){
[_vkNilPointerTempMemoryPoolsetObject:objforKey:[NSNumbernumberWithInteger:[(NSObject*)objhash]]];
}
}
}
}

这段代码放在[invocation invoke]之后,因为只有执行之后我们才知道void*指向的位置是否创建了新对象,判断obj是否存在,如果存在则向一个全局的static字典_vkNilPointerTempMemoryPool写入这个对象。

有人有更好的办法不?我想不到了,也求建议。

4.执行NSInvocation

[invocationinvoke];

注意上文提到的invoke后处理一下 id* 的内存问题

5.取出返回值 具体可以看下一篇 NSInvocation内存处理

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

constchar*returnType=[methodSignaturemethodReturnType];

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

1)int 等基础类型,注意我包装成了NSNumber* 返回的,后文有讲(只举一个例子,其他见源码)

intreturnValue;
[invocationgetReturnValue:&returnValue];
return@(returnValue);
break;

2)CGSize等基础类型,注意我包装成了NSValue* 返回的,后文有讲(只举一个例子,其他见源码)

CGSizeresult;
[invocationgetReturnValue:&result];
NSValue*returnValue=[NSValuevalueWithBytes:&(result)objCType:@encode(CGSize)];\
returnreturnValue;

3)id类型,这里面也有个坑。我是这么做的

void*result;
[invocationgetReturnValue:&result];
if(result==NULL){
returnnil;
}
idreturnValue;
returnValue=(__bridgeid)result;
returnreturnValue;

为什么这么做,是因为getReturnValue只是拷贝返回值到指定的地址,你现在返回的是一个id,是一个指针,那么实际对象会在函数runloop结束后自动释放的,原因很类似之前的id*参数问题,但是这里是返回值。

一个详细介绍这一块的博客

还有一点瑕疵

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

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

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

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

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

感谢

感谢bang哥,好多invocation的使用都是学习bang哥的JSPatch里面,拆解+学习

感谢彩虹,各种疑难杂症帮我一起动脑解决

上一篇  下一篇

I 相关 / Other

嫩模小棠致命诱惑

模特颖儿性感气质内衣写真

源码推荐(01.11B):iOS项目分层,Widget手机任务栏

iOS项目分层(上传者:踏浪帅)主项目中的分层主要包含四个模块,Main(主要)、Expand(扩展)、Resource(照片)、

通过Moya+RxSwift+Argo完成网络请求

作者:@请叫我汪二 授权本站转载。最近在新项目中尝试使用 Moya+RxSwift+Argo 进行网络请求和解析,感觉还阔

Loading动画外篇·圆的不规则变形

一款Loading动画的实现思路系列已经结束了,非常感谢大家的捧场。看过本系列的同学可能还记得,我对原动效做

I 热点 / Hot