前言
一般的开发者对“反射”这个词很陌生,事实上他们自己用过了都不知道。但是在做聚合SDK开发当中,用得最多的就是反射了。
现在我想实现反射调用该方法:
+ (NSString *)test:(id)test p1:(NSString *)p1 p2:(unsigned int)p2 p3:(double)p3 p4:(CGSize)p4 p5:(NSNull *)p5 p6:(void(^)(void))p6 p7:(BOOL)p7 p8:(char)p8 p9:(char *)p9 p10:(YY)p10 p11:(SEL)p11 p12:(void **)p12 p13:(void ***)p13 p14:(Class)p14 p15:(IMP)p15; // YY是自定义结构体;p9是C字符串
然而遗憾的是,系统提供的反射最多只能带2个参数,而且参数只能是对象,但事实上并不是所有的参数都是对象的(比如 void **)。
虽然网上有很多的资料,但我都试过,都不咋地,总有缺陷。没办法,只能自己动手撸一个了。
实现多参数反射目前我只知道有2种方式:
- runtime
- NSInvocation
实现方式的选择
虽然runtime号称是万能的,但事实上在64位里并不能直接使用objc_msgSend,必须强制转换成对应类型。可是,既然是不定参数、不定数据类型,所以我们根本无法去确定类型,换句话说,objc_msgSend是不可行的。
所以我们只能选择NSInvocation了。
一些坑和需要解决的问题
众所周知,OC是基于C的,事实上OC只是C的一层封装,所以我们使用的东西也是C的东西。
而不定参数的实现,也是用C的va_list。但va_list在取值之前是需要知道数据类型的,因为每次取值都要给个数据类型,然后va_list会根据该类型的长度去截取数据,如果给的数据类型不对,那截取的长度也会不对,就会导致之后的数据都不对了。
当然有人会说,那全部转成一个类型不就好了?
我开始也是这么想的,事实上网上大多数人的做法也是这样子。
但是,并不是所有类型都能转的。比如全部用对象,那么void **和C字符串怎么转呢?也有人会说C字符串包装成NSString啊,嗯,这是没问题的,可是你怎么还原呢?人家参数需要的是C字符串啊,你给个OC字符串??
另外,void *是一种类型,void **也是一种类型,char *也不一样,怎么处理呢?
对于结构体也是,每个自定义的结构体的类型都是不一致的。
此外,NSInvocation是不支持自动解包的。也就是说,如果你传了NSNumber对象当做参数,而调用的方法需求参数是int,你不自己解包成int的话就会数据不对。
所以,当下我们需要解决的问题就是数据类型的问题。
网上的一些实现方法
- 网上很多方法都是通过把所有参数加入到一个数组里,然后遍历数组把参数直接写到NSInvocation,但我之前说了,并不是所有的类型都能转成对象的,而且NSInvocation不支持自动解包,这样子当遇到基本数据类型参数的时候,势必会出错。
- 有些人使用不定参数来,然后全部转成对象,实际上和上面的大同小异。
- 另外我还发现了挺多人为了调用方便,用分类来实现。但是做SDK最怕的就是重名了,分类的话,如果重名导致的问题还不好搞。因此,我封装为类,如果有命名问题,只需要简单的改个前缀即可。
我的实现思路
我观察了NSString的声明,得到了一些启发:
+ (instancetype)stringWithFormat:(NSString *)format, ...
在NSString的创建方法里,系统使用了format来提供信息,也就是说,当匹配到%d的时候,就会认为是int,这个时候在va_list里取值int不就好了?
不过我并不打算这样子做,因为不单自己搞得麻烦,调用者也麻烦,每次都要写一串的%d%s什么的。
那么,有没有别的方法可以预知所需要的参数类型呢?
当然有了,NSMethodSignature里就有一个getArgumentTypeAtIndex:
方法,该方法可以获取到index位置参数的数据类型。到了这里,数据类型的问题我们就解决了一半了,也不需要想着怎么包装成对象了,直接按照实际类型传参岂不是更舒服?
接下来我们来解决void *、void **的问题。
对于指针类型,从一开始我就掉进了一个坑。众所周知,指针是一种派生类型,只要在某种类型后面加个*号,就能派生出该类型的指针。换句话说,指针的类型的无数的,所以我们不可能是判断无数的类型,但是,指针的储存空间是固定的,也就是说我们取值只需要取指针的长度即可。
剩下的就是结构体的问题了。
结构体是基本数据类型的一个包装,能实现无数的结构体类型。所以除了内置的常用结构体,其它自定义的是无法识别的,因此,可以通过包装成NAValue解决,而取值的时候,使用指针去取值就好了。
获取返回值的问题
事实上返回值的类型也是各种各种的。但是我们并不需要判断类型,我们只需要给一个指针就可以了,因为- (void)getReturnValue:(void *)retLoc;
这个方法只是简单的把指针指向返回值的地址而已。
不过需要注意的是,在ARC环境下,如果不使用__weak
或者__unsafe_unretained
修饰对象的话就会崩溃,因为从invocation获取的返回值并没有为我们增加引用计数,而我们定义变量时,默认就是__strong
类型的,ARC就会假设该内存块已被retain(实际没有),在出了定义域时就会release一次,导致返回值已经是销毁状态了,从而导致崩溃。
当使用
__weak
时控制台会打印该错误:Attempted to unregister unknown __weak variable at 0x7ffee9909f70. This is probably incorrect use of objc_storeWeak() and objc_loadWeak(). Break on objc_weak_error to debug.
该错误可以无视,上面的地址0x7ffee9909f70
其实就是我们给getReturnValue
参数的地址,对此感兴趣的同学可以用符号捕获来调试(全局异常断点是没用的)。即在Symbolic Breakpoint
的Symbolic
里输入objc_weak_error
即可调试。
代码就不贴了,这里只说实现的思路,想看具体的实现请到GitHub上下载源码。
获取源码请点击这里:ZTReflectionDemo
造个轮子不易,对你有用的请给个星星,谢谢了
iOS OC Swift Flutter开发群 139322447