Objective C 添加方法到 类 参数不匹配

Objective C adding methods to classes argument mismatch

我正在编写这段代码,基本上是向 NSObject 添加一个块:

class_addMethod(object_getClass([NSObject class]), @selector(toUpper2:), imp_implementationWithBlock(^NSString*(id self, SEL _cmd, NSString* s) {
        NSLog(@"self: %@", self);
        NSLog(@"_cmd: %@", _cmd); // I know %@ is not SEL, but look for yourself
        NSLog(@"s: %@", s);
        return [s uppercaseString];
    }), "@@:@"); // the type signature was created using @encode

对我来说,这看起来很无辜,但如果我这样做:(我也在另一个 class 中定义了一个 +toUpper2,这样编译器就不会抱怨):

[(id)[NSObject class] toUpper2:@"hallo"];

发生这种情况:

2018-07-06 16:45:52.302676+0200 xctest[43736:32056962] self: NSObject
2018-07-06 16:45:52.302706+0200 xctest[43736:32056962] _cmd: hallo
2018-07-06 16:45:52.302721+0200 xctest[43736:32056962] s: (null)

如您所见,参数被搞乱了。更重要的是,如果我使用 performSelector 执行相同的方法,例如:

[NSObject performSelector:@selector(toUpper2:) withObject:@"hallo"];

然后,事情变得更加误入歧途:

2018-07-06 16:45:52.302737+0200 xctest[43736:32056962] self: NSObject
2018-07-06 16:45:52.302751+0200 xctest[43736:32056962] _cmd: hallo
2018-07-06 16:45:52.302763+0200 xctest[43736:32056962] s: hallo

谁能解释这种行为?

此致, 杰克

我自己没有用过这个方法,但我觉得你误解了这里的API。

docs 描述了 block 参数的结构。

block
The block that implements this method. The signature of block should be method_return_type ^(id self, self, method_args …). The selector of the method is not available to block. block is copied with Block_copy().

这里的重点是我的,但应该很清楚你的 SEL 论点在这里不合适。我不完全确定为什么他们在描述中重复 self,特别是考虑到您通常会看到 method_args 从该参数索引开始。

文档有误,已提交错误 (41908695)。 objc头文件正确:

/** 
 * Creates a pointer to a function that will call the block
 * when the method is called.
 * 
 * @param block The block that implements this method. Its signature should
 *  be: method_return_type ^(id self, method_args...). 
 *  The selector is not available as a parameter to this block.
 *  The block is copied with \c Block_copy().
 * 
 * @return The IMP that calls this block. Must be disposed of with
 *  \c imp_removeBlock.
 */
OBJC_EXPORT IMP _Nonnull
imp_implementationWithBlock(id _Nonnull block)
    OBJC_AVAILABLE(10.7, 4.3, 9.0, 1.0, 2.0);

之所以这样实现,速度速度速度。和简单。

具体来说,一个方法分解为一个 总是 需要 至少 两个参数的 C 函数; self_cmd,两者都发生在指针上。接下来是 0...N 任意参数,这些参数将按照目标体系结构 ABI 的定义任意打包到寄存器或堆栈上。

另一方面,块调用站点总是分解为C函数调用,其中函数有一个保证的参数;对块的引用。正是这个引用,编译器可以通过它发出代码来引用捕获的状态(如果有的话)。与方法一样,块的参数后跟任意参数列表。

现在,在任何体系结构上重新编码参数列表都是一场噩梦。速度慢,容易出错,而且极其复杂。

为了避免这种情况,imp_implementationWithBlock() 在幕后做了一些魔术 returns 一个函数指针,在调用时, 将第一个参数视为指针(应该是 self) 到第二个参数的槽中(覆盖 _cmd,将块引用推到第一个参数的槽中,然后尾调用块的代码。

该块不知道它是作为方法调用的。并且 objc 运行时不知道方法调用蹦床到一个块。