1、前言
我们在分析Block的动态调用之前,先简单了解一下消息的转发机制。作为一个iOS开发者,消息的转发机制应该都是我们耳熟能详的知识点了,这里贴一个转发流程图,不再占用篇幅:
NSMethodSignature
和NSInvocation
来分析Block的动态调用。
2、实例探究
先解释一些概念
方法的构成:在runtime源码中,可以清晰的看到,方法method_t
是由SEL
+types
+IMP
组成。
1、SEL:方法的编号。
2、types:方法的签名。简单的说方法签名包含:返回值、参数、参数类型,一个简单的例子v@:
,v
对应返回值为void,@
对应一个id类型的对象,:
对应SEL
。具体对比可以参照Type Encodings。
3、IMP:一个函数指针,保存的是方法的地址。
接下来我们来看下面这段代码:
- (void)viewDidLoad {
[super viewDidLoad];
// 方法的签名
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:"];
// NSInvocation对象
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:@selector(testInvocation)];
[invocation invoke];
}
- (void)testInvocation {
NSLog(@"testInvocation");
}
显然,我们的testInvocation
方法会被调用。由我的 上篇文章 可以知道Block的本身也是对象,那Block如何进行动态调用呢?
我们再看下面Block动态调用的简单写法:
- (void)viewDidLoad {
[super viewDidLoad];
void (^testBlock)(NSString *, double) = ^(NSString *test, double a) {
NSLog(@"block test %@ -- %.1f", test, a);
};
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?@d"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:testBlock];
NSString *str = @"invocation block";
[invocation setArgument:&str atIndex:1];
double dou = 0.1;
[invocation setArgument:&dou atIndex:2];
[invocation invoke];
}
这是一段带有参数Block的动态调用代码,值得说的是,在一般方法的NSInvocation 中setArgument : atIndex :
的index传的值是从2开始的,而Block的动态调用是从1开始的。原因是跟我们的方法签名有关,在越界信息中我们是可以看到所有types
的遍历是从-1开始的-[NSInvocation setArgument:atIndex:]: index (3) out of bounds [-1, 2]
,一般方法的v@:
分别是3个独立的代表,而在Blockv@?
中@?是绑定一起的。(没有相关解释文档)
首先看调用的API:
+ (id<AspectToken>)aspect_hookSelector:(SEL)selector
withOptions:(AspectOptions)options
usingBlock:(id)block
error:(NSError **)error;
来看源码中定义了AspectBlockRef
的结构体:
AspectIdentifier
类就可以了。首先初始化的时候,在这个aspect_blockMethodSignature
方法中,生成Block的NSMethodSignature,并且包装在AspectIdentifier对象中。后续再- (BOOL)invokeWithInfo:(id<AspectInfo>)info
中,生成hook方法的NSInvocation中,触发block的调用。下面贴上
AspectIdentifier
类中Block动态调用过程的注释。
+ (instancetype)identifierWithSelector:(SEL)selector object:(id)object options:(AspectOptions)options block:(id)block error:(NSError **)error {
NSCParameterAssert(block);
NSCParameterAssert(selector);
NSMethodSignature *blockSignature = aspect_blockMethodSignature(block, error); // TODO: check signature compatibility, etc.
if (!aspect_isCompatibleBlockSignature(blockSignature, object, selector, error)) {
return nil;
}
AspectIdentifier *identifier = nil;
if (blockSignature) {
identifier = [AspectIdentifier new];
// 方法的声明
identifier.selector = selector;
// block对象
identifier.block = block;
// block的签名信息
identifier.blockSignature = blockSignature;
identifier.options = options;
identifier.object = object; // weak
}
return identifier;
}
- (BOOL)invokeWithInfo:(id<AspectInfo>)info {
// 生成NSInvocation对象
NSInvocation *blockInvocation = [NSInvocation invocationWithMethodSignature:self.blockSignature];
// 交换之前的 invocation
NSInvocation *originalInvocation = info.originalInvocation;
// 参数个数
NSUInteger numberOfArguments = self.blockSignature.numberOfArguments;
// Be extra paranoid. We already check that on hook registration.
// 检查block中写了几个参数,如果比原方法个数还多,那么是有问题的
if (numberOfArguments > originalInvocation.methodSignature.numberOfArguments) {
AspectLogError(@"Block has too many arguments. Not calling %@", info);
return NO;
}
// The `self` of the block will be the AspectInfo. Optional.
// 如果block中有参数,那么先把info放到1位置上
if (numberOfArguments > 1) {
[blockInvocation setArgument:&info atIndex:1];
}
// 根据block中的参数个数,从原方法中逐个赋值过来
void *argBuf = NULL;
for (NSUInteger idx = 2; idx < numberOfArguments; idx++) {
const char *type = [originalInvocation.methodSignature getArgumentTypeAtIndex:idx];
NSUInteger argSize;
NSGetSizeAndAlignment(type, &argSize, NULL);
if (!(argBuf = reallocf(argBuf, argSize))) {
AspectLogError(@"Failed to allocate memory for block invocation.");
return NO;
}
[originalInvocation getArgument:argBuf atIndex:idx];
[blockInvocation setArgument:argBuf atIndex:idx];
}
// 执行block
[blockInvocation invokeWithTarget:self.block];
if (argBuf != NULL) {
free(argBuf);
}
return YES;
}
3、最后
学无止境。在源码分析中,还是要主要学思想,知识点,架构。欢迎各位大佬点评、斧正。