0

我在一个个人项目中使用了 Objective-C++,我发现SEL在全局范围内带有初始化程序的变量有奇怪的行为。考虑这个可运行的 Objective-C++ 片段:

#import <Foundation/Foundation.h>

@interface Foo : NSObject
-(void)closeWindow;
@end

@implementation Foo
-(void)closeWindow { puts("closeWindow called"); }
@end

static SEL globalSelector = @selector(closeWindow);

void printSelectorInfo(id target, SEL sel) {
    const char* name = sel_getName(sel);
    BOOL responds = [target respondsToSelector:sel];
    printf("selector=%p; name=%s; responds=%hhu\n", sel, name, responds);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        SEL localSelector = @selector(closeWindow);
        Foo* foo = [[Foo alloc] init];

        printSelectorInfo(foo, localSelector);
        printSelectorInfo(foo, globalSelector);

        [foo closeWindow];
        [foo performSelector:localSelector];
        [foo performSelector:globalSelector];
    }
}

在普通的 Objective-C 中,全局变量必须具有 C 规定的常量初始化器,因此static SEL globalSelector = @selector(closeWindow)无效。这个限制在 C++ 中被取消,因此在 Objective-C++ 中,它可以毫无问题地编译。

这将是预期的输出:

选择器=<某个地址>; 名称=关闭窗口;响应=1
选择器=<某个地址>;名称=关闭窗口;响应=1
closeWindow 调用 [3 次]

这确实是我在调试中得到的:

selector=0x7fff952d63a1; name=closeWindow; responds=1  
selector=0x7fff952d63a1; name=closeWindow; responds=1  
closeWindow called  
closeWindow called  
closeWindow called

但是,Release 中出现了问题:

selector=0x7fff952d63a1; name=closeWindow; responds=1
selector=0x100000eca; name=closeWindow; responds=0
closeWindow called
closeWindow called
2013-05-06 16:40:11.960 selectors[5048:303] *** NSForwarding: warning: selector (0x100000eca) for message 'closeWindow' does not match selector known to Objective C runtime (0x7fff952d63a1)-- abort
2013-05-06 16:40:11.964 selectors[5048:303] -[Foo closeWindow]: unrecognized selector sent to instance 0x100108240
2013-05-06 16:40:11.966 selectors[5048:303] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo closeWindow]: unrecognized selector sent to instance 0x100108240'
*** First throw call stack:
(
    0   CoreFoundation                      0x00007fff91116b06 __exceptionPreprocess + 198
    1   libobjc.A.dylib                     0x00007fff904843f0 objc_exception_throw + 43
    2   CoreFoundation                      0x00007fff911ad40a -[NSObject(NSObject) doesNotRecognizeSelector:] + 186
    3   CoreFoundation                      0x00007fff9110502e ___forwarding___ + 414
    4   CoreFoundation                      0x00007fff91104e18 _CF_forwarding_prep_0 + 232
    5   selectors                           0x0000000100000e14 main + 234
    6   libdyld.dylib                       0x00007fff944a77e1 start + 0
    7   ???                                 0x0000000000000001 0x0 + 1
)
libc++abi.dylib: terminate called throwing an exception

请注意,虽然它们仍然具有相同的名称,但选择器没有相同的地址,并且Foo只响应局部变量中的选择器。

有趣的是,这个问题似乎与名称有关。如果我将方法的名称更改为,例如foo,它可以正常工作。

我是否缺少某些东西或依赖未定义的行为?

4

2 回答 2

0

由于没有官方的 Objective-C++ 标准,我无法验证这是否应该是明确定义的行为。也就是说,我觉得这已经进入了编译器错误的领域,所以我一直在使用它作为一种解决方法:

/* reuse same heading as question */

static SEL globalSelector = @selector(closeWindow);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Foo* foo = [[Foo alloc] init];

        SEL localSelector = globalSelector;
        if (![foo respondsToSelector:localSelector])
        {
            localSelector = sel_getUID(sel_getName(localSelector));
        }

        [foo performSelector:localSelector];
    }
}

LLVM 的人员确认这是Xcode 4.6.2 附带的 Clang 版本的一个错误,它将在下一个版本中修复:

你在这个版本的 Xcode 中遇到了一个错误。我可以确认该错误已在 Xcode 的下一个版本中修复。在下一个版本和 -Os 中,printSelectorInfo 都是内联的,并且不会对其进行调用。您使用 -Os 注意到的是 printSelectorInfo 的某种尾调用优化,这对于您注意到的错误应该无关紧要。

于 2013-05-08T15:01:10.190 回答
-1

编译选择器 (SEL) 不是函数指针,它们是标识方法名称的对象。

对象.h

typedef struct objc_selector    *SEL;

苹果的文档

方法和选择器

编译的选择器识别方法名称,而不是方法实现。例如,一个类的显示方法与其他类中定义的显示方法具有相同的选择器。这对于多态性和动态绑定是必不可少的;它使您可以将相同的消息发送给属于不同类别的接收者。如果每个方法实现有一个选择器,则消息与函数调用没有什么不同。

同名的类方法和实例方法被分配相同的选择器。但是,由于它们的域不同,两者之间没有混淆。除了显示实例方法外,一个类还可以定义一个显示类方法。

当我将上面的示例添加到我的示例应用程序行时,无法编译:

static SEL globalSelector = @selector(closeWindow);
    error: initializer element is not a compile-time constant

我也收到警告

[foo performSelector:localSelector];
    warning: performSelector may cause a leak because its selector is unknown 

我没有收到任何警告

[foo performSelector:@selector(closeWindow)];

我的理解是 @selector(closeWindow) 是一个编译时指令,将 closeWindow 标识为选择器,但 objc_selector 结构的查找和创建是在运行时完成的。C++ 默认是静态链接的(而且速度更快),而 Objective-C 是动态的。

有趣的花絮

http://cocoasamurai.blogspot.com/2010/01/understanding-objective-c-runtime.html

[目标 doMethodWith:var1]; 被翻译成 objc_msgSend(target,@selector(doMethodWith:),var1); 由编译器

http://www.friday.com/bbum/2009/12/18/objc_msgsend-part-1-the-road-map/

显然,objc_msgSend() 被调用了 10s 几百万次,只是引导系统并启动一些应用程序。因此,每个周期都很重要,毫无疑问,objc_msgSend() 是用手动调整的程序集编写的。

objc_msgSend() 旨在动态确定方法的实现——作为与目标实例上的选择器绑定的 IMP 的函数指针——而不改变任何调用者/被调用者状态。这启用了尾调用优化,允许 objc_msgSend() 直接跳转 [JMP] 到方法的实现。这也是您在回溯中看不到 objc_msgSend() 的原因,除非 objc_msgSend() 发生崩溃

于 2013-05-06T23:58:22.490 回答