NSPredicate导致这种奇怪行为的原因是什么?

时间:2014-09-05 17:17:48

标签: objective-c nspredicate

我一直在写一些单元测试。一个对象具有readonly属性,该属性返回由文件系统填充的数组。为了使测试不依赖于用户文件系统,我在测试中使用调配来将此属性交换为将返回预定义数组的属性。

我注意到一些奇怪的行为,但有些测试失败了应该已经过去了。

我将其追踪到NSPredicate并设法隔离下面代码中的行为

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface SomeObject : NSObject
@property (readonly) NSArray *array;
@end

@implementation SomeObject
- (NSArray *)array {
    return @[];
}
@end

NSArray *array1(id self, SEL _cmd)
{
    return @[@"a", @"b", @"c"];
}

NSArray *array2(id self, SEL _cmd)
{
    return @[@"d", @"e", @"f"];
}

int main(int argc, char *argv[]) {
    @autoreleasepool {
        SomeObject *someObject = [[SomeObject alloc] init];
        NSLog(@"someObject.array: %@", someObject.array);
        NSPredicate *predicate;
        NSString *lookFor = @"a";

        //swap array for array1 function
        method_setImplementation(class_getInstanceMethod([someObject class], @selector(array)), (IMP)array1);
        predicate = [NSPredicate predicateWithFormat:@"ANY array == %@", lookFor];
        NSLog(@"contains: %@ in %@: %@", lookFor, someObject.array, [someObject.array containsObject:lookFor] ? @"YES" : @"NO");
        NSLog(@"NSPred: %@ in %@: %@", lookFor, someObject.array, [predicate evaluateWithObject:someObject] ? @"YES" : @"NO");

        //swap array for array2 function
        method_setImplementation(class_getInstanceMethod([someObject class], @selector(array)), (IMP)array2);
        predicate = [NSPredicate predicateWithFormat:@"ANY array == %@", lookFor];
        NSLog(@"contains: %@ in %@: %@", lookFor, someObject.array, [someObject.array containsObject:lookFor] ? @"YES" : @"NO");
        NSLog(@"NSPred: %@ in %@: %@", lookFor, someObject.array, [predicate evaluateWithObject:someObject] ? @"YES" : @"NO");
    }
}

输出

2014-09-05 18:05:10.169 Untitled[37520:507] someObject.array: (
)
2014-09-05 18:05:10.171 Untitled[37520:507] contains: a in (
    a,
    b,
    c
): YES
2014-09-05 18:05:10.171 Untitled[37520:507] NSPred: a in (
    a,
    b,
    c
): YES
2014-09-05 18:05:10.172 Untitled[37520:507] contains: a in (
    d,
    e,
    f
): NO
2014-09-05 18:05:10.172 Untitled[37520:507] NSPred: a in (
    d,
    e,
    f
): YES

最后显示谓词认为a包含在数组[d, e, f]中。

这里究竟发生了什么,如何更改测试以使其按预期运行?

1 个答案:

答案 0 :(得分:1)

这是一个稍微简单的测试,可能有助于更多地解决潜在的问题:

// Assumes the SomeObject class and array1 and array2 IMPs are defined as
// shown in the example code in the OP's question.
//
- (void)testKVCAfterMethodSwizzle
{
    SomeObject *obj = [[SomeObject alloc] init];
    Method method = class_getInstanceMethod(obj.class, @selector(array));

    method_setImplementation(method, (IMP)array1);
    // NSLog(@"%@ %@", obj.array, [obj valueForKey:@"array"]);

    method_setImplementation(method, (IMP)array2);
    NSLog(@"%@ %@", obj.array, [obj valueForKey:@"array"]);
}

尝试运行一次,然后取消注释第一个NSLog语句并再次运行它。初始日志输出应如下所示:

2014-09-05 15:51:22.814 xctest[67804:303] (
    d,
    e,
    f
) (
    d,
    e,
    f
)

但是,一旦取消注释第一个NSLog语句,第二个日志的输出将如下所示:

2014-09-05 15:58:38.989 xctest[67837:303] (
    d,
    e,
    f
) (
    a,
    b,
    c
)

显然,KVC(NSPredicate的实现所依赖的)缓存方法IMP本身以减少后续调用的开销。这可能是Apple似乎不鼓励在生产代码中使用调配的原因之一。如果你必须调整方法,最好的时间可能是在课程加载期间或之后。

修改

此问题的一种可能的解决方法是定义swizzled方法的更一般的实现。例如,您可以编写一个简单返回全局变量值的混合实现,例如:

static NSArray *ArrayToReturn;

NSArray *selectedArray(id self, SEL _cmd)
{
    return ArrayToReturn;
}

您的测试用例类可以覆盖+load以应用swizzle:

+ (void)load
{
    Method method = class_getInstanceMethod([SomeObject class], @selector(array));
    method_setImplementation(method, (IMP)selectedArray);
}

然后你的测试方法看起来像这样:

- (void)testSwizzleWithSelectedArray1
{
    ArrayToReturn = @[@"a", @"b", @"c"];

    SomeObject *obj = [[SomeObject alloc] init];
    XCTAssertEqualObjects(obj.array, [obj valueForKey:@"array"]);
}

- (void)testSwizzleWithSelectedArray2
{
    ArrayToReturn = @[@"d", @"e", @"f"];

    SomeObject *obj = [[SomeObject alloc] init];
    XCTAssertEqualObjects(obj.array, [obj valueForKey:@"array"]);
}