【问题标题】:Different behavior on iPhone Emulator and Real Device about Message ForwardingiPhone 模拟器和真实设备上关于消息转发的不同行为
【发布时间】:2013-01-13 09:49:03
【问题描述】:

我想使用消息转发让任何未实现的 getter 方法返回 0,而不是抛出无法识别的选择器异常。喜欢

MyClass *r = [[MyClass alloc] init];
NSNumber *n = (NSNumber *)r;
NSLog(@"%d", [n integerValue]); // output 0
NSLog(@"%f", [n doubleValue]); // output 0.00000
NSLog(@"%@", [n stringValue]); // output (null)

所以我写了这个例子:

#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    NSNumber *n = (NSNumber *)self;
    NSLog(@"%d", [n integerValue]);
    NSLog(@"%f", [n doubleValue]);
    NSLog(@"%@", [n stringValue]);

    return YES;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *ms = [super methodSignatureForSelector:aSelector];
    if(ms)
        return ms;

    // Q = uint64_t, so it should also works for double which is also 64bit
    return [NSMethodSignature signatureWithObjCTypes:"Q@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    uint64_t ZERO64 = 0;
    [anInvocation setReturnValue:&ZERO64];
}

在真机上输出结果是 0, 0.00000, (null),但在模拟器上是 0, NaN, (null)

所以 double 类型不能按预期工作。我的第一个想法是将 NSMethodSignature 更改为“d@:”(d 是双精度)

输出结果在设备和模拟器上都是正确的,但只有模拟器上发生了一些奇怪的事情。运行这段代码,它会在第 6 次循环中崩溃,并出现某种 CALayer 异常:

#pragma mark -
#pragma mark Application lifecycle

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {    

    for(NSInteger i = 0; i < 100; i++) {
        NSInteger t = [(NSNumber *)self integerValue];

        UIViewController *view = [[UIViewController alloc] init];
        // it always crash on the 6th loop on this line**
        UINavigationController *nc = [[UINavigationController alloc] initWithRootViewController:view];
    }

    return YES;
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *ms = [super methodSignatureForSelector:aSelector];
    if(ms)
        return ms;

    // we change to return double
    return [NSMethodSignature signatureWithObjCTypes:"d@:"];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    uint64_t ZERO64 = 0;
    [anInvocation setReturnValue:&ZERO64];
}

我很好奇两个问题,为什么在第一个示例中在模拟器上返回 NaN,以及在第二个示例中发生了什么?

【问题讨论】:

标签: ios objective-c objective-c-runtime


【解决方案1】:

对于你的第一个问题,这是我在模拟器上发现的

union {
    double d;
    uint64_t l;
} u;
NSNumber *n = (NSNumber *)self;
u.d = [n doubleValue];
NSLog(@"%f", u.d);  // nan
NSLog(@"%llx",u.l); // fff8000000000000
bzero(&u, sizeof(double));
NSLog(@"%f", u.d);  // 0.000000
NSLog(@"%llx",u.l); // 0

很明显,返回的是 NAN(fff8000000000000) 而不是 0.0。

要深入了解 [NSMethodSignature signatureWithObjCTypes:"d@:"][NSMethodSignature signatureWithObjCTypes:"Q@:"] 之间的区别,请查看此

NSLog(@"%@\n%@", [[NSMethodSignature signatureWithObjCTypes:"Q@:"] debugDescription], [[NSMethodSignature signatureWithObjCTypes:"d@:"] debugDescription]);

输出

<NSMethodSignature: 0x74a0950>
    number of arguments = 2
    frame size = 8
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (Q) 'Q'
        flags {}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 4, size adjust = 0}
        memory {offset = 0, size = 4}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 4, offset adjust = 0, size = 4, size adjust = 0}
        memory {offset = 0, size = 4}

<NSMethodSignature: 0x74a1e80>
    number of arguments = 2
    frame size = 8
    is special struct return? NO
    return value: -------- -------- -------- --------
        type encoding (d) 'd'
        flags {isFloat}    <<<<----- this flag should be set if the return value is float type
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 8, size adjust = 0}
        memory {offset = 0, size = 8}
    argument 0: -------- -------- -------- --------
        type encoding (@) '@'
        flags {isObject}
        modifiers {}
        frame {offset = 0, offset adjust = 0, size = 4, size adjust = 0}
        memory {offset = 0, size = 4}
    argument 1: -------- -------- -------- --------
        type encoding (:) ':'
        flags {}
        modifiers {}
        frame {offset = 4, offset adjust = 0, size = 4, size adjust = 0}
        memory {offset = 0, size = 4}

您可以在第二个方法签名上看到返回值有flags {isFloat}。我不是 x86 和 AMR 以及低级 ObjC 运行时方面的专家。但我认为 CPU 使用这个标志来识别返回值的类型。如果没有在 x86 CPU 上设置它,预期的浮点返回值因此被解释为 NAN。


对于您的第二个问题,我认为这是因为您告诉运行时它将返回一个 64 位大小的值,因此堆栈上的 64 位大小的内存被归零。但是,调用者期望返回大小为 32 位 (NSInteger)。因此会发生某种堆栈溢出并导致崩溃。


我实际上实现了类似的东西,旨在使NSNullnil 一样工作。

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
    if (signature)
        return signature;

    const Class forwardClasses[] = {[NSNumber class], [NSString class], [NSArray class], [NSOrderedSet class]}; // add new classes if you think the list is not enough

    for (int i = 0; i < sizeof(forwardClasses)/sizeof(Class); i++) {
        Class cls = forwardClasses[i];
        signature = [cls instanceMethodSignatureForSelector:aSelector];
        if (signature) {
            return signature;
        }
    }

    return signature;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    NSUInteger len = [[anInvocation methodSignature] methodReturnLength];
    char buff[len];
    bzero(buff, len);
    [anInvocation setReturnValue:buff];
}

【讨论】:

    【解决方案2】:

    如果您想使用消息转发让任何未实现的 getter 方法返回 0,而不是抛出无法识别的选择器异常,也许您可​​以改用 +resolveInstanceMethod ?

    这是一个返回 NSString 的示例。您必须对其进行调整以返回原语。如果你有麻烦,请告诉我。

    如果您使用的是 ARC,则还需要在 void* 上架设一座桥。

    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        NSString* name = NSStringFromSelector(sel);
    
        IMP imp = imp_implementationWithBlock((void*) objc_unretainedPointer(^(id me, BOOL selected)
        {
            return @"Hello!";
        }));
        class_addMethod(self, sel, imp, "@"); //The type '@' is an object. For int use 'i'. Google "obj-c runtime types" 
        return YES;
    }
    

    当我们使用class_addMethod时,第三个参数是类型代码。 . .解决它们的一个好方法是制作一个真正的方法,然后对其进行反省。这是一个返回类上(真实)选择器类型代码的实用程序: https://github.com/jasperblues/spring-objective-c/blob/master/Source/… – user404201 6 分钟前

    【讨论】:

    • 你无法制作返回正确类型值的块
    • @xlc0212,当我们使用class_addMethod时,第三个参数是类型代码。 . .解决它们的一个好方法是制作一个真正的方法,然后对其进行反省。您可以在此类上调用 typeCodesForSelector 方法:github.com/jasperblues/spring-objective-c/blob/master/Source/…
    • @xlc0212,我刚刚阅读了您的答案,它更好。 .也许我会把我的留在这里,因为这些信息仍然有用。
    猜你喜欢
    • 2011-05-01
    • 1970-01-01
    • 2022-12-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-03-26
    • 1970-01-01
    相关资源
    最近更新 更多