iOS关于动态执行方法的探索-performSelect-NSInvocation-objc_msgSend

2,363 阅读4分钟

1.jpg

我为什么会写这篇文章

今天在写Bugly上报错误的时候遇到了一个问题,由于项目使用组件化开发思想,在上报错误组件里需要知道当前是否导入了Bugly组件,如果没有Bugly组件,则不进行Bugly上报,我第一个想到了用NSClassFromString来判断是否存在Bugly,如果存在,再使用performSelector:withObjectruntime调用Bugly上报方法,但是Bugly输出日志的方法BuglyLog level:<#(BuglyLogLevel)#> log:<#(NSString *), ...#>第一个参数需要传入NSUInterger类型的基本数据,但是performSelector:withObject方法只能传入OC对象,所以我在解决问题的同时顺带研究了一下OC执行方法,消息传递的几种常见方法。

第一种方式

performSelector方式

这应该是开发过程中动态执行方法最常用的方式了吧,最常见的用法就是performSelector:withObject,或者可以再带一个参数performSelector:withObject:withObject:,就像这样调用:

id BuglyClass = (id)NSClassFromString(@"Bugly");
NSError *error = [NSError errorWithDomain:errorTitle code:-1 userInfo:@{NSLocalizedDescriptionKey:@""}];
[BuglyClass performSelector:@selector(reportError:) withObject:error];

也有延时触发的用法

performSelector:withObject:afterDelay:

在某个线程中执行的用法,当然这些不是本文讨论的重点,所以一笔带过

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
其实不难发现performSelector方式有一些弊端

1.不能传超过三个参数 2.参数只能为OC对象

第二种方式

NSInvocation方式

先不说啥,直接上代码

NSString *string = @"test";
NSNumber *number = [NSNumber numberWithInt:1];

SEL selector = @selector(function2:count:);
NSMethodSignature *signature = [self methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];

invocation.target = self;
invocation.selector = selector;
// 第一和第二个参数是target和selector
[invocation setArgument:&string atIndex:2];
[invocation setArgument:&number atIndex:3];
//执行
[invocation invoke];
  • NSMethodSignature: 方法签名,如果方法不存在或者参数对不上,会直接crach,其实我也不是很理解这个签名的意思,可能是代码写得不够多吧,按照我的理解,这个数字签名就像是找一下这个target里面是否有这个方法,参数是否正确,如果都对,则签名生效,否则就可能奔溃
  • NSInvocation: 包装了一次消息传递的所有内容,包括target,select,argument等等,会在运行时找到目标发送消息
  • 注意这里传的参数是地址,所以还不不满足我们的需求

为了解决今天上报Bugly的问题,我还得继续探索

搜索中... 搜索中... 搜索中... 找到方案了!

第三种方式

函数指针方式

上代码

id target = self;
SEL selector = @selector(function:count:);
// 第一个第二个参数是self和selector
typedef void (*function)(id, SEL, NSString *, int);
function methodToCall = (function)[target methodForSelector:selector];
methodToCall(target, selector, @"string",1);

可能有些小伙伴没有见过这种用法,我其实也是解决Bugly问题的时候才了解到的,不过看代码应该是能看懂的,首先用selector找到要调用的方法,再定义一个函数指针,指向selector选择的方法,然后调用这个函数执行,我试了下,能完美解决文章开头我遇到的问题

继续探索

第四种方式

最底层的消息传递方式

上代码

id BuglyLogClass = (id)NSClassFromString(@"BuglyLog");
SEL selector = @selector(level:log:);
void (*logAction)(id, SEL, NSUInteger,NSString *) = (void (*)(id, SEL, NSUInteger,NSString *))objc_msgSend;
logAction(BuglyLogClass, selector, 1,@"description");

这是我最后使用的方法。所有target执行方法在底层都是通过objc_msgSend消息传递实现的,要执行的方法归根结底就是一条消息,发送给目标target,我们来看一下Apple官方的解释

/* Basic Messaging Primitives
 *
 * On some architectures, use objc_msgSend_stret for some struct return types.
 * On some architectures, use objc_msgSend_fpret for some float return types.
 * On some architectures, use objc_msgSend_fp2ret for some float return types.
 *
 * These functions must be cast to an appropriate function pointer type 
 * before being called. 
 */
void objc_msgSend(void /* id self, SEL op, ... */ )

其实performSelector的底层实现也是调用了objc_msgSend实现消息发送

- (id)performSelector:(SEL)sel {
    if (!sel) [self doesNotRecognizeSelector:sel];
    return ((id(*)(id, SEL))objc_msgSend)(self, sel);
}

研究到这里,我已经解决了我的Bugly传参数上报日志的问题,在此期间学到了很多知识,可能有些写的不是很正确,望大牛指正。